feat: add installable Lingji Android shell
This commit is contained in:
@@ -23,6 +23,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
package-manager-cache: false
|
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
|
- name: Set up Gradle
|
||||||
uses: gradle/actions/setup-gradle@v6
|
uses: gradle/actions/setup-gradle@v6
|
||||||
- name: Run local quality gates
|
- name: Run local quality gates
|
||||||
|
|||||||
@@ -30,19 +30,19 @@ tools are unavailable or insufficient. State the fallback in the handoff.
|
|||||||
|
|
||||||
## Current build boundary
|
## Current build boundary
|
||||||
|
|
||||||
The repository uses `brainwave` only as a code name. Formal product name,
|
The repository keeps `brainwave` only as its code name. The accepted Android
|
||||||
organization-owned application ID, and final `minSdk` remain product decisions.
|
identity is product name `灵机`, namespace/application ID `net.opcapp.flash`, and
|
||||||
Until they are accepted, the `app` project is a Kotlin/JVM domain harness, not a
|
`minSdk` 26. The `app` project is an installable Android application; the app icon
|
||||||
publishable Android application. Do not invent placeholder release identity.
|
and store assets remain product decisions and must not be silently finalized.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
Run from the repository root:
|
Run from the repository root:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\gradlew.bat verifyLocal --offline
|
.\gradlew.bat verifyLocal
|
||||||
```
|
```
|
||||||
|
|
||||||
After the Android application plugin is configured, also run the Android gates
|
`verifyLocal` includes Android lint, debug unit tests, APK assembly, repository
|
||||||
listed in `docs/quality-gates.md`. Report skipped device or Android checks
|
checks, and dependency-license coverage. Run `connectedDebugAndroidTest` separately
|
||||||
explicitly; do not imply they passed.
|
when an authorized device is present, and report skipped device checks explicitly.
|
||||||
|
|||||||
+122
-19
@@ -1,26 +1,110 @@
|
|||||||
import groovy.json.JsonSlurper
|
import groovy.json.JsonSlurper
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
plugins {
|
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 {
|
kotlin {
|
||||||
jvmToolchain(17)
|
jvmToolchain(17)
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main {
|
|
||||||
kotlin.srcDir("src/main/java")
|
|
||||||
}
|
|
||||||
test {
|
|
||||||
kotlin.srcDir("src/test/java")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
correctErrorTypes = true
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<KotlinCompile>().configureEach {
|
||||||
|
kotlinOptions.jvmTarget = "17"
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
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)
|
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<Test>().configureEach {
|
||||||
useJUnit()
|
useJUnit()
|
||||||
testLogging {
|
testLogging {
|
||||||
events("failed", "skipped")
|
events("failed", "skipped")
|
||||||
@@ -30,25 +114,44 @@ tasks.test {
|
|||||||
|
|
||||||
tasks.register("verifyDependencyLicenses") {
|
tasks.register("verifyDependencyLicenses") {
|
||||||
group = "verification"
|
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 {
|
doLast {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
val manifest = JsonSlurper().parse(rootProject.file("config/dependency-licenses.json")) as Map<String, Any>
|
val manifest = JsonSlurper().parse(rootProject.file("config/dependency-licenses.json")) as Map<String, Any>
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
val components = manifest.getValue("components") as List<Map<String, Any>>
|
val components = manifest.getValue("components") as List<Map<String, Any>>
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val groupPolicies = manifest.getValue("groupPolicies") as List<Map<String, Any>>
|
||||||
val reviewedCoordinates = components.mapNotNull { it["coordinates"] as String? }.toSet()
|
val reviewedCoordinates = components.mapNotNull { it["coordinates"] as String? }.toSet()
|
||||||
val resolvedCoordinates = configurations.getByName("testRuntimeClasspath")
|
val resolvedCoordinates = listOf("debugRuntimeClasspath", "debugUnitTestRuntimeClasspath")
|
||||||
.resolvedConfiguration
|
.flatMap { configurationName ->
|
||||||
.resolvedArtifacts
|
configurations.getByName(configurationName)
|
||||||
.map { artifact ->
|
.resolvedConfiguration
|
||||||
val id = artifact.moduleVersion.id
|
.resolvedArtifacts
|
||||||
"${id.group}:${id.name}:${id.version}"
|
.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()
|
.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()) {
|
check(missing.isEmpty()) {
|
||||||
"Dependencies missing reviewed license entries: ${missing.sorted().joinToString()}"
|
"Dependencies missing reviewed license entries: ${missing.sorted().joinToString()}"
|
||||||
}
|
}
|
||||||
|
logger.lifecycle(
|
||||||
|
"Dependency license coverage passed for ${resolvedCoordinates.size} artifacts " +
|
||||||
|
"with ${groupPolicies.size} reviewed group policies.",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
# Project-specific R8 rules will be added when release minification is enabled.
|
||||||
@@ -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<MainActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun domainFixtureCanBeVerifiedFromHome() {
|
||||||
|
composeRule.onNodeWithText("灵机").assertIsDisplayed()
|
||||||
|
composeRule.onNodeWithText("验证起卦核心").performClick()
|
||||||
|
composeRule.onNodeWithText("复 24 → 坤 2").assertIsDisplayed()
|
||||||
|
composeRule.onNodeWithText("初爻动").assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".app.LingjiApplication"
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.Lingji"
|
||||||
|
android:usesCleartextTraffic="false">
|
||||||
|
<activity
|
||||||
|
android:name=".app.MainActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package net.opcapp.flash.app
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
|
||||||
|
@HiltAndroidApp
|
||||||
|
class LingjiApplication : Application()
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package brainwave.core.model
|
package net.opcapp.flash.core.model
|
||||||
|
|
||||||
data class HexagramContent(
|
data class HexagramContent(
|
||||||
val kingWenNumber: Int,
|
val kingWenNumber: Int,
|
||||||
+2
-2
@@ -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 {
|
interface HexagramContentRepository {
|
||||||
val contentVersion: String
|
val contentVersion: String
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package brainwave.domain.casting
|
package net.opcapp.flash.domain.casting
|
||||||
|
|
||||||
data class CastComputation internal constructor(
|
data class CastComputation internal constructor(
|
||||||
val roundsBottomUp: List<CastRound>,
|
val roundsBottomUp: List<CastRound>,
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package brainwave.domain.casting
|
package net.opcapp.flash.domain.casting
|
||||||
|
|
||||||
data class CastRecordDto(
|
data class CastRecordDto(
|
||||||
val schemaVersion: Int,
|
val schemaVersion: Int,
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package brainwave.domain.casting
|
package net.opcapp.flash.domain.casting
|
||||||
|
|
||||||
enum class CoinSide(val score: Int) {
|
enum class CoinSide(val score: Int) {
|
||||||
CHARACTER(2),
|
CHARACTER(2),
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package brainwave.domain.casting
|
package net.opcapp.flash.domain.casting
|
||||||
|
|
||||||
object HexagramCatalog {
|
object HexagramCatalog {
|
||||||
private enum class Trigram(val patternBottomUp: String) {
|
private enum class Trigram(val patternBottomUp: String) {
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Polarity>,
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.Lingji" parent="android:style/Theme.Material.NoActionBar">
|
||||||
|
<item name="android:fontFamily">sans</item>
|
||||||
|
<item name="android:statusBarColor">#1E1B18</item>
|
||||||
|
<item name="android:navigationBarColor">#1E1B18</item>
|
||||||
|
<item name="android:windowLightStatusBar">false</item>
|
||||||
|
<item name="android:windowActionModeOverlay">true</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">灵机</string>
|
||||||
|
<string name="development_build">内部构建 · 开发验证页</string>
|
||||||
|
<string name="home_tagline">本机起卦 · 离线可用</string>
|
||||||
|
<string name="seal_character">易</string>
|
||||||
|
<string name="core_check_title">领域核心已接入</string>
|
||||||
|
<string name="core_check_body">用固定六爻夹具验证卦象、变卦与动爻计算链路。</string>
|
||||||
|
<string name="verify_casting_core">验证起卦核心</string>
|
||||||
|
<string name="local_data_notice">当前验证页不写入记录。正式功能将默认保存在本机,只有明确发起 AI 解读时才会联网。</string>
|
||||||
|
<string name="back">返回</string>
|
||||||
|
<string name="core_check_passed">计算通过</string>
|
||||||
|
<string name="fixture_result">复 %1$d → 坤 %2$d</string>
|
||||||
|
<string name="moving_line_result">初爻动</string>
|
||||||
|
<string name="fixture_explanation">固定输入为初爻老阳、其余五爻少阴。结果来自纯 Kotlin 起卦核心,不是界面写死的演示值。</string>
|
||||||
|
<string name="primary_hexagram_description">本卦复,第二十四卦</string>
|
||||||
|
<string name="transformed_hexagram_description">变卦坤,第二卦</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.Lingji" parent="android:style/Theme.Material.Light.NoActionBar">
|
||||||
|
<item name="android:fontFamily">sans</item>
|
||||||
|
<item name="android:statusBarColor">#F5F0E6</item>
|
||||||
|
<item name="android:navigationBarColor">#F5F0E6</item>
|
||||||
|
<item name="android:windowLightStatusBar">true</item>
|
||||||
|
<item name="android:windowActionModeOverlay">true</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<full-backup-content>
|
||||||
|
<exclude domain="root" path="." />
|
||||||
|
<exclude domain="file" path="." />
|
||||||
|
<exclude domain="database" path="." />
|
||||||
|
<exclude domain="sharedpref" path="." />
|
||||||
|
<exclude domain="external" path="." />
|
||||||
|
</full-backup-content>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<data-extraction-rules>
|
||||||
|
<cloud-backup disableIfNoEncryptionCapabilities="true">
|
||||||
|
<exclude domain="root" path="." />
|
||||||
|
<exclude domain="file" path="." />
|
||||||
|
<exclude domain="database" path="." />
|
||||||
|
<exclude domain="sharedpref" path="." />
|
||||||
|
<exclude domain="external" path="." />
|
||||||
|
</cloud-backup>
|
||||||
|
<device-transfer>
|
||||||
|
<exclude domain="root" path="." />
|
||||||
|
<exclude domain="file" path="." />
|
||||||
|
<exclude domain="database" path="." />
|
||||||
|
<exclude domain="sharedpref" path="." />
|
||||||
|
<exclude domain="external" path="." />
|
||||||
|
</device-transfer>
|
||||||
|
</data-extraction-rules>
|
||||||
+2
-2
@@ -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(
|
class FakeHexagramContentRepository(
|
||||||
override val contentVersion: String = "fixture-only-v1",
|
override val contentVersion: String = "fixture-only-v1",
|
||||||
+2
-2
@@ -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.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Assert.fail
|
import org.junit.Assert.fail
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package brainwave.domain.casting
|
package net.opcapp.flash.domain.casting
|
||||||
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
+7
-1
@@ -2,6 +2,10 @@ import org.gradle.api.tasks.Exec
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
base
|
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(
|
fun registerNodeVerificationTask(
|
||||||
@@ -61,7 +65,9 @@ tasks.register("verifyLocal") {
|
|||||||
group = LifecycleBasePlugin.VERIFICATION_GROUP
|
group = LifecycleBasePlugin.VERIFICATION_GROUP
|
||||||
description = "Runs every environment-independent local quality gate."
|
description = "Runs every environment-independent local quality gate."
|
||||||
dependsOn(
|
dependsOn(
|
||||||
":app:test",
|
":app:lintDebug",
|
||||||
|
":app:testDebugUnitTest",
|
||||||
|
":app:assembleDebug",
|
||||||
":app:verifyDependencyLicenses",
|
":app:verifyDependencyLicenses",
|
||||||
verifyDocs,
|
verifyDocs,
|
||||||
scanSecrets,
|
scanSecrets,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"reviewedAt": "2026-08-04",
|
"reviewedAt": "2026-08-07",
|
||||||
"scopeNote": "Current JVM runtime/test graph and direct build entry points; Android dependencies are not configured yet.",
|
"scopeNote": "Android debug runtime and debug unit-test runtime graphs, plus direct build entry points. Release notices still require a release-graph review.",
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"id": "gradle-wrapper",
|
"id": "gradle-wrapper",
|
||||||
@@ -11,6 +11,14 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"source": "https://github.com/gradle/gradle/blob/v8.2.0/LICENSE"
|
"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",
|
"id": "kotlin-gradle-plugin",
|
||||||
"coordinates": null,
|
"coordinates": null,
|
||||||
@@ -19,6 +27,54 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"source": "https://github.com/JetBrains/kotlin/blob/v1.9.20/license/LICENSE.txt"
|
"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",
|
"id": "kotlin-stdlib",
|
||||||
"coordinates": "org.jetbrains.kotlin:kotlin-stdlib:1.9.20",
|
"coordinates": "org.jetbrains.kotlin:kotlin-stdlib:1.9.20",
|
||||||
@@ -29,11 +85,35 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "jetbrains-annotations",
|
"id": "jetbrains-annotations",
|
||||||
"coordinates": "org.jetbrains:annotations:13.0",
|
"coordinates": "org.jetbrains:annotations:23.0.0",
|
||||||
"version": "13.0",
|
"version": "23.0.0",
|
||||||
"scope": "runtime-transitive",
|
"scope": "runtime-transitive",
|
||||||
"license": "Apache-2.0",
|
"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",
|
"id": "junit4",
|
||||||
@@ -51,5 +131,35 @@
|
|||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"source": "https://github.com/hamcrest/JavaHamcrest/blob/hamcrest-java-1.3/LICENSE.txt"
|
"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"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -1,14 +1,14 @@
|
|||||||
# Brainwave 项目文档
|
# Brainwave 项目文档
|
||||||
|
|
||||||
> 文档状态:方案基线
|
> 文档状态:方案基线
|
||||||
> 最后核验:2026-08-04
|
> 最后核验:2026-08-07
|
||||||
> 当前阶段:P-1 v0.3 待视觉复核;P1 纯 Kotlin 领域核心已完成;P0 仓库门禁与 P2 内容契约已部分完成;可发布 Android 壳仍等待正式身份与 `minSdk` 决策
|
> 当前阶段:正式产品名“灵机”;P0 Android 壳与本地门禁已建立,真机闭环正在执行;P-1 v0.3 待品牌文案刷新和视觉复核;P1 已完成;P2 授权内容未开始
|
||||||
|
|
||||||
本目录是 Brainwave 的项目知识事实源。产品决策、领域算法、架构边界、验收标准和已知失败模式必须写入仓库;聊天记录、口头约定和临时提示不构成项目规范。
|
本目录是 Brainwave 的项目知识事实源。产品决策、领域算法、架构边界、验收标准和已知失败模式必须写入仓库;聊天记录、口头约定和临时提示不构成项目规范。
|
||||||
|
|
||||||
## 项目一句话
|
## 项目一句话
|
||||||
|
|
||||||
Brainwave 是一个以《易经》三枚铜币法为文化背景的 Android 个人反思工具:用户亲手投掷并录入六次结果,应用在本地确定本卦、之卦和动爻;用户主动点击「解」后,本地内容或 AI 才把既有结果翻译成现代语言,并收束到一项低风险、可撤销的现实行动。
|
灵机(仓库代号 Brainwave)是一个以《易经》三枚铜币法为文化背景的 Android 个人反思工具:用户亲手投掷并录入六次结果,应用在本地确定本卦、之卦和动爻;用户主动点击「解」后,本地内容或 AI 才把既有结果翻译成现代语言,并收束到一项低风险、可撤销的现实行动。
|
||||||
|
|
||||||
## 文档地图
|
## 文档地图
|
||||||
|
|
||||||
@@ -69,6 +69,7 @@ Brainwave 是一个以《易经》三枚铜币法为文化背景的 Android 个
|
|||||||
已确认:
|
已确认:
|
||||||
|
|
||||||
- 目标平台为原生 Android。
|
- 目标平台为原生 Android。
|
||||||
|
- 正式产品名为“灵机”,namespace/application ID 为 `net.opcapp.flash`,`minSdk=26`。
|
||||||
- 使用 Kotlin、Jetpack Compose 和单 Activity 架构。
|
- 使用 Kotlin、Jetpack Compose 和单 Activity 架构。
|
||||||
- 起卦完全在本地完成,AI 不得参与或更改起卦结果。
|
- 起卦完全在本地完成,AI 不得参与或更改起卦结果。
|
||||||
- 用户真实投币并录入;MVP 不提供随机起卦按钮。
|
- 用户真实投币并录入;MVP 不提供随机起卦按钮。
|
||||||
|
|||||||
@@ -39,11 +39,11 @@ Android 官方建议新应用采用清晰的 UI/数据分层、单向数据流
|
|||||||
工程初始化后建议采用:
|
工程初始化后建议采用:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
app/src/main/java/<package>/
|
app/src/main/java/net/opcapp/flash/
|
||||||
├── app/
|
├── app/
|
||||||
│ ├── BrainwaveApplication.kt
|
│ ├── LingjiApplication.kt
|
||||||
│ ├── MainActivity.kt
|
│ ├── MainActivity.kt
|
||||||
│ └── BrainwaveNavHost.kt
|
│ └── LingjiApp.kt
|
||||||
├── core/
|
├── core/
|
||||||
│ ├── model/ # 跨层不可变领域模型
|
│ ├── model/ # 跨层不可变领域模型
|
||||||
│ ├── designsystem/ # 主题、字体、间距、卦象 Canvas
|
│ ├── designsystem/ # 主题、字体、间距、卦象 Canvas
|
||||||
@@ -180,7 +180,7 @@ App → HTTPS backend → model provider
|
|||||||
- 禁止直接引入完整“算命 SDK”或无法审计的卦象计算库。
|
- 禁止直接引入完整“算命 SDK”或无法审计的卦象计算库。
|
||||||
- Compose 依赖使用 BOM 对齐版本。
|
- Compose 依赖使用 BOM 对齐版本。
|
||||||
- `compileSdk`/`targetSdk` 使用实现时最新稳定且满足商店要求的版本,不在方案文档硬编码会迅速过期的数值。
|
- `compileSdk`/`targetSdk` 使用实现时最新稳定且满足商店要求的版本,不在方案文档硬编码会迅速过期的数值。
|
||||||
- `minSdk` 默认提案为 26,最终值见[决策记录](decisions.md#未决问题)。
|
- `minSdk=26` 已由 [ADR-014](decisions.md#adr-014正式-android-身份与最低版本) 确认;提高最低版本必须以依赖要求或覆盖率数据另立决策。
|
||||||
|
|
||||||
## 11. 多模块化触发条件
|
## 11. 多模块化触发条件
|
||||||
|
|
||||||
|
|||||||
+12
-3
@@ -131,13 +131,22 @@
|
|||||||
- 后果:`spotlessCheck` 当前是仓库内的无依赖格式兼容入口,不表示已引入 Spotless 插件。Android 壳建立后必须把 lint/debug build 加入 `verifyLocal`;将来若采用维护良好的专用插件,应保留命令兼容或同步更新 CI 与文档。
|
- 后果:`spotlessCheck` 当前是仓库内的无依赖格式兼容入口,不表示已引入 Spotless 插件。Android 壳建立后必须把 lint/debug build 加入 `verifyLocal`;将来若采用维护良好的专用插件,应保留命令兼容或同步更新 CI 与文档。
|
||||||
- 复审触发:Android 工具链启用、现有脚本无法表达新边界,或专用工具能以可接受成本提供明显更强的检查。
|
- 复审触发: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 | 问题 | 推荐默认 | 阻塞阶段 |
|
| ID | 问题 | 推荐默认 | 阻塞阶段 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| TBD-001 | 正式产品名与应用图标 | Brainwave 仅作代码代号 | P0 商店配置 |
|
| TBD-001 | 应用图标与商店素材 | 产品名已由 ADR-014 确认为“灵机”;图标不使用未确认成稿 | P6 商店配置 |
|
||||||
| TBD-002 | application ID | 使用组织所有的反向域名 | P0 |
|
|
||||||
| TBD-003 | minSdk | 26;创建工程时复核覆盖率和依赖要求 | P0 |
|
|
||||||
| TBD-004 | 问题是否允许留空 | 允许选择“不写具体内容”,但需显式操作 | P3 |
|
| TBD-004 | 问题是否允许留空 | 允许选择“不写具体内容”,但需显式操作 | P3 |
|
||||||
| TBD-005 | 经典原文、现代白话的版本与授权 | 自有白话 + 可核验公版原文 | P2,发布阻塞 |
|
| TBD-005 | 经典原文、现代白话的版本与授权 | 自有白话 + 可核验公版原文 | P2,发布阻塞 |
|
||||||
| TBD-006 | 乾用九、坤用六是否纳入 MVP | 内容具备时展示 | P2/P3 |
|
| TBD-006 | 乾用九、坤用六是否纳入 MVP | 内容具备时展示 | P2/P3 |
|
||||||
|
|||||||
+33
-10
@@ -1,17 +1,40 @@
|
|||||||
# 依赖与许可证清单
|
# 依赖与许可证清单
|
||||||
|
|
||||||
> 状态:当前 JVM harness 已核验;Android application 依赖尚未配置
|
> 状态:Android debug runtime 与 debug unit-test runtime 依赖图已建立自动覆盖门禁
|
||||||
> 适用范围:实际解析的 JVM runtime/test graph,以及直接使用的构建入口
|
> 适用范围:实际解析的 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 | 权威许可来源 |
|
| ID | 版本 | 当前范围 | SPDX | 权威许可来源 |
|
||||||
|---|---:|---|---|---|
|
|---|---:|---|---|---|
|
||||||
| `gradle-wrapper` | 8.2 | 构建入口 | Apache-2.0 | [Gradle 8.2 license](https://github.com/gradle/gradle/blob/v8.2.0/LICENSE) |
|
| `gradle-wrapper` | 8.2 | 构建入口 | Apache-2.0 | [Gradle 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) |
|
| `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-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) |
|
| `kotlin-gradle-plugin` | 1.9.20 | 构建插件 | Apache-2.0 | [Kotlin 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) |
|
| `hilt-gradle-plugin` | 2.48.1 | 构建插件 | Apache-2.0 | [Dagger/Hilt license](https://github.com/google/dagger/blob/dagger-2.48.1/LICENSE.txt) |
|
||||||
| `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) |
|
| `compose-bom` | 2023.10.01 | 运行时版本约束 | Apache-2.0 | [AndroidX license](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/LICENSE.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) |
|
| `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 系统字体和代码绘制的卦象;这句话只描述本提交时的资源图,不构成未来授权。
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
本文件定义本项目唯一允许的计算规则。UI 文案、AI 输出和数据源都不能覆盖这些规则。
|
本文件定义本项目唯一允许的计算规则。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. 术语和类型
|
## 1. 术语和类型
|
||||||
|
|
||||||
|
|||||||
+14
-17
@@ -1,7 +1,7 @@
|
|||||||
# 本地开发环境基线
|
# 本地开发环境基线
|
||||||
|
|
||||||
> 状态:已从当前主机实测
|
> 状态:已从当前主机实测
|
||||||
> 最后核验:2026-08-04
|
> 最后核验:2026-08-07
|
||||||
> 适用范围:`D:\OPC\brainwave` 的 Android 开发、构建和设备验证
|
> 适用范围:`D:\OPC\brainwave` 的 Android 开发、构建和设备验证
|
||||||
|
|
||||||
本文件记录项目当前可用的本地工具链,不是期望环境清单。后续实现必须先使用这里已经确认的能力;需要升级或安装新工具时,应说明原因并在完成后更新本文件。
|
本文件记录项目当前可用的本地工具链,不是期望环境清单。后续实现必须先使用这里已经确认的能力;需要升级或安装新工具时,应说明原因并在完成后更新本文件。
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
| Node.js / npm | `22.22.1` / `11.12.1` |
|
| Node.js / npm | `22.22.1` / `11.12.1` |
|
||||||
| Google Chrome | `150.0.7871.188`,`C:\Program Files\Google\Chrome\Application\chrome.exe` |
|
| 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)。
|
Windows 路径可能较长;如果后续依赖缓存或生成代码触发路径长度错误,应优先缩短包/生成目录或评估仓库级长路径配置,并把实际决定写入[决策记录](decisions.md)。
|
||||||
|
|
||||||
@@ -111,23 +111,21 @@ Android Studio 不是当前环境的可用前提。项目必须先支持 PowerSh
|
|||||||
.\gradlew.bat <task>
|
.\gradlew.bat <task>
|
||||||
```
|
```
|
||||||
|
|
||||||
不要要求用户安装全局 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 真机
|
## 7. 已连接 Android 真机
|
||||||
|
|
||||||
检测时存在 1 台已授权的物理设备。为避免持久化设备标识,本文件不记录序列号。
|
2026-08-07 本轮曾检测到 1 台已授权的物理设备。为避免持久化设备标识,本文件不记录序列号。
|
||||||
|
|
||||||
| 项目 | 实测值 |
|
| 项目 | 实测值 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 厂商/型号 | OnePlus `PKG110` |
|
| 厂商/型号 | Samsung `SM-G9700` |
|
||||||
| Android | 16 |
|
| Android | 12 |
|
||||||
| API Level | 36 |
|
| API Level | 31 |
|
||||||
| ABI | `arm64-v8a` |
|
| 物理分辨率 | `1080 × 2280` |
|
||||||
| 物理分辨率 | `1264 × 2780` |
|
| 物理密度 | `480 dpi` |
|
||||||
| 当前覆盖分辨率 | `1080 × 2376` |
|
|
||||||
| 物理/覆盖密度 | `560 / 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. 网络与代理状态
|
## 8. 网络与代理状态
|
||||||
|
|
||||||
@@ -146,21 +144,20 @@ Android Studio 不是当前环境的可用前提。项目必须先支持 PowerSh
|
|||||||
2. 使用项目自带 Gradle Wrapper,不依赖全局 `gradle` 或 `kotlinc`。
|
2. 使用项目自带 Gradle Wrapper,不依赖全局 `gradle` 或 `kotlinc`。
|
||||||
3. 首次可构建配置只能依赖已安装的 SDK Platform 34 和 Build Tools 34.0.0;若模板要求更高版本,应先记录差异,再决定调整模板或安装 SDK。
|
3. 首次可构建配置只能依赖已安装的 SDK Platform 34 和 Build Tools 34.0.0;若模板要求更高版本,应先记录差异,再决定调整模板或安装 SDK。
|
||||||
4. 命令行构建是必须能力,不假设 Android Studio 存在。
|
4. 命令行构建是必须能力,不假设 Android Studio 存在。
|
||||||
5. 仪器测试优先使用已连接的 API 36、arm64 真机;不假设 Emulator/AVD 存在。
|
5. 仪器测试优先使用当前可用的 API 31 三星真机;不假设 Emulator/AVD 或固定设备存在。
|
||||||
6. 不自动安装系统级工具、SDK 平台或模拟器;确需新增环境能力时,先说明影响。
|
6. 不自动安装系统级工具、SDK 平台或模拟器;确需新增环境能力时,先说明影响。
|
||||||
7. 发布时的 `targetSdk` 和商店要求是独立发布门禁,不能因为本机只有 Platform 34 就永久锁定;工具链升级后必须更新本文件和相关 ADR。
|
7. 发布时的 `targetSdk` 和商店要求是独立发布门禁,不能因为本机只有 Platform 34 就永久锁定;工具链升级后必须更新本文件和相关 ADR。
|
||||||
8. P-1 原型截图使用现有 Node.js 与 Chrome DevTools 协议生成,不额外安装前端依赖;命令见[移动端交互原型](prototype.md#3-运行方式)。
|
8. P-1 原型截图使用现有 Node.js 与 Chrome DevTools 协议生成,不额外安装前端依赖;命令见[移动端交互原型](prototype.md#3-运行方式)。
|
||||||
|
|
||||||
## 10. 已知缺口
|
## 10. 已知缺口
|
||||||
|
|
||||||
- 可安装的 Android application 壳尚未创建;正式名称、组织所有的 application ID 与最终 `minSdk` 仍待确认。
|
|
||||||
- Android Studio 未安装。
|
- Android Studio 未安装。
|
||||||
- Android Emulator、system image 和 AVD 不可用。
|
- Android Emulator、system image 和 AVD 不可用。
|
||||||
- 本机只确认安装了 Android Platform 34 / Build Tools 34.0.0。
|
- 本机只确认安装了 Android Platform 34 / Build Tools 34.0.0。
|
||||||
- Compose、Hilt、Room、DataStore 与 Navigation 的 Maven 依赖尚未完整离线验证。
|
- `PATH` 中旧便携 ADB 与 SDK ADB 可能争用 server;设备接口消失时需要手机端重新授权或重新连接。
|
||||||
- 代理已配置,但外部仓库和 Android CLI 网络连通性尚未验证。
|
- 代理已配置;本轮 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)处理。环境缺口不是跳过验证的理由;无法运行的门禁必须在交付报告中准确说明。
|
这些缺口分别由[实施计划](implementation-plan.md)的 P0 和[质量门禁](quality-gates.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
|
> 计划原则:先用原型确认高返工成本体验,再锁定确定性领域核心,随后接内容和 UI,最后接网络 AI
|
||||||
|
|
||||||
## 1. 依赖图
|
## 1. 依赖图
|
||||||
@@ -47,11 +47,11 @@ P1 与 P2 可并行,但 P3 不能在领域与内容契约未稳定时复制原
|
|||||||
任务:
|
任务:
|
||||||
|
|
||||||
- [x] 读取并遵守[本地开发环境](environment.md):JDK 17、SDK 34、命令行优先、Gradle Wrapper、真机验证。
|
- [x] 读取并遵守[本地开发环境](environment.md):JDK 17、SDK 34、命令行优先、Gradle Wrapper、真机验证。
|
||||||
- [ ] 基于 Google `android/architecture-templates` 的 `base` 分支初始化 Android 壳;模板定制需要 TBD-002 的正式 application ID,不能用临时发布身份替代。
|
- [x] 参照 Google `android/architecture-templates` 的 `base` 分支建立单模块 Android 壳和分层边界。
|
||||||
- [ ] 确认正式应用名称、package/application ID、minSdk(TBD-001~003)。
|
- [x] 确认正式应用名称“灵机”、namespace/application ID `net.opcapp.flash`、`minSdk=26`(ADR-014)。
|
||||||
- [ ] 配置 Compose、Material 3、Hilt、Room、DataStore 和 Navigation;Version Catalog 已先用于 JVM harness。
|
- [x] 配置 Compose、Material 3、Hilt、Room、DataStore 和 Navigation,并由 Version Catalog 固定兼容版本。
|
||||||
- [ ] 配置 Gradle Wrapper、格式化、lint、单元测试和 CI:Wrapper、无依赖格式门禁、JVM 单测与 CI 已完成;Android lint 要等 application 插件启用。
|
- [x] 配置 Gradle Wrapper、格式、Android lint、单元测试、debug APK、依赖许可证门禁和 CI 同入口验证。
|
||||||
- [ ] 建立 [系统架构](architecture.md)中的包结构和空 feature 边界:`domain/casting` 已落地,其余随 Android 壳建立。
|
- [x] 建立 [系统架构](architecture.md)中的 `app`、`core/designsystem`、`domain`、`data`、`feature/home` 与 `feature/result` 边界;未实施 feature 不创建空包。
|
||||||
- [x] 将根 `AGENTS.md` 设计为短地图,指向本目录和验证命令。
|
- [x] 将根 `AGENTS.md` 设计为短地图,指向本目录和验证命令。
|
||||||
- [x] 增加 `verifyLocal` 聚合任务、文档链接、领域边界、内容契约与基础 secret scan。
|
- [x] 增加 `verifyLocal` 聚合任务、文档链接、领域边界、内容契约与基础 secret scan。
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ P1 与 P2 可并行,但 P3 不能在领域与内容契约未稳定时复制原
|
|||||||
|
|
||||||
- Windows 上一条命令可执行格式、lint、单测和 debug 构建。
|
- Windows 上一条命令可执行格式、lint、单测和 debug 构建。
|
||||||
- CI 使用同一组命令。
|
- CI 使用同一组命令。
|
||||||
- 空应用可在模拟器启动,导航到占位欢迎页。
|
- 开发验证应用可在授权真机启动,并从首页导航到已知领域夹具结果页。
|
||||||
- 没有生产服务密钥或真实内容。
|
- 没有生产服务密钥或真实内容。
|
||||||
|
|
||||||
## 4. P1:领域核心
|
## 4. P1:领域核心
|
||||||
@@ -82,7 +82,7 @@ P1 与 P2 可并行,但 P3 不能在领域与内容契约未稳定时复制原
|
|||||||
- 测试无随机、无网络、无系统时间依赖。
|
- 测试无随机、无网络、无系统时间依赖。
|
||||||
- `CastEngine` API 经评审后冻结为 `coin-v1`。
|
- `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:内容数据管线
|
## 5. P2:内容数据管线
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
在初始化 Android 工程前,先用可点击原型确认最容易返工的产品决策:信息层级、六次录入方式、结果阅读顺序、AI 同意门和东方文化表达。原型确认后,P3 的 Compose 页面应复用这里的设计令牌和状态契约,而不是重新发明流程。
|
在初始化 Android 工程前,先用可点击原型确认最容易返工的产品决策:信息层级、六次录入方式、结果阅读顺序、AI 同意门和东方文化表达。原型确认后,P3 的 Compose 页面应复用这里的设计令牌和状态契约,而不是重新发明流程。
|
||||||
|
|
||||||
本轮使用“一问”作为视觉工作名。它不构成正式命名决定;正式名称、应用图标和商店素材仍是 [TBD-001](decisions.md#未决问题)。
|
v0.3 截图仍保留当时的视觉工作名“一问”,不得当作当前产品文案。正式产品名已由 [ADR-014](decisions.md#adr-014正式-android-身份与最低版本) 确认为“灵机”;应用图标与商店素材仍见 [TBD-001](decisions.md#未决问题),P3 固化正式页面前须刷新原型品牌文案。
|
||||||
|
|
||||||
## 2. 交付范围
|
## 2. 交付范围
|
||||||
|
|
||||||
|
|||||||
+6
-11
@@ -1,6 +1,6 @@
|
|||||||
# 质量门禁与验证策略
|
# 质量门禁与验证策略
|
||||||
|
|
||||||
> 状态:JVM/harness 门禁已启用;Android lint、构建、UI 与设备门禁等待 Android application 壳
|
> 状态:Android 本地门禁已启用;设备门禁在授权真机存在时单独执行
|
||||||
> 原则:完成必须有可重复证据,不能以“代码看起来正确”代替验证
|
> 原则:完成必须有可重复证据,不能以“代码看起来正确”代替验证
|
||||||
|
|
||||||
## 1. 反馈循环
|
## 1. 反馈循环
|
||||||
@@ -21,26 +21,21 @@
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\gradlew.bat spotlessCheck --offline
|
.\gradlew.bat spotlessCheck --offline
|
||||||
.\gradlew.bat :app:test --offline
|
.\gradlew.bat :app:testDebugUnitTest --offline
|
||||||
.\gradlew.bat verifyContentContract --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
|
```powershell
|
||||||
.\gradlew.bat spotlessCheck
|
.\gradlew.bat spotlessCheck
|
||||||
.\gradlew.bat lintDebug
|
|
||||||
.\gradlew.bat testDebugUnitTest
|
|
||||||
.\gradlew.bat assembleDebug
|
|
||||||
.\gradlew.bat connectedDebugAndroidTest
|
.\gradlew.bat connectedDebugAndroidTest
|
||||||
```
|
```
|
||||||
|
|
||||||
若采用不同格式化插件,命令可以调整,但必须在本文件和 CI 同步更新。`connectedDebugAndroidTest` 需要模拟器或设备,应与纯 JVM 快速门禁分开。
|
若采用不同格式化插件,命令可以调整,但必须在本文件和 CI 同步更新。`connectedDebugAndroidTest` 需要模拟器或设备,继续与环境独立的 `verifyLocal` 分开;`verifyLocal` 通过证明 APK 已构建,但不等于真机测试已经通过。
|
||||||
|
|
||||||
到那时必须把 Android lint 和 debug build 加入现有 `verifyLocal`,使代理仍不必猜测验证组合。在完成这一步之前,`verifyLocal` 通过只证明 JVM 与仓库门禁,不代表 APK 已构建或真机测试已通过。
|
|
||||||
|
|
||||||
## 3. 测试层次
|
## 3. 测试层次
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
android.useAndroidX=true
|
||||||
|
kotlin.code.style=official
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
@@ -1,9 +1,44 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
agp = "8.2.0"
|
||||||
kotlin = "1.9.20"
|
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"
|
junit = "4.13.2"
|
||||||
|
androidxTestExt = "1.1.5"
|
||||||
|
androidxTestRunner = "1.5.2"
|
||||||
|
|
||||||
[libraries]
|
[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" }
|
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]
|
[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" }
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ const kotlinCatalogPath = path.join(
|
|||||||
"src",
|
"src",
|
||||||
"main",
|
"main",
|
||||||
"java",
|
"java",
|
||||||
"brainwave",
|
"net",
|
||||||
|
"opcapp",
|
||||||
|
"flash",
|
||||||
"domain",
|
"domain",
|
||||||
"casting",
|
"casting",
|
||||||
"HexagramCatalog.kt",
|
"HexagramCatalog.kt",
|
||||||
|
|||||||
@@ -17,19 +17,42 @@ if (manifest.schemaVersion !== 1) failures.push("manifest schemaVersion must be
|
|||||||
if (!Array.isArray(manifest.components) || manifest.components.length === 0) {
|
if (!Array.isArray(manifest.components) || manifest.components.length === 0) {
|
||||||
failures.push("manifest components must not be empty");
|
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 allowedLicenses = new Set(["Apache-2.0", "BSD-3-Clause", "EPL-1.0"]);
|
||||||
const ids = new Set();
|
const ids = new Set();
|
||||||
const coordinates = new Set();
|
const coordinates = new Set();
|
||||||
for (const [index, component] of (manifest.components ?? []).entries()) {
|
const validateCommonFields = (entry, prefix) => {
|
||||||
const prefix = `components[${index}]`;
|
for (const field of ["id", "scope", "license", "source"]) {
|
||||||
for (const field of ["id", "version", "scope", "license", "source"]) {
|
if (typeof entry[field] !== "string" || entry[field].trim() === "") {
|
||||||
if (typeof component[field] !== "string" || component[field].trim() === "") {
|
|
||||||
failures.push(`${prefix}.${field} must be non-blank text`);
|
failures.push(`${prefix}.${field} must be non-blank text`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ids.has(component.id)) failures.push(`${prefix}.id must be unique`);
|
if (ids.has(entry.id)) failures.push(`${prefix}.id must be unique`);
|
||||||
ids.add(component.id);
|
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 (component.coordinates !== null) {
|
||||||
if (typeof component.coordinates !== "string" || component.coordinates.split(":").length !== 3) {
|
if (typeof component.coordinates !== "string" || component.coordinates.split(":").length !== 3) {
|
||||||
failures.push(`${prefix}.coordinates must be group:name:version or null`);
|
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);
|
coordinates.add(component.coordinates);
|
||||||
}
|
}
|
||||||
if (!allowedLicenses.has(component.license)) failures.push(`${prefix}.license is not reviewed`);
|
}
|
||||||
try {
|
|
||||||
const url = new URL(component.source);
|
const groupPrefixes = new Set();
|
||||||
if (url.protocol !== "https:") throw new Error("not HTTPS");
|
for (const [index, policy] of (manifest.groupPolicies ?? []).entries()) {
|
||||||
} catch {
|
const prefix = `groupPolicies[${index}]`;
|
||||||
failures.push(`${prefix}.source must be an absolute HTTPS URL`);
|
validateCommonFields(policy, prefix);
|
||||||
}
|
if (typeof policy.groupPrefix !== "string" || !/^[a-z][a-z0-9.]+$/u.test(policy.groupPrefix)) {
|
||||||
if (!documentation.includes(component.id) ||
|
failures.push(`${prefix}.groupPrefix must be a Maven group prefix`);
|
||||||
!documentation.includes(component.version) ||
|
} else if (groupPrefixes.has(policy.groupPrefix)) {
|
||||||
!documentation.includes(component.license)) {
|
failures.push(`${prefix}.groupPrefix must be unique`);
|
||||||
failures.push(`${prefix} is not represented in docs/dependency-licenses.md`);
|
|
||||||
}
|
}
|
||||||
|
groupPrefixes.add(policy.groupPrefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
const version = (name) => new RegExp(`^${name}\\s*=\\s*"([^"]+)"`, "mu").exec(catalog)?.[1];
|
const version = (name) => new RegExp(`^${name}\\s*=\\s*"([^"]+)"`, "mu").exec(catalog)?.[1];
|
||||||
const expectedVersions = new Map([
|
const expectedVersions = new Map([
|
||||||
["gradle-wrapper", /gradle-([\d.]+)-bin\.zip/u.exec(wrapper)?.[1]],
|
["gradle-wrapper", /gradle-([\d.]+)-bin\.zip/u.exec(wrapper)?.[1]],
|
||||||
|
["android-gradle-plugin", version("agp")],
|
||||||
["kotlin-gradle-plugin", version("kotlin")],
|
["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")],
|
["kotlin-stdlib", version("kotlin")],
|
||||||
["junit4", version("junit")],
|
["junit4", version("junit")],
|
||||||
]);
|
]);
|
||||||
@@ -71,5 +101,8 @@ if (failures.length > 0) {
|
|||||||
for (const failure of failures) console.error(`- ${failure}`);
|
for (const failure of failures) console.error(`- ${failure}`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
} else {
|
} 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).`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,17 @@ import {
|
|||||||
walkFiles,
|
walkFiles,
|
||||||
} from "./repository-files.mjs";
|
} 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 = [
|
const forbiddenImports = [
|
||||||
/^android\./u,
|
/^android\./u,
|
||||||
/^androidx\./u,
|
/^androidx\./u,
|
||||||
|
|||||||
+1
-1
@@ -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"
|
rootProject.name = "brainwave"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|||||||
Reference in New Issue
Block a user