app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit f2db0a06f4281d1e1dc54c4d1f04c01cb5d63c3b
parent 3f30b4a4ff80c3e569f62713d8ea8dd3bd9de9e4
Author: triesap <tyson@radroots.org>
Date:   Mon, 10 Aug 2026 20:41:30 +0000

build: add the root convention plugin

- replace imperative root task registration with one convention plugin
- preserve verification task names, inputs, dependencies, and output routing
- carry structural audits behind lazy typed task adapters
- prove root-only application and configuration-cache reuse with TestKit

Diffstat:
Mbuild-logic/contracts/src/main/kotlin/org/harvestcircle/buildlogic/contracts/FfiCompatibilityBaseline.kt | 4++++
Mbuild-logic/contracts/src/main/kotlin/org/harvestcircle/buildlogic/contracts/ProductCoordinates.kt | 5+++++
Mbuild-logic/contracts/src/main/kotlin/org/harvestcircle/buildlogic/contracts/SourceProvenance.kt | 4++++
Mbuild-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt | 32+++++++++++++++++++++++++++++---
Mbuild-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleConventionPlugins.kt | 4----
Abuild-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleRootPlugin.kt | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abuild-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/FoundationBoundaryAudit.kt | 308+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abuild-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/GitSourcePolicy.kt | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abuild-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/ProductCoordinateConsumers.kt | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abuild-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/ProductCoordinatesTask.kt | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abuild-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/VerificationLanes.kt | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbuild.gradle.kts | 91+------------------------------------------------------------------------------
12 files changed, 919 insertions(+), 97 deletions(-)

