This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import groovy.json.JsonSlurper
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
}
|
}
|
||||||
@@ -25,3 +27,28 @@ tasks.test {
|
|||||||
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
|
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.register("verifyDependencyLicenses") {
|
||||||
|
group = "verification"
|
||||||
|
description = "Ensures every resolved JVM runtime/test artifact has a reviewed license entry."
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val manifest = JsonSlurper().parse(rootProject.file("config/dependency-licenses.json")) as Map<String, Any>
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val components = manifest.getValue("components") as List<Map<String, Any>>
|
||||||
|
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}"
|
||||||
|
}
|
||||||
|
.toSet()
|
||||||
|
val missing = resolvedCoordinates - reviewedCoordinates
|
||||||
|
check(missing.isEmpty()) {
|
||||||
|
"Dependencies missing reviewed license entries: ${missing.sorted().joinToString()}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,17 +51,25 @@ val spotlessCheck = registerNodeVerificationTask(
|
|||||||
script = "scripts/verify-format.mjs",
|
script = "scripts/verify-format.mjs",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val verifyDependencyLicenseManifest = registerNodeVerificationTask(
|
||||||
|
name = "verifyDependencyLicenseManifest",
|
||||||
|
description = "Checks the reviewed dependency-license manifest against pinned build versions.",
|
||||||
|
script = "scripts/verify-dependency-licenses.mjs",
|
||||||
|
)
|
||||||
|
|
||||||
tasks.register("verifyLocal") {
|
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:test",
|
||||||
|
":app:verifyDependencyLicenses",
|
||||||
verifyDocs,
|
verifyDocs,
|
||||||
scanSecrets,
|
scanSecrets,
|
||||||
verifyDomainBoundaries,
|
verifyDomainBoundaries,
|
||||||
verifyPrototype,
|
verifyPrototype,
|
||||||
verifyContentContract,
|
verifyContentContract,
|
||||||
spotlessCheck,
|
spotlessCheck,
|
||||||
|
verifyDependencyLicenseManifest,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"reviewedAt": "2026-08-04",
|
||||||
|
"scopeNote": "Current JVM runtime/test graph and direct build entry points; Android dependencies are not configured yet.",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"id": "gradle-wrapper",
|
||||||
|
"coordinates": null,
|
||||||
|
"version": "8.2",
|
||||||
|
"scope": "build",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"source": "https://github.com/gradle/gradle/blob/v8.2.0/LICENSE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kotlin-gradle-plugin",
|
||||||
|
"coordinates": null,
|
||||||
|
"version": "1.9.20",
|
||||||
|
"scope": "build",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"source": "https://github.com/JetBrains/kotlin/blob/v1.9.20/license/LICENSE.txt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kotlin-stdlib",
|
||||||
|
"coordinates": "org.jetbrains.kotlin:kotlin-stdlib:1.9.20",
|
||||||
|
"version": "1.9.20",
|
||||||
|
"scope": "runtime",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"source": "https://github.com/JetBrains/kotlin/blob/v1.9.20/license/LICENSE.txt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "jetbrains-annotations",
|
||||||
|
"coordinates": "org.jetbrains:annotations:13.0",
|
||||||
|
"version": "13.0",
|
||||||
|
"scope": "runtime-transitive",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"source": "https://repo1.maven.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0.pom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "junit4",
|
||||||
|
"coordinates": "junit:junit:4.13.2",
|
||||||
|
"version": "4.13.2",
|
||||||
|
"scope": "test",
|
||||||
|
"license": "EPL-1.0",
|
||||||
|
"source": "https://github.com/junit-team/junit4/blob/r4.13.2/LICENSE-junit.txt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hamcrest-core",
|
||||||
|
"coordinates": "org.hamcrest:hamcrest-core:1.3",
|
||||||
|
"version": "1.3",
|
||||||
|
"scope": "test-transitive",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"source": "https://github.com/hamcrest/JavaHamcrest/blob/hamcrest-java-1.3/LICENSE.txt"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ Brainwave 是一个以《易经》三枚铜币法为文化背景的 Android 个
|
|||||||
| [数据与内容](data-content.md) | 修改卦库、历史记录或内容来源时 | 数据契约、授权、隐私和迁移规则 |
|
| [数据与内容](data-content.md) | 修改卦库、历史记录或内容来源时 | 数据契约、授权、隐私和迁移规则 |
|
||||||
| [AI 解释与安全](ai-safety.md) | 修改提示词、模型调用或解释结果时 | AI 调用门、输入输出契约和安全边界 |
|
| [AI 解释与安全](ai-safety.md) | 修改提示词、模型调用或解释结果时 | AI 调用门、输入输出契约和安全边界 |
|
||||||
| [质量门禁](quality-gates.md) | 实现、评审、发布前 | 自动化验证、需求追踪和完成定义 |
|
| [质量门禁](quality-gates.md) | 实现、评审、发布前 | 自动化验证、需求追踪和完成定义 |
|
||||||
|
| [依赖与许可证](dependency-licenses.md) | 新增/升级依赖或准备分发时 | 当前解析依赖、SPDX 与权威许可来源 |
|
||||||
| [实施计划](implementation-plan.md) | 领取任务或判断下一步时 | 阶段、依赖、交付物和退出条件 |
|
| [实施计划](implementation-plan.md) | 领取任务或判断下一步时 | 阶段、依赖、交付物和退出条件 |
|
||||||
| [决策记录](decisions.md) | 遇到架构分歧或未决问题时 | 已接受决定、默认假设和 TBD |
|
| [决策记录](decisions.md) | 遇到架构分歧或未决问题时 | 已接受决定、默认假设和 TBD |
|
||||||
| [代理工作手册](agent-playbook.md) | 任何编码代理开始工作前 | 检索、修改、验证和交付流程 |
|
| [代理工作手册](agent-playbook.md) | 任何编码代理开始工作前 | 检索、修改、验证和交付流程 |
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# 依赖与许可证清单
|
||||||
|
|
||||||
|
> 状态:当前 JVM harness 已核验;Android application 依赖尚未配置
|
||||||
|
> 适用范围:实际解析的 JVM runtime/test graph,以及直接使用的构建入口
|
||||||
|
|
||||||
|
本清单不是未来 Android APK 的最终 third-party notices。每次新增或升级依赖时,必须同步 `config/dependency-licenses.json` 并运行 `.\gradlew.bat verifyLocal --offline`;Android 依赖启用后,还要生成 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) |
|
||||||
|
|
||||||
|
当前没有第三方 Android runtime 库、字体、纹理、插画、音效或可发布《易经》内容进入工程;这句话只描述本提交时的依赖图,不构成未来授权。
|
||||||
@@ -177,7 +177,7 @@ P1 与 P2 可并行,但 P3 不能在领域与内容契约未稳定时复制原
|
|||||||
|
|
||||||
贯穿所有阶段:
|
贯穿所有阶段:
|
||||||
|
|
||||||
- 建立 CI、架构检查、内容校验、secret scan 和依赖许可证报告。
|
- [x] 建立 CI、架构检查、内容校验、secret scan 和当前 JVM 依赖许可证报告;Android release 依赖启用后扩展报告。
|
||||||
- 建立脱敏崩溃监控和最小匿名指标。
|
- 建立脱敏崩溃监控和最小匿名指标。
|
||||||
- 增加可复现 screenshot/accessibility 测试环境。
|
- 增加可复现 screenshot/accessibility 测试环境。
|
||||||
- 准备隐私政策、内容来源、免责声明和应用商店素材。
|
- 准备隐私政策、内容来源、免责声明和应用商店素材。
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
.\gradlew.bat verifyLocal --offline
|
.\gradlew.bat verifyLocal --offline
|
||||||
```
|
```
|
||||||
|
|
||||||
`verifyLocal` 当前聚合无依赖格式检查、10 个领域测试、3 个内容 repository 测试、domain 依赖边界、内容契约与负向夹具、文档链接、高置信 secret scan 和原型 JavaScript 语法检查。CI 执行同一个聚合任务。
|
`verifyLocal` 当前聚合无依赖格式检查、10 个领域测试、3 个内容 repository 测试、已解析 JVM 依赖许可证、domain 依赖边界、内容契约与负向夹具、文档链接、高置信 secret scan 和原型 JavaScript 语法检查。CI 执行同一个聚合任务。
|
||||||
|
|
||||||
Android application 插件配置后,Windows 环境还必须提供:
|
Android application 插件配置后,Windows 环境还必须提供:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { repositoryRoot } from "./repository-files.mjs";
|
||||||
|
|
||||||
|
const manifestPath = path.join(repositoryRoot, "config", "dependency-licenses.json");
|
||||||
|
const catalogPath = path.join(repositoryRoot, "gradle", "libs.versions.toml");
|
||||||
|
const wrapperPath = path.join(repositoryRoot, "gradle", "wrapper", "gradle-wrapper.properties");
|
||||||
|
const documentationPath = path.join(repositoryRoot, "docs", "dependency-licenses.md");
|
||||||
|
|
||||||
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
||||||
|
const catalog = await readFile(catalogPath, "utf8");
|
||||||
|
const wrapper = await readFile(wrapperPath, "utf8");
|
||||||
|
const documentation = await readFile(documentationPath, "utf8");
|
||||||
|
const failures = [];
|
||||||
|
|
||||||
|
if (manifest.schemaVersion !== 1) failures.push("manifest schemaVersion must be 1");
|
||||||
|
if (!Array.isArray(manifest.components) || manifest.components.length === 0) {
|
||||||
|
failures.push("manifest components 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() === "") {
|
||||||
|
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 (component.coordinates !== null) {
|
||||||
|
if (typeof component.coordinates !== "string" || component.coordinates.split(":").length !== 3) {
|
||||||
|
failures.push(`${prefix}.coordinates must be group:name:version or null`);
|
||||||
|
} else if (coordinates.has(component.coordinates)) {
|
||||||
|
failures.push(`${prefix}.coordinates must be unique`);
|
||||||
|
}
|
||||||
|
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 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]],
|
||||||
|
["kotlin-gradle-plugin", version("kotlin")],
|
||||||
|
["kotlin-stdlib", version("kotlin")],
|
||||||
|
["junit4", version("junit")],
|
||||||
|
]);
|
||||||
|
for (const [id, expectedVersion] of expectedVersions) {
|
||||||
|
const component = manifest.components.find((candidate) => candidate.id === id);
|
||||||
|
if (!component || component.version !== expectedVersion) {
|
||||||
|
failures.push(`${id} license entry must match pinned version ${expectedVersion ?? "<missing>"}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures.length > 0) {
|
||||||
|
console.error("Dependency-license verification failed:");
|
||||||
|
for (const failure of failures) console.error(`- ${failure}`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
} else {
|
||||||
|
console.log(`Dependency-license manifest passed (${manifest.components.length} reviewed components).`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user