diff --git a/build-logic/contracts/src/main/kotlin/org/harvestcircle/buildlogic/contracts/FfiCompatibilityBaseline.kt b/build-logic/contracts/src/main/kotlin/org/harvestcircle/buildlogic/contracts/FfiCompatibilityBaseline.kt @@ -1,5 +1,7 @@ package org.harvestcircle.buildlogic.contracts +import java.io.File + public class FfiCompatibilityBaseline private constructor( private val values: Map<String, String>, ) { @@ -23,6 +25,8 @@ public class FfiCompatibilityBaseline private constructor( "source.foundation_baseline", ) + public fun load(file: File): FfiCompatibilityBaseline = parse(file.readText()) + public fun parse(source: String): FfiCompatibilityBaseline { require(!source.startsWith('\uFEFF')) { "FFI baseline must not contain a UTF-8 BOM" } val values = linkedMapOf<String, String>() diff --git a/build-logic/contracts/src/main/kotlin/org/harvestcircle/buildlogic/contracts/ProductCoordinates.kt b/build-logic/contracts/src/main/kotlin/org/harvestcircle/buildlogic/contracts/ProductCoordinates.kt @@ -1,5 +1,7 @@ package org.harvestcircle.buildlogic.contracts +import java.io.File + public class ProductCoordinates private constructor( private val values: Map<String, String>, public val canonical: String, @@ -10,6 +12,7 @@ public class ProductCoordinates private constructor( public companion object { public const val SCHEMA: String = "harvestcircle.product.v1" + public const val schema: String = SCHEMA public val requiredKeys: List<String> = listOf( @@ -32,6 +35,8 @@ public class ProductCoordinates private constructor( "copyright.notice", ) + public fun load(file: File): ProductCoordinates = parse(file.readText()) + public fun parse(source: String): ProductCoordinates { require(!source.startsWith('\uFEFF')) { "Product coordinates must not contain a UTF-8 BOM" } val parsed = linkedMapOf<String, String>() diff --git a/build-logic/contracts/src/main/kotlin/org/harvestcircle/buildlogic/contracts/SourceProvenance.kt b/build-logic/contracts/src/main/kotlin/org/harvestcircle/buildlogic/contracts/SourceProvenance.kt @@ -1,5 +1,7 @@ package org.harvestcircle.buildlogic.contracts +import java.io.File + public class SourceProvenance private constructor( private val root: Map<String, String>, private val imports: List<Map<String, String>>, @@ -29,6 +31,8 @@ public class SourceProvenance private constructor( private val importKeys = linkedSetOf("component", "commit") private val assignment = Regex("^([A-Za-z0-9_]+)\\s*=\\s*\"([^\"]*)\"\\s*(?:#.*)?$") + public fun load(file: File): SourceProvenance = parse(file.readText()) + public fun parse(source: String): SourceProvenance { require(!source.startsWith('\uFEFF')) { "Source provenance must not contain a UTF-8 BOM" } val root = linkedMapOf<String, String>() diff --git a/build-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt b/build-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt @@ -1,6 +1,7 @@ package org.harvestcircle.buildlogic.plugins import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.UnexpectedBuildFailure import kotlin.io.path.createTempDirectory import kotlin.io.path.createDirectories import kotlin.io.path.writeText @@ -25,14 +26,39 @@ class ConventionPluginSmokeTest { fixture.resolve("settings.gradle.kts").writeText("rootProject.name = \"fixture\"\n") fixture.resolve("build.gradle.kts").writeText("plugins { id(\"$pluginId\") }\n") - val result = + val runner = GradleRunner.create() .withProjectDir(fixture.toFile()) .withPluginClasspath() - .withArguments("tasks", "--stacktrace") - .build() + .withArguments("tasks", "--configuration-cache", "--configuration-cache-problems=fail", "--stacktrace") + val result = runner.build() assertTrue(result.output.contains("BUILD SUCCESSFUL"), pluginId) + if (pluginId == "org.harvestcircle.build.root") { + assertTrue(result.output.contains("verifyProductCoordinates"), result.output) + assertTrue(runner.build().output.contains("Reusing configuration cache")) + } } } + + @Test + fun rootPluginRejectsApplicationToASubproject() { + val fixture = createTempDirectory("harvestcircle-root-plugin-") + fixture.resolve("settings.gradle.kts").writeText("rootProject.name = \"fixture\"\ninclude(\":child\")\n") + fixture.resolve("build.gradle.kts").writeText("// root intentionally has no convention plugin\n") + val child = fixture.resolve("child").createDirectories() + child.resolve("build.gradle.kts").writeText("plugins { id(\"org.harvestcircle.build.root\") }\n") + + val failure = + runCatching { + GradleRunner.create() + .withProjectDir(fixture.toFile()) + .withPluginClasspath() + .withArguments("tasks", "--stacktrace") + .build() + }.exceptionOrNull() + + assertTrue(failure is UnexpectedBuildFailure) + assertTrue(failure.message.orEmpty().contains("may only be applied to the root project")) + } } diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleConventionPlugins.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleConventionPlugins.kt @@ -3,10 +3,6 @@ package org.harvestcircle.buildlogic.plugins import org.gradle.api.Plugin import org.gradle.api.Project -public class HarvestCircleRootPlugin : Plugin<Project> { - override fun apply(target: Project) = Unit -} - public class HarvestCircleKmpSharedPlugin : Plugin<Project> { override fun apply(target: Project) = Unit } diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleRootPlugin.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleRootPlugin.kt @@ -0,0 +1,98 @@ +package org.harvestcircle.buildlogic.plugins + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.harvestcircle.buildlogic.plugins.tasks.VerifyFoundationBoundaries +import org.harvestcircle.buildlogic.plugins.tasks.VerifyGitSourcePolicy +import org.harvestcircle.buildlogic.plugins.tasks.VerifyProductCoordinateConsumers +import org.harvestcircle.buildlogic.plugins.tasks.VerifyProductCoordinates +import org.harvestcircle.buildlogic.plugins.tasks.VerifyVerificationLanes + +public class HarvestCircleRootPlugin : Plugin<Project> { + override fun apply(target: Project) { + require(target == target.rootProject) { "The HarvestCircle root plugin may only be applied to the root project" } + + val productCoordinatesFile = target.layout.projectDirectory.file("config/product/harvestcircle-v1.properties") + val ffiCompatibilityBaselineFile = + target.layout.projectDirectory.file("core/compatibility/harvestcircle-ffi-v4.properties") + val verificationLanesFile = target.layout.projectDirectory.file("config/verification/lanes-v2.properties") + val legacyProduct = "stu" + "dio" + + val verifyProductCoordinates = + target.tasks.register("verifyProductCoordinates", VerifyProductCoordinates::class.java) { task -> + task.group = "verification" + task.description = "Validates the canonical HarvestCircle product-coordinate authority." + task.manifestFile.set(productCoordinatesFile) + task.uniFfiConfigFile.set( + target.layout.projectDirectory.file("core/crates/harvestcircle_ffi/uniffi.toml"), + ) + task.ffiBaselineFile.set(ffiCompatibilityBaselineFile) + task.sourceProvenanceFile.set( + target.layout.projectDirectory.file("core/provenance/$legacyProduct-import-v1.toml"), + ) + task.nativeCompatibilityFile.set( + target.layout.projectDirectory.file( + "app/desktop/src/main/kotlin/org/harvestcircle/application/NativeCompatibility.kt", + ), + ) + } + + target.tasks.register("verifyCompatibilityBaseline") { task -> + task.group = "verification" + task.description = "Validates the generated-code and native compatibility baseline." + task.dependsOn(verifyProductCoordinates) + } + target.tasks.register("verifySourceProvenance") { task -> + task.group = "verification" + task.description = "Validates canonical source provenance and its governed digest." + task.dependsOn(verifyProductCoordinates) + } + target.tasks.register("verifyProductCoordinateConsumers", VerifyProductCoordinateConsumers::class.java) { task -> + task.group = "verification" + task.description = "Validates that build and runtime identities consume the product manifest." + task.manifestFile.set(productCoordinatesFile) + task.desktopBuildFile.set(target.layout.projectDirectory.file("app/desktop/build.gradle.kts")) + task.uniFfiConfigFile.set(target.layout.projectDirectory.file("core/crates/harvestcircle_ffi/uniffi.toml")) + task.productBuildFile.set(target.layout.projectDirectory.file("core/crates/harvestcircle_product/build.rs")) + task.ffiConsumerFile.set(target.layout.projectDirectory.file("core/crates/harvestcircle_ffi/src/commands.rs")) + task.keyringConsumerFile.set(target.layout.projectDirectory.file("core/crates/harvestcircle_storage/src/os_keyring.rs")) + } + target.tasks.register("verifyVerificationLanes", VerifyVerificationLanes::class.java) { task -> + task.group = "verification" + task.description = "Validates forge-agnostic verification lanes and least-privilege policy." + task.policyFile.set(verificationLanesFile) + task.productManifestFile.set(productCoordinatesFile) + task.repositoryRoot.set(target.layout.projectDirectory) + } + val verifyGitSourcePolicy = + target.tasks.register("verifyGitSourcePolicy", VerifyGitSourcePolicy::class.java) { task -> + task.group = "verification" + task.description = "Validates immutable and allowlisted Cargo Git dependency sources." + task.denyConfigFile.set(target.layout.projectDirectory.file("core/deny.toml")) + task.cargoLockFile.set(target.layout.projectDirectory.file("core/Cargo.lock")) + task.cargoManifestFiles.from( + target.fileTree("core") { tree -> + tree.include("Cargo.toml", "crates/*/Cargo.toml") + }, + ) + } + target.tasks.register("verifyFoundationBoundaries", VerifyFoundationBoundaries::class.java) { task -> + task.group = "verification" + task.description = "Audits tracked sources against the HarvestCircle foundation boundaries." + task.repositoryRoot.set(target.layout.projectDirectory) + task.gitAware.set(true) + task.dependsOn(verifyGitSourcePolicy) + } + target.tasks.register("verifyFoundationArchive", VerifyFoundationBoundaries::class.java) { task -> + task.group = "verification" + task.description = "Audits a source-archive inventory without Git metadata." + task.repositoryRoot.set(target.layout.projectDirectory) + task.gitAware.set(false) + task.dependsOn(verifyGitSourcePolicy) + } + + target.providers.environmentVariable("EXT_BUILD_GRADLE_BUILD_DIR").orNull?.let { outputRoot -> + target.layout.buildDirectory.set(target.file(outputRoot).resolve("root")) + } + } +} diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/FoundationBoundaryAudit.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/FoundationBoundaryAudit.kt @@ -0,0 +1,308 @@ +package org.harvestcircle.buildlogic.plugins.tasks + +import org.harvestcircle.buildlogic.contracts.ProductCoordinates +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.TaskAction +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.extension +import kotlin.io.path.name +import kotlin.io.path.readText + +abstract class VerifyFoundationBoundaries : DefaultTask() { + @get:Internal + abstract val repositoryRoot: DirectoryProperty + + @get:Input + abstract val gitAware: Property<Boolean> + + @TaskAction + fun verify() { + val root = repositoryRoot.get().asFile.toPath() + val useGitInventory = gitAware.get() && Files.exists(root.resolve(".git")) + val paths = if (useGitInventory) trackedPaths(root) else archivePaths(root) + FoundationBoundaryAudit(root, paths).verify() + if (!gitAware.get()) { + verifyNegativeFixtures(root, paths) + } + } + + private fun trackedPaths(root: Path): List<String> { + val process = + ProcessBuilder("git", "-C", root.toString(), "ls-files", "-z") + .redirectErrorStream(true) + .start() + val output = process.inputStream.readAllBytes() + require(process.waitFor() == 0) { + "Unable to enumerate tracked HarvestCircle sources: ${output.toString(StandardCharsets.UTF_8)}" + } + return output + .toString(StandardCharsets.UTF_8) + .split('\u0000') + .filter(String::isNotEmpty) + .filter { Files.exists(root.resolve(it)) } + .sorted() + } + + private fun archivePaths(root: Path): List<String> = + Files.walk(root).use { paths -> + paths + .filter { path -> + path != root && + shouldInspect(root.relativize(path).toString().replace('\\', '/')) + }.map { root.relativize(it).toString().replace('\\', '/') } + .sorted() + .toList() + } + + private fun shouldInspect(relative: String): Boolean { + val segments = relative.split('/') + return segments.none { it in setOf(".git", ".gradle", ".kotlin", ".idea", "build", "target", "out") } + } + + private fun verifyNegativeFixtures( + root: Path, + paths: List<String>, + ) { + val fixtures = + listOf( + ".github/ISSUE_TEMPLATE/bug.md" to "# Bug report", + "app/shared/src/commonMain/kotlin/org/harvestcircle/application/Leak.kt" to + ("import org.harvestcircle." + "ffi.BuildInfoDto"), + "app/desktop/src/main/kotlin/org/harvestcircle/desktop/Blocking.kt" to + ("fun bad() = run" + "Blocking {}"), + "app/desktop/src/main/kotlin/org/harvestcircle/desktop/Counter.kt" to + ("val bad = Atomic" + "Long(0)"), + "core/target/generated/native.bin" to "generated", + "config/credentials/release.key" to "not-a-real-key", + ) + fixtures.forEach { (path, source) -> + check( + runCatching { + FoundationBoundaryAudit(root, paths + path, mapOf(path to source)).verify() + }.isFailure, + ) { "Foundation audit accepted negative fixture $path" } + } + val symlinkPath = "docs/escape.md" + check( + runCatching { + FoundationBoundaryAudit( + root, + paths + symlinkPath, + overrides = mapOf(symlinkPath to "outside"), + symbolicLinks = setOf(symlinkPath), + ).verify() + }.isFailure, + ) { "Foundation audit accepted symlink fixture $symlinkPath" } + val provenancePath = "core/provenance/" + "stu" + "dio-import-v1.toml" + val altered = root.resolve(provenancePath).readText().replace("09065a610d95e57acdc895a14c07580fa099e7c3", "0".repeat(40)) + check( + runCatching { + FoundationBoundaryAudit(root, paths, mapOf(provenancePath to altered)).verify() + }.isFailure, + ) { "Foundation audit accepted altered source provenance" } + } +} + +private class FoundationBoundaryAudit( + private val root: Path, + paths: List<String>, + private val overrides: Map<String, String> = emptyMap(), + private val symbolicLinks: Set<String> = emptySet(), +) { + private val inventory = paths.distinct().sorted() + private val legacyProduct = "stu" + "dio" + private val provenancePath = "core/provenance/$legacyProduct-import-v1.toml" + private val legacyRepository = "https://github.com/radrootslabs/${legacyProduct}_app" + private val temporaryNamespace = listOf("org", "radroots", "harvestcircle").joinToString(".") + private val textExtensions = + setOf("gradle", "json", "kt", "kts", "lock", "md", "properties", "rs", "sql", "toml", "txt", "xml", "yaml", "yml") + private val textNames = + setOf(".gitattributes", ".gitignore", "AGENTS.md", "LICENSE", "Makefile", "NOTICE", "gradlew", "gradlew.bat") + + fun verify() { + val findings = mutableListOf<String>() + inventory.forEach { relative -> + verifyPath(relative, findings) + if (isText(relative)) { + val source = overrides[relative] ?: readText(relative) + verifyText(relative, source, findings) + } + } + verifyExactContracts(findings) + check(findings.isEmpty()) { findings.sorted().joinToString("\n") } + } + + private fun verifyPath( + relative: String, + findings: MutableList<String>, + ) { + val normalized = relative.lowercase() + if (normalized.startsWith("docs/") || normalized.startsWith("spec/") || + normalized.startsWith(".github/") || normalized.startsWith(".act/")) { + findings += "$relative: forbidden repository root" + } + if (relative in symbolicLinks || Files.isSymbolicLink(root.resolve(relative))) { + findings += "$relative: symbolic links are not allowed in public sources" + } + if (normalized.startsWith("core/target/") || normalized.contains("/build/") || + normalized.contains("/generated/") || + normalized.contains("generated/uniffi") || normalized.endsWith(".dylib") || + normalized.endsWith(".so") || normalized.endsWith(".dll") || normalized.endsWith(".class") + ) { + findings += "$relative: generated build output must not be source controlled" + } + if (normalized.endsWith(".pem") || normalized.endsWith(".key") || normalized.endsWith(".p12") || + normalized.endsWith(".pfx") || normalized.endsWith(".jks") || normalized.endsWith(".keystore") || + normalized.endsWith(".env") || normalized.contains("/credentials/") + ) { + findings += "$relative: credential or secret-shaped source path" + } + if (relative != provenancePath && normalized.contains(legacyProduct)) { + findings += "$relative: legacy product name in source path" + } + val kotlinMarker = "/kotlin/" + if (normalized.startsWith("app/") && normalized.contains(kotlinMarker) && normalized.endsWith(".kt")) { + val packagePath = normalized.substringAfter(kotlinMarker) + if (!packagePath.startsWith("org/harvestcircle/")) { + findings += "$relative: Kotlin source is outside the final namespace" + } + } + } + + private fun verifyText( + relative: String, + source: String, + findings: MutableList<String>, + ) { + if (relative != provenancePath) { + var inspected = if (relative == "core/Cargo.toml") source.replace(legacyRepository, "") else source + if (relative == "NOTICE") { + val legacyDisplayName = legacyProduct.replaceFirstChar { it.uppercase() } + inspected = + inspected + .replace("Radroots $legacyDisplayName application work", "") + .replace("core/provenance/$legacyProduct-import-v1.toml", "") + } + if (inspected.lowercase().contains(legacyProduct)) { + findings += "$relative: legacy product name outside the exact provenance allowlist" + } + } + if (source.contains(temporaryNamespace) || source.contains(temporaryNamespace.replace('.', '/'))) { + findings += "$relative: temporary product namespace" + } + val productionKotlin = + relative.startsWith("app/") && + relative.endsWith(".kt") && + (relative.contains("/src/main/") || relative.contains("/src/commonMain/") || relative.contains("/src/desktopMain/")) + if (productionKotlin && source.contains("run" + "Blocking")) { + findings += "$relative: blocking coroutine bridge in application source" + } + if (productionKotlin && (source.contains("Atomic" + "Long") || source.contains("desktop" + "-operation:"))) { + findings += "$relative: process-local operation counter" + } + if (relative.startsWith("app/shared/src/commonMain/") && + listOf("org.harvestcircle." + "ffi", "com.sun." + "jna", "java.", "javax.").any(source::contains) + ) { + findings += "$relative: platform dependency in shared common source" + } + inheritedPreferenceTokens().filter(source.lowercase()::contains).forEach { token -> + findings += "$relative: inherited non-product preference $token" + } + val secretMarkers = + listOf( + "-----BEGIN " + "PRIVATE KEY-----", + "AWS_" + "SECRET_ACCESS_KEY=", + "gh" + "p_", + "sk_" + "live_", + ) + if (secretMarkers.any(source::contains)) { + findings += "$relative: credential or private-key material in source text" + } + if (productionKotlin && source.lowercase().contains("nsec1")) { + findings += "$relative: secret key literal in production Kotlin" + } + } + + private fun verifyExactContracts(findings: MutableList<String>) { + val requiredPublicFiles = + setOf( + "README.md", + "NOTICE", + "CONTRIBUTING.md", + "SECURITY.md", + "LICENSE", + "LICENSES/GPL-3.0-only.txt", + ) + (requiredPublicFiles - inventory.toSet()).sorted().forEach { relative -> + findings += "$relative: required public repository file is missing" + } + val cargo = text("core/Cargo.toml") + if (cargo.lineSequence().count { it.trim() == "repository = \"$legacyRepository\"" } != 1) { + findings += "core/Cargo.toml: legacy repository allowlist must be exact" + } + val provenance = text(provenancePath) + if (!provenance.contains("source_repository = \"$legacyRepository\"") || + !provenance.contains("canonical_radroots_revision = \"09065a610d95e57acdc895a14c07580fa099e7c3\"") || + !provenance.contains("foundation_baseline = \"a2038b3e25b9e34f0b8fd001f26a8ed10b5772cb\"") + ) { + findings += "$provenancePath: exact source provenance changed" + } + val productCoordinates = + runCatching { + ProductCoordinates.parse(text("config/product/harvestcircle-v1.properties")) + }.getOrElse { error -> + findings += "config/product/harvestcircle-v1.properties: ${error.message}" + null + } + val uniFfi = text("core/crates/harvestcircle_ffi/uniffi.toml") + if (!uniFfi.contains("[crates.harvestcircle_ffi.bindings.kotlin]") || + productCoordinates == null || + !uniFfi.contains("package_name = \"${productCoordinates["ffi.kotlin_package"]}\"") || + !uniFfi.contains("cdylib_name = \"${productCoordinates["ffi.cdylib_name"]}\"") + ) { + findings += "core/crates/harvestcircle_ffi/uniffi.toml: final FFI identity changed" + } + val baseline = text("core/compatibility/harvestcircle-ffi-v4.properties") + if (!baseline.contains("contract.id=harvestcircle-desktop-ffi-v4") || !baseline.contains("contract.major=4")) { + findings += "core/compatibility/harvestcircle-ffi-v4.properties: FFI v4 identity changed" + } + val sharedBuild = text("app/shared/build.gradle.kts") + if (Regex("(?m)^\\s*jvm\\(\"desktop\"\\)").findAll(sharedBuild).count() != 1 || + listOf("androidTarget", "iosArm", "iosX", "js(", "wasm").any(sharedBuild::contains) + ) { + findings += "app/shared/build.gradle.kts: shared KMP target boundary changed" + } + } + + private fun inheritedPreferenceTokens(): List<String> { + val separator = "_" + return listOf( + listOf("use", "radroots", "dns").joinToString(separator), + listOf("use", "radroots", "subnets").joinToString(separator), + listOf("vpn", "on", "demand", "enabled").joinToString(separator), + listOf("run", "as", "exit", "node").joinToString(separator), + listOf("automatically", "check", "for", "updates").joinToString(separator), + listOf("update", "channel").joinToString(separator), + listOf("last", "update", "check", "summary").joinToString(separator), + listOf("alternate", "server", "url").joinToString(separator), + ) + } + + private fun isText(relative: String): Boolean { + val path = Path.of(relative) + return path.extension in textExtensions || path.name in textNames + } + + private fun text(relative: String): String = overrides[relative] ?: readText(relative) + + private fun readText(relative: String): String { + val path = root.resolve(relative) + return if (Files.isRegularFile(path)) path.readText() else "" + } +} diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/GitSourcePolicy.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/GitSourcePolicy.kt @@ -0,0 +1,102 @@ +package org.harvestcircle.buildlogic.plugins.tasks + +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +object GitSourcePolicy { + private val gitExpression = Regex("""git\s*=\s*"([^"]+)"""") + private val revisionExpression = Regex("""rev\s*=\s*"([0-9a-f]{40})"""") + private val forbiddenSpec = Regex("""(?:branch|tag)\s*=""") + + fun validateDependency( + expression: String, + allowedGit: Set<String>, + ): String? { + val git = gitExpression.find(expression)?.groupValues?.get(1) ?: return null + require(git in allowedGit) { "Git dependency source is not allowlisted: $git" } + require(!forbiddenSpec.containsMatchIn(expression)) { "Git dependency uses a branch or tag" } + val revisions = revisionExpression.findAll(expression).map { it.groupValues[1] }.toList() + require(revisions.size == 1) { "Git dependency must use exactly one full revision pin" } + return revisions.single() + } +} + +abstract class VerifyGitSourcePolicy : DefaultTask() { + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val denyConfigFile: RegularFileProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val cargoLockFile: RegularFileProperty + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val cargoManifestFiles: ConfigurableFileCollection + + @TaskAction + fun verify() { + val denyConfig = denyConfigFile.get().asFile.readText() + check(Regex("(?m)^required-git-spec\\s*=\\s*\"rev\"$").containsMatchIn(denyConfig)) { + "cargo-deny must require revision-pinned Git sources" + } + val allowedGit = + Regex("(?s)allow-git\\s*=\\s*\\[(.*?)]") + .find(denyConfig) + ?.groupValues + ?.get(1) + ?.let { block -> Regex("\"([^\"]+)\"").findAll(block).map { it.groupValues[1] }.toSet() } + .orEmpty() + check(allowedGit.isNotEmpty()) { "cargo-deny Git allowlist is empty" } + + val revisions = mutableMapOf<String, MutableSet<String>>() + cargoManifestFiles.files.sortedBy { it.path }.forEach { manifest -> + manifest.readLines().forEachIndexed { index, line -> + if (!line.contains("git")) return@forEachIndexed + val git = Regex("""git\s*=\s*"([^"]+)""").find(line)?.groupValues?.get(1) ?: return@forEachIndexed + val revision = + runCatching { GitSourcePolicy.validateDependency(line, allowedGit) } + .getOrElse { error("${manifest.path}:${index + 1}: ${it.message}") } + ?: return@forEachIndexed + revisions.getOrPut(git) { mutableSetOf() } += revision + } + } + check(revisions.isNotEmpty()) { "No revision-pinned Git dependencies were inspected" } + check( + revisions["https://github.com/rust-nostr/nostr.git"] == + setOf("5bba5163eb77107f82c4a8262cf29d7f33a73219"), + ) { "The direct rust-nostr revision changed" } + + cargoLockFile.get().asFile.useLines { lines -> + lines.filter { it.startsWith("source = \"git+") }.forEach { source -> + check(Regex("\\?rev=[0-9a-f]{40}#[0-9a-f]{40}\"$").containsMatchIn(source)) { + "Cargo.lock contains a Git source without an immutable revision: $source" + } + } + } + + val allowed = allowedGit.first() + check( + GitSourcePolicy.validateDependency( + "dependency = { git = \"$allowed\", rev = \"${"a".repeat(40)}\" }", + allowedGit, + ) == "a".repeat(40), + ) + listOf( + "dependency = { git = \"$allowed\", branch = \"main\" }", + "dependency = { git = \"$allowed\", tag = \"v1.0.0\" }", + "dependency = { git = \"$allowed\" }", + "dependency = { git = \"https://example.invalid/repository\", rev = \"${"b".repeat(40)}\" }", + ).forEach { fixture -> + check(runCatching { GitSourcePolicy.validateDependency(fixture, allowedGit) }.isFailure) { + "Git source policy accepted a mutable or unknown fixture" + } + } + } +} diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/ProductCoordinateConsumers.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/ProductCoordinateConsumers.kt @@ -0,0 +1,85 @@ +package org.harvestcircle.buildlogic.plugins.tasks + +import org.harvestcircle.buildlogic.contracts.ProductCoordinates +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +abstract class VerifyProductCoordinateConsumers : DefaultTask() { + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val manifestFile: RegularFileProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val desktopBuildFile: RegularFileProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val uniFfiConfigFile: RegularFileProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val productBuildFile: RegularFileProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val ffiConsumerFile: RegularFileProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val keyringConsumerFile: RegularFileProperty + + @TaskAction + fun verify() { + val coordinates = ProductCoordinates.load(manifestFile.get().asFile) + val desktopBuild = desktopBuildFile.get().asFile.readText() + listOf( + "product.name", + "product.slug", + "desktop.application_id", + "desktop.bundle_id", + "desktop.main_class", + "ffi.kotlin_package", + "ffi.cdylib_name", + "environment.prefix", + "vendor.name", + "copyright.notice", + ).forEach { key -> + check(desktopBuild.contains("productCoordinates[\"$key\"]")) { + "Desktop build does not consume product coordinate $key" + } + } + listOf( + "desktop.application_id", + "desktop.bundle_id", + "desktop.main_class", + "database.filename", + "keyring.service", + "environment.prefix", + ).forEach { key -> + check(!desktopBuild.contains("\"${coordinates[key]}\"")) { + "Desktop build duplicates the approved value for $key" + } + } + + val uniFfi = uniFfiConfigFile.get().asFile.readText() + check(uniFfi.contains("package_name = \"${coordinates["ffi.kotlin_package"]}\"")) + check(uniFfi.contains("cdylib_name = \"${coordinates["ffi.cdylib_name"]}\"")) + + val productBuild = productBuildFile.get().asFile.readText() + check(productBuild.contains("generate_rust_constants(&source)")) + val ffiConsumer = ffiConsumerFile.get().asFile.readText() + listOf( + "DATABASE_APPLICATION", + "DATABASE_FILENAME", + "DATABASE_ORGANIZATION", + "DATABASE_QUALIFIER", + "DEVELOPMENT_DATA_DIR_ENVIRONMENT", + ).forEach { constant -> check(ffiConsumer.contains(constant)) } + check(keyringConsumerFile.get().asFile.readText().contains("harvestcircle_product::KEYRING_SERVICE")) + } +} diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/ProductCoordinatesTask.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/ProductCoordinatesTask.kt @@ -0,0 +1,179 @@ +package org.harvestcircle.buildlogic.plugins.tasks + +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.harvestcircle.buildlogic.contracts.FfiCompatibilityBaseline +import org.harvestcircle.buildlogic.contracts.ProductCoordinates +import org.harvestcircle.buildlogic.contracts.SourceProvenance + +abstract class VerifyProductCoordinates : DefaultTask() { + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val manifestFile: RegularFileProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val uniFfiConfigFile: RegularFileProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val ffiBaselineFile: RegularFileProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val sourceProvenanceFile: RegularFileProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val nativeCompatibilityFile: RegularFileProperty + + @TaskAction + fun verify() { + val source = manifestFile.get().asFile.readText() + val coordinates = ProductCoordinates.parse(source) + check(coordinates.digest.matches(Regex("[0-9a-f]{64}"))) + val equivalentSources = + listOf( + source.replace("\n", "\r\n"), + source.trimEnd(), + "# comment\n$source", + source.lineSequence().joinToString("\n") { line -> + if (line.isBlank() || line.startsWith('#')) line else line.replaceFirst("=", " = ") + }, + ) + equivalentSources.forEach { equivalent -> + check(ProductCoordinates.parse(equivalent).digest == coordinates.digest) + } + check(runCatching { ProductCoordinates.parse("\uFEFF$source") }.isFailure) + check(runCatching { ProductCoordinates.parse(source + "\nschema=${ProductCoordinates.schema}") }.isFailure) + check(runCatching { ProductCoordinates.parse(source + "\nunknown=value") }.isFailure) + check(runCatching { ProductCoordinates.parse(source.substringAfter('\n')) }.isFailure) + check(runCatching { ProductCoordinates.parse(source.replaceCoordinate("product.slug", "INVALID")) }.isFailure) + check( + runCatching { + ProductCoordinates.parse(source.replaceCoordinate("database.filename", "../other.sqlite3")) + }.isFailure, + ) + validCoordinateMutations.forEach { (key, replacement) -> + val mutated = ProductCoordinates.parse(source.replaceCoordinate(key, replacement)) + check(mutated[key] == replacement) + check(mutated.digest != coordinates.digest) + } + + val uniFfiConfig = uniFfiConfigFile.get().asFile.readText() + check( + uniFfiConfig.contains( + "package_name = \"${coordinates["ffi.kotlin_package"]}\"", + ), + ) + check( + uniFfiConfig.contains( + "cdylib_name = \"${coordinates["ffi.cdylib_name"]}\"", + ), + ) + + val baseline = FfiCompatibilityBaseline.load(ffiBaselineFile.get().asFile) + val baselineSource = ffiBaselineFile.get().asFile.readText() + check(runCatching { FfiCompatibilityBaseline.parse(baselineSource + "\nunknown=value") }.isFailure) + check(runCatching { FfiCompatibilityBaseline.parse(baselineSource.substringAfter('\n')) }.isFailure) + check(runCatching { FfiCompatibilityBaseline.parse("\uFEFF$baselineSource") }.isFailure) + check( + runCatching { + FfiCompatibilityBaseline.parse( + baselineSource.replace( + Regex("(?m)^contract\\.hash=.*$"), + "contract.hash=malformed", + ), + ) + }.isFailure, + ) + check( + runCatching { + FfiCompatibilityBaseline.parse( + baselineSource.replace( + Regex("(?m)^package\\.version=.*$"), + "package.version=invalid", + ), + ) + }.isFailure, + ) + check( + runCatching { + FfiCompatibilityBaseline.parse( + baselineSource + "\ncontract.id=harvestcircle-desktop-ffi-v4", + ) + }.isFailure, + ) + check(baseline["product.coordinate_digest"] == coordinates.digest) + val provenanceSource = sourceProvenanceFile.get().asFile.readText() + val provenance = SourceProvenance.parse(provenanceSource) + check(baseline["source.provenance_digest"] == provenance.digest) + check(provenance.foundationBaseline == baseline["source.foundation_baseline"]) + val equivalentProvenance = + listOf( + provenanceSource.replace("\n", "\r\n"), + provenanceSource.trimEnd(), + "# comment\n$provenanceSource", + provenanceSource.replace( + "component = \"domain\"\ncommit = \"a4d7deebec3e2ce2c1daa455de6d79857839aed0\"", + "commit = \"a4d7deebec3e2ce2c1daa455de6d79857839aed0\"\ncomponent = \"domain\"", + ), + ) + equivalentProvenance.forEach { equivalent -> + check(SourceProvenance.parse(equivalent).digest == provenance.digest) + } + check(runCatching { SourceProvenance.parse("\uFEFF$provenanceSource") }.isFailure) + check(runCatching { SourceProvenance.parse("unknown = \"value\"\n$provenanceSource") }.isFailure) + check( + SourceProvenance.parse( + provenanceSource.replace( + "a4d7deebec3e2ce2c1daa455de6d79857839aed0", + "b4d7deebec3e2ce2c1daa455de6d79857839aed0", + ), + ).digest != provenance.digest, + ) + val nativeCompatibility = nativeCompatibilityFile.get().asFile.readText() + check(nativeCompatibility.contains("NativeCompatibilityExpectations as Expected")) + listOf( + "contract.id", + "contract.hash", + "product.coordinate_digest", + "source.provenance_digest", + "source.foundation_baseline", + ).forEach { key -> check(!nativeCompatibility.contains(baseline[key])) } + } + + private fun String.replaceCoordinate( + key: String, + replacement: String, + ): String = + lineSequence().joinToString("\n") { line -> + if (line.substringBefore('=', missingDelimiterValue = "") == key) "$key=$replacement" else line + } + + private val validCoordinateMutations = + linkedMapOf( + "product.name" to "Harvest Circle Test", + "product.slug" to "harvestcircle_test", + "kotlin.root_namespace" to "org.example", + "desktop.application_id" to "org.example.desktop", + "desktop.bundle_id" to "org.example.bundle", + "desktop.main_class" to "org.example.MainKt", + "ffi.kotlin_package" to "org.example.ffi", + "ffi.cdylib_name" to "example_ffi", + "database.qualifier" to "com", + "database.organization" to "example", + "database.application" to "test", + "database.filename" to "example.sqlite3", + "keyring.service" to "org.example.desktop.nostr", + "environment.prefix" to "EXAMPLE_", + "vendor.name" to "Example Cooperative", + "copyright.notice" to "Copyright Example contributors", + ).also { mutations -> + check(mutations.size + 1 == ProductCoordinates.requiredKeys.size) + } +} diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/VerificationLanes.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/VerificationLanes.kt @@ -0,0 +1,104 @@ +package org.harvestcircle.buildlogic.plugins.tasks + +import org.harvestcircle.buildlogic.contracts.ProductCoordinates +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +object VerificationLanes { + private fun expected(environmentPrefix: String) = + linkedMapOf( + "schema" to "harvestcircle.verification-lanes.v2", + "orchestration" to "standalone-make", + "source.command" to "make source-check", + "source.runner" to "host", + "source.credentials" to "none", + "package.command" to "make package-check", + "package.runners" to "linux,macos,windows", + "package.credentials" to "none", + "provenance.commit" to environmentPrefix + "BUILD_SOURCE_COMMIT", + "provenance.dirty" to environmentPrefix + "BUILD_SOURCE_DIRTY", + "provenance.radroots" to environmentPrefix + "BUILD_RADROOTS_REVISION", + "provenance.epoch" to "SOURCE_DATE_EPOCH", + "signing.command" to "make signing-check", + "signing.runner" to "macos", + "signing.credentials" to "signing", + "notarization.command" to "make notarization-check", + "notarization.runner" to "macos", + "notarization.credentials" to "notarization", + ) + + fun parse( + source: String, + environmentPrefix: String, + ): Map<String, String> { + val expected = expected(environmentPrefix) + val parsed = linkedMapOf<String, String>() + source.trimEnd('\n', '\r').lineSequence().forEachIndexed { index, raw -> + val line = raw.trim() + require(line.isNotEmpty() && !line.startsWith('#')) { + "Verification lane policy contains an empty or comment line at ${index + 1}" + } + val separator = line.indexOf('=') + require(separator > 0 && separator < line.lastIndex && line.indexOf('=', separator + 1) == -1) { + "Verification lane policy contains malformed syntax at ${index + 1}" + } + val key = line.substring(0, separator) + val value = line.substring(separator + 1) + require(parsed.put(key, value) == null) { "Duplicate verification lane key: $key" } + } + require(parsed.keys == expected.keys) { "Verification lane keys do not match the authority" } + require(parsed == expected) { "Verification lane values do not match the authority" } + return parsed + } +} + +abstract class VerifyVerificationLanes : DefaultTask() { + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val policyFile: RegularFileProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val productManifestFile: RegularFileProperty + + @get:Internal + abstract val repositoryRoot: DirectoryProperty + + @TaskAction + fun verify() { + val source = policyFile.get().asFile.readText() + val environmentPrefix = + ProductCoordinates.load(productManifestFile.get().asFile)["environment.prefix"] + val policy = VerificationLanes.parse(source, environmentPrefix) + check(policy.size == 18) + check(runCatching { VerificationLanes.parse(source + "source.workflow=forbidden", environmentPrefix) }.isFailure) + check( + runCatching { + VerificationLanes.parse(source.replace("credentials=none", "credentials=all"), environmentPrefix) + }.isFailure, + ) + check( + runCatching { + VerificationLanes.parse(source.replace("source.runner=host", "source.runner=remote"), environmentPrefix) + }.isFailure, + ) + val root = repositoryRoot.get().asFile.toPath() + val makefile = root.resolve("Makefile").toFile().readText() + listOf("source.command", "package.command", "signing.command", "notarization.command").forEach { key -> + val command = policy.getValue(key) + val target = command.removePrefix("make ") + check(command == "make $target" && Regex("(?m)^${Regex.escape(target)}:").containsMatchIn(makefile)) { + "Verification lane $key does not name a standalone Make target" + } + } + check(policy.values.none { ".github/" in it || ".act/" in it }) { + "Standalone verification policy must not reference an orchestration root" + } + } +} diff --git a/build.gradle.kts b/build.gradle.kts @@ -1,96 +1,7 @@ -import org.harvestcircle.gradle.VerifyFoundationBoundaries -import org.harvestcircle.gradle.VerifyGitSourcePolicy -import org.harvestcircle.gradle.VerifyProductCoordinateConsumers -import org.harvestcircle.gradle.VerifyProductCoordinates -import org.harvestcircle.gradle.VerifyVerificationLanes - plugins { + id("org.harvestcircle.build.root") alias(libs.plugins.kotlin.multiplatform) apply false alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.compose.multiplatform) apply false alias(libs.plugins.compose.compiler) apply false } - -val productCoordinatesFile = layout.projectDirectory.file("config/product/harvestcircle-v1.properties") -val ffiCompatibilityBaselineFile = - layout.projectDirectory.file("core/compatibility/harvestcircle-ffi-v4.properties") -val verificationLanesFile = layout.projectDirectory.file("config/verification/lanes-v2.properties") -val legacyProduct = "stu" + "dio" - -val verifyProductCoordinates by tasks.registering(VerifyProductCoordinates::class) { - group = "verification" - description = "Validates the canonical HarvestCircle product-coordinate authority." - manifestFile.set(productCoordinatesFile) - uniFfiConfigFile.set( - layout.projectDirectory.file("core/crates/harvestcircle_ffi/uniffi.toml"), - ) - ffiBaselineFile.set(ffiCompatibilityBaselineFile) - sourceProvenanceFile.set( - layout.projectDirectory.file("core/provenance/$legacyProduct-import-v1.toml"), - ) - nativeCompatibilityFile.set( - layout.projectDirectory.file( - "app/desktop/src/main/kotlin/org/harvestcircle/application/NativeCompatibility.kt", - ), - ) -} - -val verifyCompatibilityBaseline by tasks.registering { - group = "verification" - description = "Validates the generated-code and native compatibility baseline." - dependsOn(verifyProductCoordinates) -} - -val verifySourceProvenance by tasks.registering { - group = "verification" - description = "Validates canonical source provenance and its governed digest." - dependsOn(verifyProductCoordinates) -} - -val verifyProductCoordinateConsumers by tasks.registering(VerifyProductCoordinateConsumers::class) { - group = "verification" - description = "Validates that build and runtime identities consume the product manifest." - manifestFile.set(productCoordinatesFile) - desktopBuildFile.set(layout.projectDirectory.file("app/desktop/build.gradle.kts")) - uniFfiConfigFile.set(layout.projectDirectory.file("core/crates/harvestcircle_ffi/uniffi.toml")) - productBuildFile.set(layout.projectDirectory.file("core/crates/harvestcircle_product/build.rs")) - ffiConsumerFile.set(layout.projectDirectory.file("core/crates/harvestcircle_ffi/src/commands.rs")) - keyringConsumerFile.set(layout.projectDirectory.file("core/crates/harvestcircle_storage/src/os_keyring.rs")) -} - -val verifyVerificationLanes by tasks.registering(VerifyVerificationLanes::class) { - group = "verification" - description = "Validates forge-agnostic verification lanes and least-privilege policy." - policyFile.set(verificationLanesFile) - productManifestFile.set(productCoordinatesFile) - repositoryRoot.set(layout.projectDirectory) -} - -val verifyFoundationBoundaries by tasks.registering(VerifyFoundationBoundaries::class) { - group = "verification" - description = "Audits tracked sources against the HarvestCircle foundation boundaries." - repositoryRoot.set(layout.projectDirectory) - gitAware.set(true) -} - -val verifyFoundationArchive by tasks.registering(VerifyFoundationBoundaries::class) { - group = "verification" - description = "Audits a source-archive inventory without Git metadata." - repositoryRoot.set(layout.projectDirectory) - gitAware.set(false) -} - -val verifyGitSourcePolicy by tasks.registering(VerifyGitSourcePolicy::class) { - group = "verification" - description = "Validates immutable and allowlisted Cargo Git dependency sources." - denyConfigFile.set(layout.projectDirectory.file("core/deny.toml")) - cargoLockFile.set(layout.projectDirectory.file("core/Cargo.lock")) - cargoManifestFiles.from(fileTree("core") { include("Cargo.toml", "crates/*/Cargo.toml") }) -} - -verifyFoundationBoundaries.configure { dependsOn(verifyGitSourcePolicy) } -verifyFoundationArchive.configure { dependsOn(verifyGitSourcePolicy) } - -providers.environmentVariable("EXT_BUILD_GRADLE_BUILD_DIR").orNull?.let { extBuildGradleRoot -> - layout.buildDirectory.set(file(extBuildGradleRoot).resolve("root")) -}