Output:
Chunk ID: 2797bb
Wall time: 0.6674 seconds
Process exited with code 0
Original token count: 3255
Output:
{"baseRefName":"master","comments":[{"id":"IC_kwDOR2Hmq88AAAABDWGo2Q","author":{"login":"Mesya82"},"authorAssociation":"MEMBER","body":"## Code review\n\n### Overview\nPhase 1 of the regression scaffold: five JUnit marker annotations (`Smoke`, `Regression`, `RegressionFull`, `BillingSandbox`, `Quarantined`), four Gradle aggregates (`regressionCheck`, `regressionFullNightlyCoreCheck`, `regressionFullExtendedCheck`, `regressionFullCheck`) implemented as `GradleBuild` child invocations against `pixel2api33DebugAndroidTest`, a workflow pair (`regression.yml` + `regression-label.yml`) mirroring smoke, `androidTest` helpers (`Permissions`, `Camera`, `SystemPicker`, `FixtureAssetsRule`) + a minimal `test_receipt.pdf`, and a JVM `RegressionTierScannerTest` that source-scans `regression/` for tier-annotation invariants. `SmokeTest` gains `@Smoke`; the smoke-device runner-arg now applies `annotation = Smoke` unless an explicit annotation is passed.\n\n### Correctness\n\n**1. Nightly schedule runs the non-quarantined regression set twice.** `regression.yml` invokes `regressionCheck regressionFullCheck`, and `regressionFullCheck` already depends on `regressionFullNightlyCoreCheck` whose `annotation=Regression` filter is a superset of `regressionCheck`'s `Regression && !Quarantined`. Because each `GradleBuild` is a fresh nested invocation, this is real wall-clock + CI-minute waste — not a free fail-fast (the child build re-configures from scratch). Either drop `regressionCheck` from the schedule line, or keep it and comment the fail-fast intent.\n\n**2. `isExplicitAnnotationRun` is captured eagerly at outer-build configuration time** (`app/build.gradle.kts:222`). It's a `Boolean`, not a `Provider<Boolean>`, so `providers.gradleProperty(...).isPresent` resolves when outer `afterEvaluate` runs. Fine for child `GradleBuild` invocations (they configure fresh) and fine for direct `-P` overrides. Worth a one-line note so the next reader doesn't try to switch it to a provider chain.\n\n**3. `gradle.startParameter.projectProperties + runnerArguments.mapKeys{…}`** (build.gradle.kts:282) makes the hard-coded annotation/notAnnotation filters win over any outer `-P…annotation=Foo`. Almost certainly the intent — but silent override is a footgun for anyone trying to narrow a run from the CLI. A doc-comment on the task descriptions would suffice.\n\n### Conventions\n\n**4. `regressionFullNightlyCoreCheck` bakes a CI cadence into a build-graph identifier.** What it *is* is \"the core regression tier, including quarantined\". A consumer who runs it manually from `gradlew tasks` will be misled. Suggest `regressionFullCoreCheck`; let the workflow file own the \"nightly\" framing.\n\n**5. `RegressionTierScanner` parses Kotlin with line-oriented regex.** It handles the present fixtures, but several common shapes will trip it: multi-line class headers, primary-constructor parameter lists that span lines, generic type parameters before the supertype, nested classes (`pendingAnnotations` resets on inner-class declarations and leaks across siblings since there's no brace tracking). Phase 1 has zero files under `regression/`, so the production scan is a no-op today — but when Phase 2 lands real test classes the scanner will silently miss violations rather than fail loudly. Two cheap mitigations: (a) header comment naming the supported shapes; (b) one fixture that asserts an unsupported shape currently *passes*, so the limit is documented in tests rather than folklore.\n\n### Workflows\n- `regression.yml` carrying `workflow_dispatch` + `workflow_call` + `schedule` in one file is fine; the `schedule` only fires from `master`, consistent with GitHub's behavior.\n- `regression-label.yml` and `smoke-label.yml` are byte-for-byte structural twins. Acceptable.\n\n### Tests / fixtures\n- `RegressionTierScannerTest` covers the four main violation paths (zero-tier, two-tier, orphan-quarantine, inherited tier) and the forward-compat annotation-argument case. Good coverage of the parser's actual contract.\n- `FixtureAssetsRule.copyToCache` re-appends to `cacheFiles` if called twice with the same asset; cleanup is idempotent, so harmless.\n- `Permissions.acceptIfShown` worst-case is 4 × 2s; `Camera.captureAndAccept` is up to 5 × 10s. Worth knowing when a test hangs.\n\n### Docs\n**6. `plan-regression-tests.md` lists \"one small PDF and one small JPG\"**, but only `test_receipt.pdf` was added. If the JPG is intentionally Phase-2, change the doc to \"one small PDF (JPG follows in Phase 2)\"; otherwise add the JPG now.\n\n### Security\nNo surface-area concerns. Workflows are `contents: read`; the label workflow's `pull-requests: read` is the minimum needed for `pull_request` events. No new secrets, no network calls outside the existing GMD pipeline.","createdAt":"2026-05-22T14:12:51Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/yorvana/android/pull/123#issuecomment-4519471321","viewerDidAuthor":true},{"id":"IC_kwDOR2Hmq88AAAABDXOquA","author":{"login":"Mesya82"},"authorAssociation":"MEMBER","body":"## Re-review after `6076aef`\n\n### Delta vs prior 6 concerns\n\n| # | Prior issue | Status |\n|---|---|---|\n| 1 | Cron ran `regressionCheck` *and* `regressionFullCheck` (Regression set executed twice) | ✅ Fixed — cron now runs only `regressionFullCheck` (`.github/workflows/regression.yml:55`) |\n| 2 | `isExplicitAnnotationRun` evaluation timing unexplained | ✅ Comment added (`app/build.gradle.kts:233-234`); behaviour is correct given GradleBuild child builds see their own `startParameter.projectProperties` |\n| 3 | Silent `-P` override in regression child builds | ✅ Comment added (`app/build.gradle.kts:282-283`) |\n| 4 | Naming bleed — `regressionFullNightlyCoreCheck` ran outside the nightly path | ✅ Renamed to `regressionFullCoreCheck`, propagated to plan doc |\n| 5 | Regex Kotlin parser fragility | ✅ Documented in scanner comment + pinned with `unsupportedBacktickTestNamesAreNotScanned` fixture |\n| 6 | Plan-doc bundled-JPG mismatch | ✅ Doc now says \"one small PDF in Phase 1; JPG follows in Phase 2 if needed\" |\n\n### New issues from the full sweep\n\n1. **`FixtureAssetsRule.copyToDownloads` leaks MediaStore rows on partial failure** (`app/src/androidTest/java/com/yorvana/testsupport/FixtureAssetsRule.kt:46-60`). The Uri is appended to `mediaStoreUris` *after* the `openOutputStream`/copy/`IS_PENDING=0` update. If any step in that window throws, the row stays in MediaStore with `IS_PENDING=1` and `after()` won't clean it. Move `mediaStoreUris += uri` to immediately follow the `resolver.insert(...)` line so the cleanup list always reflects what was inserted.\n\n2. **Live-source scan is a no-op today** (`RegressionTierScannerTest.kt:7-15`). The `regression/` directory doesn't exist yet, and `scanDirectory` returns `emptyList()` for a missing root, so `androidTestRegressionSourcesHaveValidTierAnnotations` passes vacuously — only the in-memory fixtures actually exercise the parser. The path from `File(\"src/androidTest/.../regression\")` to the real source tree is unverified until Phase 2 ships a regression test. Worth either (a) seeding one minimal `@Regression`-annotated class under the regression package now so the live scan actually runs against real source, or (b) failing the test when the root is missing rather than treating absence as success.\n\n3. **`bySimpleName` collisions in inheritance walk** (`RegressionTierScannerTest.kt:202-204`). `inheritedClassScopes` keys the supertype lookup by simple name (`classes.associateBy { it.simpleName }`), so two classes sharing a simple name across packages silently mask each other — the walk follows whichever entry wins `associateBy`'s last-write semantics. Phase 1 doesn't hit it, but a future regression base class named the same as another test class would behave nondeterministically. Key by `fqName` (or document the limitation alongside the backtick caveat).\n\n### Clean across the rest of the diff\n- Workflows: `regression.yml`/`regression-label.yml` mirror `smoke.yml`/`smoke-label.yml` exactly — same KVM udev rule, same Gradle/JDK setup, same concurrency-group/cancel-in-progress shape, same `if-no-files-found: warn` artifact pattern. Distinct artifact name (`gmd-regression-test-reports`) avoids collision when both flows run.\n- `isTaskForDevice` refactor correctly tightens the previous `contains(..., ignoreCase = true)` matches; the exact-name check makes the smoke-annotation injection device-scoped.\n- `BillingSandbox` annotation exists but is intentionally unwired in Phase 1 (deferred to Phase 5 per plan doc); fine.\n- `@Smoke` added at class level on `SmokeTest` — pure annotation, no test-logic change.\n- `Camera`, `Permissions`, `SystemPicker` helpers are stubs as the plan promised; fallback selector chains are reasonable for the GMD target.\n- `RegressionTierScannerTest` fixture coverage (zero tier, two tier, orphan quarantine, inherited class, annotation-args + unknown-sibling, unsupported backticks) matches the documented contract.","createdAt":"2026-05-22T16:33:40Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/yorvana/android/pull/123#issuecomment-4520651448","viewerDidAuthor":true},{"id":"IC_kwDOR2Hmq88AAAABDakIkg","author":{"login":"Mesya82"},"authorAssociation":"MEMBER","body":"## Re-review after `25d1844`\n\nDelta against the prior sweep on `6076aef`.\n\n### Prior sweep issues — status\n\n| Prior sweep issue | Status | Verification |\n|---|---|---|\n| `mediaStoreUris += uri` ordering leak | ✅ Fixed | `FixtureAssetsRule.kt:50` — append now happens immediately after `resolver.insert(...)`, before the copy/`IS_PENDING` window that can throw |\n| Live-source scan vacuous (no `regression/` dir) | ✅ Fixed | New `RegressionScaffoldTest.kt` lands a `@Regression class` in `com.yorvana.regression`, so `scanDirectory(...)` now has at least one source to walk |\n| `bySimpleName` collisions on inherited supers | ✅ Fixed | Renamed to `byFqName`; `String.toFqName(packageName)` normalizes bare super names; `ParsedClass.superClassFqName` carries the qualified key; new `inheritedClassTiersUsePackageQualifiedLookup` fixture pins it |\n\n### Notes on the new code\n\n- **`toFqName` resolution rule** — for an unqualified super (`class Child : Base`), the scanner assumes `Base` lives in the child's package. Correct for the same-package case the fixture exercises, but doesn't model Kotlin imports: a `: Base` whose `Base` is `import com.yorvana.shared.Base` would resolve to `current.pkg.Base`, miss the lookup, and silently drop the inherited scope. The failure mode is a *false-positive* violation (test fails the guard with \"0 tier annotations\") rather than a silent pass, so the direction is safe — but worth a one-line fixture pinning the behavior so a future contributor doesn't quietly start relying on it. The scanner comment at `RegressionTierScannerTest.kt:192-195` covers the spirit of unsupported shapes but not this specific shape.\n- **`RegressionScaffoldTest.scaffoldIsDiscoverable() = Unit`** — placeholder serves a dual purpose (target for the live-source scanner, and at least one runtime match for `pixel2api33DebugAndroidTest -P…annotation=…tiers.Regression`). Reasonable for Phase 1.\n- **Cleanup ordering on `copyToDownloads` throw** — with the URI tracked before the write, a thrown `error(...)` from `openOutputStream` leaves `IS_PENDING=1`, but `after()`'s `resolver.delete(uri, …)` removes the pending row cleanly. ✓\n- **Fixture proves the fix** — `inheritedClassTiersUsePackageQualifiedLookup` would fail under the pre-fix `bySimpleName + associateBy` because the second `Base` (in `com.yorvana.other`) overwrites the first, dropping the inherited `@Regression`. With FQ keys it resolves deterministically. ✓\n\n### Verdict\n\nDelta is clean — all three prior items resolved, no new regressions, only an optional fixture to pin the cross-package-import limitation.","createdAt":"2026-05-23T04:25:54Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/yorvana/android/pull/123#issuecomment-4524148882","viewerDidAuthor":true}],"headRefName":"issue-114-regression-infra","reviewDecision":"","reviews":[],"statusCheckRollup":[{"__typename":"CheckRun","completedAt":"2026-05-22T16:37:37Z","conclusion":"SKIPPED","detailsUrl":"https://github.com/yorvana/android/actions/runs/26300040303/job/77422542218","name":"ci","startedAt":"2026-05-22T16:37:37Z","status":"COMPLETED","workflowName":"CI (Manual Label)"},{"__typename":"CheckRun","completedAt":"2026-05-22T16:38:19Z","conclusion":"SUCCESS","detailsUrl":"https://github.com/yorvana/android/actions/runs/26300040300/job/77422541637","name":"Semgrep Scan","startedAt":"2026-05-22T16:37:40Z","status":"COMPLETED","workflowName":"Semgrep"}],"url":"https://github.com/yorvana/android/pull/123"}


The latest comment is clean overall, with one optional request: pin the scanner’s limitation around imported supertypes. I’m going to add a focused fixture that documents the current behavior, then run the scanner test and quality checks.


sed -n '1,320p' app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt


sed -n '1,120p' app/src/androidTest/java/com/yorvana/regression/RegressionScaffoldTest.kt


Output:
Chunk ID: b4a0e2
Wall time: 0.1702 seconds
Process exited with code 0
Original token count: 2776
Output:
package com.yorvana.testsupport

import com.google.common.truth.Truth.assertThat
import org.junit.Test
import java.io.File

class RegressionTierScannerTest {
    @Test
    fun androidTestRegressionSourcesHaveValidTierAnnotations() {
        val regressionRoot = File("src/androidTest/java/com/yorvana/regression")

        val violations = RegressionTierScanner.scanDirectory(regressionRoot)

        assertThat(violations).isEmpty()
    }

    @Test
    fun zeroTierFixtureFails() {
        val violations =
            scanFixture(
                """
                package com.yorvana.regression

                import org.junit.Test

                class MissingTierTest {
                    @Test
                    fun runsWithoutTier() = Unit
                }
                """,
            )

        assertThat(violations.single()).contains("has 0 tier annotations")
    }

    @Test
    fun twoTierFixtureFails() {
        val violations =
            scanFixture(
                """
                package com.yorvana.regression

                import com.yorvana.testsupport.tiers.Regression
                import com.yorvana.testsupport.tiers.RegressionFull
                import org.junit.Test

                class TooManyTiersTest {
                    @Regression
                    @RegressionFull
                    @Test
                    fun runsWithTwoTiers() = Unit
                }
                """,
            )

        assertThat(violations.single()).contains("has 2 tier annotations")
    }

    @Test
    fun orphanQuarantineFixtureFails() {
        val violations =
            scanFixture(
                """
                package com.yorvana.regression

                import com.yorvana.testsupport.tiers.Quarantined
                import org.junit.Test

                class OrphanQuarantineTest {
                    @Quarantined
                    @Test
                    fun quarantinedWithoutTier() = Unit
                }
                """,
            )

        assertThat(violations).contains("com.yorvana.regression.OrphanQuarantineTest.quarantinedWithoutTier has 0 tier annotations")
        assertThat(violations)
            .contains(
                "com.yorvana.regression.OrphanQuarantineTest.quarantinedWithoutTier " +
                    "is quarantined without a tier at the same effective scope",
            )
    }

    @Test
    fun inheritedClassTierFixturePasses() {
        val violations =
            scanFixture(
                """
                package com.yorvana.regression

                import com.yorvana.testsupport.tiers.Regression
                import org.junit.Test

                @Regression
                open class RegressionBase

                class InheritedTierTest : RegressionBase() {
                    @Test
                    fun inheritsClassTier() = Unit
                }
                """,
            )

        assertThat(violations).isEmpty()
    }

    @Test
    fun inheritedClassTiersUsePackageQualifiedLookup() {
        val violations =
            RegressionTierScanner.scanSources(
                mapOf(
                    "src/androidTest/java/com/yorvana/regression/Base.kt" to
                        """
                        package com.yorvana.regression

                        import com.yorvana.testsupport.tiers.Regression
                        import org.junit.Test

                        @Regression
                        open class Base

                        class SamePackageChildTest : Base() {
                            @Test
                            fun inheritsSamePackageTier() = Unit
                        }
                        """.trimIndent(),
                    "src/androidTest/java/com/yorvana/other/Base.kt" to
                        """
                        package com.yorvana.other

                        open class Base
                        """.trimIndent(),
                ),
            )

        assertThat(violations).isEmpty()
    }

    @Test
    fun annotationArgumentsAndUnknownSiblingsAreAccepted() {
        val violations =
            scanFixture(
                """
                package com.yorvana.regression

                import com.yorvana.testsupport.tiers.Regression
                import com.yorvana.testsupport.tiers.Quarantined
                import org.junit.Test

                annotation class ScenarioId(val value: String)

                class ForwardCompatibleTest {
                    @Regression
                    @Quarantined(since = "2026-05-22", issue = 120, reason = "fixture")
                    @ScenarioId("R-A01")
                    @Test
                    fun acceptsArgumentsAndUnknownSibling() = Unit
                }
                """,
            )

        assertThat(violations).isEmpty()
    }

    @Test
    fun unsupportedBacktickTestNamesAreNotScanned() {
        val violations =
            scanFixture(
                """
                package com.yorvana.regression

                import org.junit.Test

                class UnsupportedBacktickNameTest {
                    @Test
                    fun `missing tier slips through because this shape is unsupported`() = Unit
                }
                """,
            )

        assertThat(violations).isEmpty()
    }

    private fun scanFixture(source: String): List<String> = RegressionTierScanner.scanSources(mapOf("Fixture.kt" to source.trimIndent()))
}

private object RegressionTierScanner {
    private val TIER_NAMES = setOf("Regression", "RegressionFull", "BillingSandbox")
    private const val QUARANTINE_NAME = "Quarantined"

    // Source-level guard for simple regression-test shapes only: package declarations, annotations,
    // class headers, inheritance, and @Test function names must be discoverable on single lines.
    // Unsupported Kotlin shapes are documented by fixture tests so future phases can either avoid
    // them under com.yorvana.regression or replace this scanner with a real Kotlin parser.
    fun scanDirectory(root: File): List<String> {
        if (!root.exists()) return emptyList()
        val sources =
            root
                .walkTopDown()
                .filter { it.isFile && it.extension == "kt" }
                .associate { it.path to it.readText() }
        return scanSources(sources)
    }

    fun scanSources(sources: Map<String, String>): List<String> {
        val classes = sources.values.flatMap { source -> parseSource(source) }
        val byFqName = classes.associateBy { it.fqName }
        val violations = mutableListOf<String>()

        classes.forEach { testClass ->
            testClass.testMethods.forEach { method ->
                val scopes =
                    listOf(
                        AnnotationScope(method.annotations),
                        AnnotationScope(testClass.annotations),
                    ) +
                        inheritedClassScopes(testClass, byFqName)
                val effectiveTierScope = scopes.firstOrNull { scope -> scope.annotations.count { it in TIER_NAMES } > 0 }
                val tierCount = effectiveTierScope?.annotations?.count { it in TIER_NAMES } ?: 0
                val effectiveQuarantineScope = scopes.firstOrNull { QUARANTINE_NAME in it.annotations }

                if (tierCount != 1) {
                    violations += "${testClass.fqName}.${method.name} has $tierCount tier annotations"
                }
                if (effectiveQuarantineScope != null && effectiveQuarantineScope != effectiveTierScope) {
                    violations +=
                        "${testClass.fqName}.${method.name} is quarantined without a tier at the same effective scope"
                }
            }
        }

        return violations
    }

    private fun inheritedClassScopes(
        testClass: ParsedClass,
        byFqName: Map<String, ParsedClass>,
    ): List<AnnotationScope> {
        val scopes = mutableListOf<AnnotationScope>()
        var currentSuperName = testClass.superClassFqName
        val seen = mutableSetOf<String>()
        while (currentSuperName != null && seen.add(currentSuperName)) {
            val superClass = byFqName[currentSuperName] ?: break
            scopes += AnnotationScope(superClass.annotations)
            currentSuperName = superClass.superClassFqName
        }
        return scopes
    }

    private fun parseSource(source: String): List<ParsedClass> {
        val packageName =
            Regex("""^\s*package\s+([A-Za-z_][\w.]*)""", RegexOption.MULTILINE)
                .find(source)
                ?.groupValues
                ?.get(1)
                .orEmpty()
        val classes = mutableListOf<ParsedClass>()
        var currentClass: ParsedClass? = null
        var pendingAnnotations = emptySet<String>()

        source.lineSequence().forEach { rawLine ->
            val line = rawLine.trim()
            val annotations = extractAnnotations(line)
            if (annotations.isNotEmpty()) {
                pendingAnnotations = pendingAnnotations + annotations
            }

            val classMatch = Regex("""\bclass\s+([A-Za-z_]\w*)\s*(?::\s*([A-Za-z_][\w.]*))?""").find(line)
            if (classMatch != null) {
                val simpleName = classMatch.groupValues[1]
                val superClassName =
                    classMatch.groupValues
                        .getOrNull(2)
                        ?.takeIf { it.isNotBlank() }
                currentClass =
                    ParsedClass(
                        packageName = packageName,
                        simpleName = simpleName,
                        superClassFqName = superClassName?.toFqName(packageName),
                        annotations = pendingAnnotations,
                        testMethods = mutableListOf(),
                    )
                classes += currentClass
                pendingAnnotations = emptySet()
                return@forEach
            }

            val methodMatch = Regex("""\bfun\s+([A-Za-z_]\w*)\s*\(""").find(line)
            if (methodMatch != null) {
                if ("Test" in pendingAnnotations) {
                    currentClass?.testMethods?.add(
                        ParsedMethod(
                            name = methodMatch.groupValues[1],
                            annotations = pendingAnnotations,
                        ),
                    )
                }
                pendingAnnotations = emptySet()
            }
        }

        return classes
    }

    private fun String.toFqName(packageName: String): String =
        if ('.' in this) {
            this
        } else {
            listOf(packageName, this).filter { it.isNotBlank() }.joinToString(".")
        }

    private fun extractAnnotations(line: String): Set<String> =
        Regex("""@([A-Za-z_][\w.]*)""")
            .findAll(line)
            .map { it.groupValues[1].substringAfterLast('.') }
            .toSet()

    private data class ParsedClass(
        val packageName: String,


Output:
Chunk ID: 023725
Wall time: 0.1862 seconds
Process exited with code 0
Original token count: 50
Output:
package com.yorvana.regression

import com.yorvana.testsupport.tiers.Regression
import org.junit.Test

@Regression
class RegressionScaffoldTest {
    @Test
    fun scaffoldIsDiscoverable() = Unit
}


sed -n '320,420p' app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt


Output:
Chunk ID: 89f2d9
Wall time: 0.1204 seconds
Process exited with code 0
Original token count: 128
Output:
        val packageName: String,
        val simpleName: String,
        val superClassFqName: String?,
        val annotations: Set<String>,
        val testMethods: MutableList<ParsedMethod>,
    ) {
        val fqName: String = listOf(packageName, simpleName).filter { it.isNotBlank() }.joinToString(".")
    }

    private data class ParsedMethod(
        val name: String,
        val annotations: Set<String>,
    )

    private data class AnnotationScope(
        val annotations: Set<String>,
    )
}


I found the scanner already has the qualified same-package fixture. I’m adding the complementary imported-superclass fixture so the current false-positive behavior is explicit and intentional.


Input:
*** Begin Patch
*** Update File: /home/Messier82/projects/service-book/app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt
@@
     fun inheritedClassTiersUsePackageQualifiedLookup() {
         val violations =
             RegressionTierScanner.scanSources(
                 mapOf(
@@

         assertThat(violations).isEmpty()
     }

+    @Test
+    fun importedSuperclassTiersAreUnsupported() {
+        val violations =
+            RegressionTierScanner.scanSources(
+                mapOf(
+                    "src/androidTest/java/com/yorvana/shared/Base.kt" to
+                        """
+                        package com.yorvana.shared
+
+                        import com.yorvana.testsupport.tiers.Regression
+
+                        @Regression
+                        open class Base
+                        """.trimIndent(),
+                    "src/androidTest/java/com/yorvana/regression/ImportedChildTest.kt" to
+                        """
+                        package com.yorvana.regression
+
+                        import com.yorvana.shared.Base
+                        import org.junit.Test
+
+                        class ImportedChildTest : Base() {
+                            @Test
+                            fun importedSuperTierIsNotResolved() = Unit
+                        }
+                        """.trimIndent(),
+                ),
+            )
+
+        assertThat(violations.single())
+            .contains("com.yorvana.regression.ImportedChildTest.importedSuperTierIsNotResolved has 0 tier annotations")
+    }
+
     @Test
     fun annotationArgumentsAndUnknownSiblingsAreAccepted() {
         val violations =
             scanFixture(
                 """
@@
-    // class headers, inheritance, and @Test function names must be discoverable on single lines.
-    // Unsupported Kotlin shapes are documented by fixture tests so future phases can either avoid
-    // them under com.yorvana.regression or replace this scanner with a real Kotlin parser.
+    // class headers, same-package inheritance, and @Test function names must be discoverable on
+    // single lines. Unsupported Kotlin shapes are documented by fixture tests so future phases can
+    // either avoid them under com.yorvana.regression or replace this scanner with a real Kotlin parser.
*** End Patch


Success. Updated the following files:
M /home/Messier82/projects/service-book/app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt

@@ -140,2 +140,35 @@
     @Test
+    fun importedSuperclassTiersAreUnsupported() {
+        val violations =
+            RegressionTierScanner.scanSources(
+                mapOf(
+                    "src/androidTest/java/com/yorvana/shared/Base.kt" to
+                        """
+                        package com.yorvana.shared
+
+                        import com.yorvana.testsupport.tiers.Regression
+
+                        @Regression
+                        open class Base
+                        """.trimIndent(),
+                    "src/androidTest/java/com/yorvana/regression/ImportedChildTest.kt" to
+                        """
+                        package com.yorvana.regression
+
+                        import com.yorvana.shared.Base
+                        import org.junit.Test
+
+                        class ImportedChildTest : Base() {
+                            @Test
+                            fun importedSuperTierIsNotResolved() = Unit
+                        }
+                        """.trimIndent(),
+                ),
+            )
+
+        assertThat(violations.single())
+            .contains("com.yorvana.regression.ImportedChildTest.importedSuperTierIsNotResolved has 0 tier annotations")
+    }
+
+    @Test
     fun annotationArgumentsAndUnknownSiblingsAreAccepted() {
@@ -192,5 +225,5 @@
     // Source-level guard for simple regression-test shapes only: package declarations, annotations,
-    // class headers, inheritance, and @Test function names must be discoverable on single lines.
-    // Unsupported Kotlin shapes are documented by fixture tests so future phases can either avoid
-    // them under com.yorvana.regression or replace this scanner with a real Kotlin parser.
+    // class headers, same-package inheritance, and @Test function names must be discoverable on
+    // single lines. Unsupported Kotlin shapes are documented by fixture tests so future phases can
+    // either avoid them under com.yorvana.regression or replace this scanner with a real Kotlin parser.
     fun scanDirectory(root: File): List<String> {


Output:
Exit code: 0
Wall time: 0.9 seconds
Output:
Success. Updated the following files:
M /home/Messier82/projects/service-book/app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt


./gradlew testDebugUnitTest --tests com.yorvana.testsupport.RegressionTierScannerTest


git diff -- app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt


Output:
Chunk ID: cde745
Wall time: 1.0008 seconds
Process running with session ID 23386
Original token count: 35
Output:
Picked up JAVA_TOOL_OPTIONS: -Xlog:os+container=off
Starting a Gradle Daemon, 2 busy Daemons could not be reused, use --status for details


Output:
Chunk ID: f60262
Wall time: 0.1326 seconds
Process exited with code 0
Original token count: 682
Output:
diff --git a/app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt b/app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt
index e1a91e2..7ec5542 100644
--- a/app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt
+++ b/app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt
@@ -137,6 +137,39 @@ class RegressionTierScannerTest {
         assertThat(violations).isEmpty()
     }

+    @Test
+    fun importedSuperclassTiersAreUnsupported() {
+        val violations =
+            RegressionTierScanner.scanSources(
+                mapOf(
+                    "src/androidTest/java/com/yorvana/shared/Base.kt" to
+                        """
+                        package com.yorvana.shared
+
+                        import com.yorvana.testsupport.tiers.Regression
+
+                        @Regression
+                        open class Base
+                        """.trimIndent(),
+                    "src/androidTest/java/com/yorvana/regression/ImportedChildTest.kt" to
+                        """
+                        package com.yorvana.regression
+
+                        import com.yorvana.shared.Base
+                        import org.junit.Test
+
+                        class ImportedChildTest : Base() {
+                            @Test
+                            fun importedSuperTierIsNotResolved() = Unit
+                        }
+                        """.trimIndent(),
+                ),
+            )
+
+        assertThat(violations.single())
+            .contains("com.yorvana.regression.ImportedChildTest.importedSuperTierIsNotResolved has 0 tier annotations")
+    }
+
     @Test
     fun annotationArgumentsAndUnknownSiblingsAreAccepted() {
         val violations =
@@ -190,9 +223,9 @@ private object RegressionTierScanner {
     private const val QUARANTINE_NAME = "Quarantined"

     // Source-level guard for simple regression-test shapes only: package declarations, annotations,
-    // class headers, inheritance, and @Test function names must be discoverable on single lines.
-    // Unsupported Kotlin shapes are documented by fixture tests so future phases can either avoid
-    // them under com.yorvana.regression or replace this scanner with a real Kotlin parser.
+    // class headers, same-package inheritance, and @Test function names must be discoverable on
+    // single lines. Unsupported Kotlin shapes are documented by fixture tests so future phases can
+    // either avoid them under com.yorvana.regression or replace this scanner with a real Kotlin parser.
     fun scanDirectory(root: File): List<String> {
         if (!root.exists()) return emptyList()
         val sources =


The patch is narrow: one new test fixture plus a scanner comment tweak. The focused Gradle test is running now.


Output:
Chunk ID: e3722b
Wall time: 5.0004 seconds
Process running with session ID 23386
Original token count: 32
Output:

> Configure project :app
WARNING: The option setting 'android.testoptions.manageddevices.emulator.gpu=host' is experimental.


Output:
Chunk ID: df45c9
Wall time: 5.0005 seconds
Process running with session ID 23386
Original token count: 377
Output:

> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:generateDebugBuildConfig UP-TO-DATE
> Task :app:generateDebugResources UP-TO-DATE
> Task :app:packageDebugResources UP-TO-DATE
> Task :app:processDebugNavigationResources UP-TO-DATE
> Task :app:parseDebugLocalResources UP-TO-DATE
> Task :app:generateDebugRFile UP-TO-DATE
> Task :app:compileDebugKotlin UP-TO-DATE
> Task :app:javaPreCompileDebug UP-TO-DATE
> Task :app:compileDebugJavaWithJavac UP-TO-DATE
> Task :app:checkDebugAarMetadata UP-TO-DATE
> Task :app:mapDebugSourceSetPaths UP-TO-DATE
> Task :app:compileDebugNavigationResources UP-TO-DATE
> Task :app:mergeDebugResources UP-TO-DATE
> Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
> Task :app:extractDeepLinksDebug UP-TO-DATE
> Task :app:processDebugMainManifest UP-TO-DATE
> Task :app:processDebugManifest UP-TO-DATE
> Task :app:processDebugManifestForPackage UP-TO-DATE
> Task :app:processDebugResources UP-TO-DATE
> Task :app:bundleDebugClassesToRuntimeJar UP-TO-DATE
> Task :app:bundleDebugClassesToCompileJar UP-TO-DATE
> Task :app:preDebugUnitTestBuild UP-TO-DATE
> Task :app:javaPreCompileDebugUnitTest UP-TO-DATE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:mergeDebugAssets UP-TO-DATE
> Task :app:packageDebugUnitTestForUnitTest UP-TO-DATE
> Task :app:processDebugUnitTestManifest UP-TO-DATE
> Task :app:generateDebugUnitTestConfig UP-TO-DATE
> Task :app:processDebugJavaRes UP-TO-DATE
> Task :app:restoreOutputDirRoborazzi SKIPPED


Output:
Chunk ID: 21dde7
Wall time: 5.0013 seconds
Process running with session ID 23386
Original token count: 37
Output:
> Task :app:compileDebugUnitTestKotlin
> Task :app:compileDebugUnitTestJavaWithJavac NO-SOURCE
> Task :app:processDebugUnitTestJavaRes UP-TO-DATE


Output:
Chunk ID: e3e2e4
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 216
Output:

> Task :app:testDebugUnitTest
Picked up JAVA_TOOL_OPTIONS: -Xlog:os+container=off

> Task :app:finalizeTestRoborazziDebug SKIPPED

[Incubating] Problems report is available at: file:///home/Messier82/projects/service-book/build/reports/problems/problems-report.html

Deprecated Gradle features were used in this build, making it incompatible with Gradle 10.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/9.5.1/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD SUCCESSFUL in 24s
30 actionable tasks: 2 executed, 28 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.5.1/userguide/configuration_cache_enabling.html


The focused scanner test passes. I’m running the project quality gate next, since this touches test Kotlin and the branch has been using that as the PR verification bar.


Output:
Chunk ID: ffcb29
Wall time: 1.0016 seconds
Process running with session ID 22495
Original token count: 13
Output:
Picked up JAVA_TOOL_OPTIONS: -Xlog:os+container=off


Output:
Chunk ID: 0814ab
Wall time: 5.0016 seconds
Process running with session ID 22495
Original token count: 511
Output:

> Configure project :app
WARNING: The option setting 'android.testoptions.manageddevices.emulator.gpu=host' is experimental.

> Task :app:loadKtlintReporters UP-TO-DATE
> Task :app:runKtlintCheckOverAndroidTestDebugSourceSet NO-SOURCE
> Task :app:ktlintAndroidTestDebugSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverAndroidTestReleaseSourceSet NO-SOURCE
> Task :app:ktlintAndroidTestReleaseSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverAndroidTestSourceSet UP-TO-DATE
> Task :app:ktlintAndroidTestSourceSetCheck UP-TO-DATE
> Task :app:runKtlintCheckOverDebugSourceSet NO-SOURCE
> Task :app:ktlintDebugSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverKotlinScripts UP-TO-DATE
> Task :app:ktlintKotlinScriptCheck UP-TO-DATE
> Task :app:runKtlintCheckOverMainSourceSet UP-TO-DATE
> Task :app:ktlintMainSourceSetCheck UP-TO-DATE
> Task :app:runKtlintCheckOverReleaseSourceSet NO-SOURCE
> Task :app:ktlintReleaseSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverTestDebugSourceSet NO-SOURCE
> Task :app:ktlintTestDebugSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverTestFixturesDebugSourceSet NO-SOURCE
> Task :app:ktlintTestFixturesDebugSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverTestFixturesReleaseSourceSet NO-SOURCE
> Task :app:ktlintTestFixturesReleaseSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverTestFixturesSourceSet NO-SOURCE
> Task :app:ktlintTestFixturesSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverTestReleaseSourceSet NO-SOURCE
> Task :app:ktlintTestReleaseSourceSetCheck SKIPPED

> Task :app:runKtlintCheckOverTestSourceSet
[0.004s][warning][os,container] Cgroup memory controller path at '/sys/fs/cgroup' seems to have moved to '/../../../app.slice/app-tmux.slice/tmux-spawn-48989792-44f6-435b-b502-206716986e06.scope', detected limits won't be accurate
[0.004s][warning][os,container] Cgroup cpu controller path at '/sys/fs/cgroup' seems to have moved to '/../../../app.slice/app-tmux.slice/tmux-spawn-48989792-44f6-435b-b502-206716986e06.scope', detected limits won't be accurate


Output:
Chunk ID: 72508a
Wall time: 5.0011 seconds
Process running with session ID 22495
Original token count: 377
Output:

> Task :app:detekt
> Task :app:ktlintTestSourceSetCheck UP-TO-DATE
> Task :app:ktlintCheck UP-TO-DATE
> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:generateDebugBuildConfig UP-TO-DATE
> Task :app:generateDebugResources UP-TO-DATE
> Task :app:packageDebugResources UP-TO-DATE
> Task :app:processDebugNavigationResources UP-TO-DATE
> Task :app:parseDebugLocalResources UP-TO-DATE
> Task :app:generateDebugRFile UP-TO-DATE
> Task :app:compileDebugKotlin UP-TO-DATE
> Task :app:javaPreCompileDebug UP-TO-DATE
> Task :app:compileDebugJavaWithJavac UP-TO-DATE
> Task :app:checkDebugAarMetadata UP-TO-DATE
> Task :app:mapDebugSourceSetPaths UP-TO-DATE
> Task :app:compileDebugNavigationResources UP-TO-DATE
> Task :app:mergeDebugResources UP-TO-DATE
> Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
> Task :app:extractDeepLinksDebug UP-TO-DATE
> Task :app:processDebugMainManifest UP-TO-DATE
> Task :app:processDebugManifest UP-TO-DATE
> Task :app:processDebugManifestForPackage UP-TO-DATE
> Task :app:processDebugResources UP-TO-DATE
> Task :app:bundleDebugClassesToCompileJar UP-TO-DATE
> Task :app:preDebugAndroidTestBuild SKIPPED
> Task :app:generateDebugAndroidTestLintModel UP-TO-DATE
> Task :app:extractProguardFiles UP-TO-DATE
> Task :app:generateDebugLintReportModel UP-TO-DATE
> Task :app:preDebugUnitTestBuild UP-TO-DATE
> Task :app:generateDebugUnitTestLintModel
> Task :app:lintAnalyzeDebug UP-TO-DATE
> Task :app:lintAnalyzeDebugAndroidTest UP-TO-DATE


Output:
Chunk ID: 8aee4b
Wall time: 5.0013 seconds
Process running with session ID 22495
Original token count: 0
Output:


Output:
Chunk ID: 71f902
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 208
Output:
> Task :app:lintAnalyzeDebugUnitTest
> Task :app:lintReportDebug UP-TO-DATE
> Task :app:lintDebug

[Incubating] Problems report is available at: file:///home/Messier82/projects/service-book/build/reports/problems/problems-report.html

Deprecated Gradle features were used in this build, making it incompatible with Gradle 10.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/9.5.1/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD SUCCESSFUL in 47s
39 actionable tasks: 5 executed, 34 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.5.1/userguide/configuration_cache_enabling.html


Output:
Chunk ID: 0df219
Wall time: 0.2197 seconds
Process exited with code 0
Original token count: 0
Output:


Output:
Chunk ID: a17ac0
Wall time: 0.2092 seconds
Process exited with code 0
Original token count: 19
Output:
 M app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt


Quality checks passed. I’m committing this as a small review follow-up and pushing it to the existing PR branch.


Output:
Chunk ID: 195e86
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


git commit -m "Pin regression scanner import limitation"


Output:
Chunk ID: 12a2f7
Wall time: 1.0008 seconds
Process running with session ID 45682
Original token count: 13
Output:
Picked up JAVA_TOOL_OPTIONS: -Xlog:os+container=off


Output:
Chunk ID: caeb20
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1023
Output:

> Configure project :app
WARNING: The option setting 'android.testoptions.manageddevices.emulator.gpu=host' is experimental.

> Task :app:loadKtlintReporters UP-TO-DATE
> Task :app:runKtlintCheckOverAndroidTestDebugSourceSet NO-SOURCE
> Task :app:ktlintAndroidTestDebugSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverAndroidTestReleaseSourceSet NO-SOURCE
> Task :app:ktlintAndroidTestReleaseSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverAndroidTestSourceSet UP-TO-DATE
> Task :app:ktlintAndroidTestSourceSetCheck UP-TO-DATE
> Task :app:runKtlintCheckOverDebugSourceSet NO-SOURCE
> Task :app:ktlintDebugSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverKotlinScripts UP-TO-DATE
> Task :app:ktlintKotlinScriptCheck UP-TO-DATE
> Task :app:runKtlintCheckOverMainSourceSet UP-TO-DATE
> Task :app:ktlintMainSourceSetCheck UP-TO-DATE
> Task :app:runKtlintCheckOverReleaseSourceSet NO-SOURCE
> Task :app:ktlintReleaseSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverTestDebugSourceSet NO-SOURCE
> Task :app:ktlintTestDebugSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverTestFixturesDebugSourceSet NO-SOURCE
> Task :app:ktlintTestFixturesDebugSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverTestFixturesReleaseSourceSet NO-SOURCE
> Task :app:ktlintTestFixturesReleaseSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverTestFixturesSourceSet NO-SOURCE
> Task :app:ktlintTestFixturesSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverTestReleaseSourceSet NO-SOURCE
> Task :app:ktlintTestReleaseSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverTestSourceSet UP-TO-DATE
> Task :app:ktlintTestSourceSetCheck UP-TO-DATE
> Task :app:ktlintCheck UP-TO-DATE
> Task :app:detekt UP-TO-DATE
> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:generateDebugBuildConfig UP-TO-DATE
> Task :app:generateDebugResources UP-TO-DATE
> Task :app:packageDebugResources UP-TO-DATE
> Task :app:processDebugNavigationResources UP-TO-DATE
> Task :app:parseDebugLocalResources UP-TO-DATE
> Task :app:generateDebugRFile UP-TO-DATE
> Task :app:compileDebugKotlin UP-TO-DATE
> Task :app:javaPreCompileDebug UP-TO-DATE
> Task :app:compileDebugJavaWithJavac UP-TO-DATE
> Task :app:checkDebugAarMetadata UP-TO-DATE
> Task :app:mapDebugSourceSetPaths UP-TO-DATE
> Task :app:compileDebugNavigationResources UP-TO-DATE
> Task :app:mergeDebugResources UP-TO-DATE
> Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
> Task :app:extractDeepLinksDebug UP-TO-DATE
> Task :app:processDebugMainManifest UP-TO-DATE
> Task :app:processDebugManifest UP-TO-DATE
> Task :app:processDebugManifestForPackage UP-TO-DATE
> Task :app:processDebugResources UP-TO-DATE
> Task :app:bundleDebugClassesToCompileJar UP-TO-DATE
> Task :app:preDebugAndroidTestBuild SKIPPED
> Task :app:generateDebugAndroidTestLintModel UP-TO-DATE
> Task :app:extractProguardFiles UP-TO-DATE
> Task :app:generateDebugLintReportModel UP-TO-DATE
> Task :app:preDebugUnitTestBuild UP-TO-DATE
> Task :app:generateDebugUnitTestLintModel UP-TO-DATE
> Task :app:lintAnalyzeDebug UP-TO-DATE
> Task :app:lintAnalyzeDebugAndroidTest UP-TO-DATE
> Task :app:lintAnalyzeDebugUnitTest UP-TO-DATE
> Task :app:lintReportDebug UP-TO-DATE
> Task :app:lintDebug

[Incubating] Problems report is available at: file:///home/Messier82/projects/service-book/build/reports/problems/problems-report.html

Deprecated Gradle features were used in this build, making it incompatible with Gradle 10.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/9.5.1/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD SUCCESSFUL in 2s
39 actionable tasks: 1 executed, 38 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.5.1/userguide/configuration_cache_enabling.html
[issue-114-regression-infra efc9cf1] Pin regression scanner import limitation
 1 file changed, 36 insertions(+), 3 deletions(-)


Output:
Chunk ID: 6e62de
Wall time: 1.0010 seconds
Process running with session ID 26976
Original token count: 0
Output:


Output:
Chunk ID: e47e57
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 28
Output:
To github.com:yorvana/android.git
   25d1844..efc9cf1  issue-114-regression-infra -> issue-114-regression-infra


gh pr view 123 --json url,headRefName,baseRefName,statusCheckRollup


Output:
Chunk ID: 7e76fa
Wall time: 0.1245 seconds
Process exited with code 0
Original token count: 17
Output:
## issue-114-regression-infra...origin/issue-114-regression-infra


Output:
Chunk ID: 4f02c6
Wall time: 0.5664 seconds
Process exited with code 0
Original token count: 173
Output:
{"baseRefName":"master","headRefName":"issue-114-regression-infra","statusCheckRollup":[{"__typename":"CheckRun","completedAt":"0001-01-01T00:00:00Z","conclusion":"","detailsUrl":"https://github.com/yorvana/android/actions/runs/26324200434/job/77498647952","name":"Semgrep Scan","startedAt":"2026-05-23T05:06:55Z","status":"QUEUED","workflowName":"Semgrep"},{"__typename":"CheckRun","completedAt":"2026-05-23T05:06:55Z","conclusion":"SKIPPED","detailsUrl":"https://github.com/yorvana/android/actions/runs/26324200435/job/77498648248","name":"ci","startedAt":"2026-05-23T05:06:55Z","status":"COMPLETED","workflowName":"CI (Manual Label)"}],"url":"https://github.com/yorvana/android/pull/123"}


Implemented the latest review comment and pushed it to PR #123.

Change: added `importedSuperclassTiersAreUnsupported()` in `RegressionTierScannerTest` to pin the scanner’s current limitation around imported supertypes, and clarified the scanner comment to say inheritance support is same-package only.

Commit pushed: `efc9cf1 Pin regression scanner import limitation`

Verified:
- `./gradlew testDebugUnitTest --tests com.yorvana.testsupport.RegressionTierScannerTest`
- `./gradlew ktlintCheck detekt lintDebug`
- `git diff --check`

Branch is clean and pushed. PR checks restarted; Semgrep is currently queued, CI label workflow is skipped as before.


https://github.com/yorvana/android/issues/114    Implement this issue

Implement the plan.

Create a PR from latest maste4

Theres a review comment

Implement the plan.

Theres a review comment

Theres a review comment


<permissions instructions>
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval. Network access is restricted.
# Escalation Requests

Commands are run outside the sandbox if they are approved by the user, or match an existing rule that allows it to run unrestricted. The command string is split into independent command segments at shell control operators, including but not limited to:

- Pipes: |
- Logical operators: &&, ||
- Command separators: ;
- Subshell boundaries: (...), $(...)

Each resulting segment is evaluated independently for sandbox restrictions and approval requirements.

Example:

git pull | tee output.txt

This is treated as two command segments:

["git", "pull"]

["tee", "output.txt"]

Commands that use more advanced shell features like redirection (>, >>, <), substitutions ($(...), ...), environment variables (FOO=bar), or wildcard patterns (*, ?) will not be evaluated against rules, to limit the scope of what an approved rule allows.

## How to request escalation

IMPORTANT: To request approval to execute a command that will require escalated privileges:

- Provide the `sandbox_permissions` parameter with the value `"require_escalated"`
- Include a short question asking the user if they want to allow the action in `justification` parameter. e.g. "Do you want to download and install dependencies for this project?"
- Optionally suggest a `prefix_rule` - this will be shown to the user with an option to persist the rule approval for future sessions.

If you run a command that is important to solving the user's query, but it fails because of sandboxing or with a likely sandbox-related network error (for example DNS/host resolution, registry/index access, or dependency download failure), rerun the command with "require_escalated". ALWAYS proceed to use the `justification` parameter - do not message the user before requesting approval for the command.

## When to request escalation

While commands are running inside the sandbox, here are some scenarios that will require escalation outside the sandbox:

- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /var)
- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files.
- If you run a command that is important to solving the user's query, but it fails because of sandboxing or with a likely sandbox-related network error (for example DNS/host resolution, registry/index access, or dependency download failure), rerun the command with `require_escalated`. ALWAYS proceed to use the `sandbox_permissions` and `justification` parameters. do not message the user before requesting approval for the command.
- You are about to take a potentially destructive action such as an `rm` or `git reset` that the user did not explicitly ask for.
- Be judicious with escalating, but if completing the user's request requires it, you should do so - don't try and circumvent approvals by using other tools.

## prefix_rule guidance

When choosing a `prefix_rule`, request one that will allow you to fulfill similar requests from the user in the future without re-requesting escalation. It should be categorical and reasonably scoped to similar capabilities. You should rarely pass the entire command into `prefix_rule`.

### Banned prefix_rules
Avoid requesting overly broad prefixes that the user would be ill-advised to approve. For example, do not request ["python3"], ["python", "-"], or other similar prefixes that would allow arbitrary scripting.
NEVER provide a prefix_rule argument for destructive commands like rm.
NEVER provide a prefix_rule if your command uses a heredoc or herestring.

### Examples
Good examples of prefixes:
- ["npm", "run", "dev"]
- ["gh", "pr", "check"]
- ["cargo", "test"]

## Approved command prefixes
The following prefix rules have already been approved: - ["curl"]
- ["./gradlew"]
- ["git", "add"]
- ["git", "push"]
- ["ss", "-ltnp"]
- ["git", "fetch"]
- ["git", "commit"]
- ["git", "rebase"]
- ["git", "switch"]
- ["hostname", "-I"]
- ["bundle", "install"]
- ["rm", "/home/Messier82/projects/site/assets/screenshots/vehicle-history-history-dark.png"]
- ["gh", "pr", "diff"]
- ["gh", "pr", "list"]
- ["gh", "pr", "view"]
- ["gh", "pr", "merge"]
- ["gh", "pr", "create"]
- ["gh", "issue", "list"]
- ["gh", "issue", "view"]
- ["gh", "pr", "comment"]
- ["gh", "api", "graphql"]
- ["gh", "pr", "checkout"]
- ["gh", "issue", "comment"]
- ["sudo", "dnf", "install"]
- ["bundle", "exec", "jekyll"]
- ["git", "push", "--force-with-lease"]
- ["gh", "api", "repos/yorvana/site/pulls/1/comments"]
- ["gh", "api", "repos/yorvana/android/pulls/102/comments"]
- ["gh", "api", "repos/yorvana/android/pulls/104/comments"]
- ["pkill", "-f", "jekyll serve --host 0.0.0.0 --port 4000 --no-watch"]
- ["pkill", "-f", "jekyll serve --host 127.0.0.1 --port 4000 --no-watch"]
- ["gh", "pr", "view", "97", "--json", "number,title,headRefName,baseRefName,files,mergeStateStatus,isDraft,statusCheckRollup"]
 The writable roots are `/home/Messier82/.codex/memories`, `/home/Messier82/projects/service-book`, `/tmp`.
</permissions instructions>

<collaboration_mode># Collaboration Mode: Default

You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active.

Your active mode changes only when new developer instructions with a different `<collaboration_mode>...</collaboration_mode>` change it; user requests or tool descriptions do not change mode by themselves. Known mode names are Default and Plan.

## request_user_input availability

Use the `request_user_input` tool only when it is listed in the available tools for this turn.

In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message.
</collaboration_mode>

<apps_instructions>
## Apps (Connectors)
Apps (Connectors) can be explicitly triggered in user messages in the format `[$app-name](app://{connector_id})`. Apps can also be implicitly triggered as long as the context suggests usage of available apps.
An app is equivalent to a set of MCP tools within the `codex_apps` MCP.
An installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool_search` tool. If `tool_search` is available, the apps that are searchable by `tools_search` will be listed by it.
Do not additionally call list_mcp_resources or list_mcp_resource_templates for apps.
</apps_instructions>

<skills_instructions>
## Skills
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.
### Available skills
- imagegen: Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas. (file: /home/Messier82/.codex/skills/.system/imagegen/SKILL.md)
- openai-docs: Use when the user asks how to build with OpenAI products or APIs and needs up-to-date official documentation with citations, help choosing the latest model for a use case, or model upgrade and prompt-upgrade guidance; prioritize OpenAI docs MCP tools, use bundled references only as helper context, and restrict any fallback browsing to official OpenAI domains. (file: /home/Messier82/.codex/skills/.system/openai-docs/SKILL.md)
- plugin-creator: Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, or generate or update marketplace entries for plugin ordering and availability metadata. (file: /home/Messier82/.codex/skills/.system/plugin-creator/SKILL.md)
- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: /home/Messier82/.codex/skills/.system/skill-creator/SKILL.md)
- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: /home/Messier82/.codex/skills/.system/skill-installer/SKILL.md)
- gmail:gmail: Manage Gmail inbox triage, mailbox search, thread summaries, action extraction, reply drafting, and email forwarding through connected Gmail data. Use when the user wants to inspect a mailbox or thread, search email with Gmail query syntax, summarize messages, extract decisions and follow-ups, prepare replies or forwarded messages, or organize messages with explicit confirmation before send, archive, delete, or label actions. (file: /home/Messier82/.codex/plugins/cache/openai-curated/gmail/0d4f5414/skills/gmail/SKILL.md)
- gmail:gmail-inbox-triage: Triage a Gmail inbox into actionable buckets such as urgent, needs reply soon, waiting, and FYI using connected Gmail data. Use when the user asks to triage the inbox, rank what needs attention, find what still needs a reply, or separate important mail from noise. (file: /home/Messier82/.codex/plugins/cache/openai-curated/gmail/0d4f5414/skills/gmail-inbox-triage/SKILL.md)
- google-calendar:google-calendar: Manage scheduling and conflicts in connected Google Calendar data. Use when the user wants to inspect calendars, compare availability, review conflicts, find a meeting room, review event notes or attachments, add or adjust reminders, place temporary holds, or draft exact create, update, reschedule, or cancel changes with timezone-aware details. (file: /home/Messier82/.codex/plugins/cache/openai-curated/google-calendar/0d4f5414/skills/google-calendar/SKILL.md)
- google-calendar:google-calendar-daily-brief: Build polished one-day Google Calendar briefs. Use when the user asks for today, tomorrow, or a specific date summary with an agenda, conflict flags, free windows, remaining-meeting readouts, or a calendar brief, and the Google Calendar connector is available. (file: /home/Messier82/.codex/plugins/cache/openai-curated/google-calendar/0d4f5414/skills/google-calendar-daily-brief/SKILL.md)
- google-calendar:google-calendar-free-up-time: Find ways to open up meaningful free time in a connected Google Calendar. Use when the user wants to clear up their day, make room for focus time, create a longer uninterrupted block, or see the smallest set of calendar changes that would give time back. (file: /home/Messier82/.codex/plugins/cache/openai-curated/google-calendar/0d4f5414/skills/google-calendar-free-up-time/SKILL.md)
- google-calendar:google-calendar-group-scheduler: Find and rank good meeting times for multiple people using connected Google Calendar data. Use when the user wants to schedule a group meeting, compare candidate slots across several attendees, find the best compromise time, or add a room check after narrowing the attendee-compatible options. (file: /home/Messier82/.codex/plugins/cache/openai-curated/google-calendar/0d4f5414/skills/google-calendar-group-scheduler/SKILL.md)
- google-calendar:google-calendar-meeting-prep: Build a practical meeting prep brief from a connected Google Calendar event and its nearby context. Use when the user wants to prepare for an upcoming meeting, understand what to read beforehand, pull in linked notes or docs, or get a concise brief on what the meeting appears to require. (file: /home/Messier82/.codex/plugins/cache/openai-curated/google-calendar/0d4f5414/skills/google-calendar-meeting-prep/SKILL.md)
### How to use skills
- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.
- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
- How to use a skill (progressive disclosure):
  1) After deciding to use a skill, open its `SKILL.md`. Read only enough to follow the workflow.
  2) When `SKILL.md` references relative paths (e.g., `scripts/foo.py`), resolve them relative to the skill directory listed above first, and only consider other paths if needed.
  3) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.
  4) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.
  5) If `assets/` or templates exist, reuse them instead of recreating from scratch.
- Coordination and sequencing:
  - If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
  - Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
- Context hygiene:
  - Keep context small: summarize long sections instead of pasting them; only load extra files when needed.
  - Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
  - When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.
</skills_instructions>

<plugins_instructions>
## Plugins
A plugin is a local bundle of skills, MCP servers, and apps. Below is the list of plugins that are enabled and available in this session.
### Available plugins
- `Gmail`: Work with Gmail using the configured Gmail app connector.
- `Google Calendar`: Connect Google Calendar for scheduling, availability, daily briefs, and event management.
### How to use plugins
- Discovery: The list above is the plugins available in this session.
- Skill naming: If a plugin contributes skills, those skill entries are prefixed with `plugin_name:` in the Skills list.
- Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn.
- Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task.
- Preference: When a relevant plugin is available, prefer using capabilities associated with that plugin over standalone capabilities that provide similar functionality.
- Missing/blocked: If the user requests a plugin that is not listed above, or the plugin does not have relevant callable capabilities for the task, say so briefly and continue with the best fallback.
</plugins_instructions>


# AGENTS.md instructions for /home/Messier82/projects/service-book

<INSTRUCTIONS>
# Repository Guidelines

## Project Structure & Module Organization

Yorvana is a single-module Android app. Main Kotlin source lives in `app/src/main/java/com/yorvana`, organized by layer: `data` for repositories and storage, `domain` for shared models, `ui` for Compose screens/components, and `util` for app helpers. Android resources are in `app/src/main/res`. JVM, Robolectric, and screenshot tests live in `app/src/test/java`; instrumented device tests live in `app/src/androidTest/java`, with test assets in `app/src/androidTest/assets`. Project docs are in `docs/`, build configuration is in `build.gradle.kts`, `app/build.gradle.kts`, `gradle/libs.versions.toml`, and quality rules are in `config/detekt/`.

## Build, Test, and Development Commands

- `./gradlew assembleDebug`: build a debug APK.
- `./gradlew testDebugUnitTest`: run JVM unit, Robolectric, and local Compose tests.
- `./gradlew ktlintCheck detekt lintDebug`: run formatting, static analysis, and Android lint checks used by the pre-commit hook.
- `./gradlew verifyRoborazziDebug`: verify screenshot baselines.
- `./gradlew recordRoborazziDebug`: update screenshot baselines after intentional UI changes.
- `./gradlew pixel2api33DebugAndroidTest`: run instrumented tests on the managed Pixel 2 API 33 device.
- `./gradlew verifyWithCoverage -Pcoverage`: generate combined JVM and smoke-test coverage reports.

## Coding Style & Naming Conventions

Use Kotlin with Java 17 targets and Jetpack Compose Material 3. Follow ktlint defaults and the detekt rules in `config/detekt/detekt.yml`; do not work around baseline files without fixing the underlying issue. Keep package paths under `com.yorvana`. Name Compose screen files `FeatureScreen.kt`, ViewModels `FeatureViewModel.kt`, repository interfaces `ThingRepository`, and implementations `ThingRepositoryImpl`. Prefer immutable UI state exposed through `StateFlow`.

## Testing Guidelines

Use JUnit 4, Robolectric, MockK, Truth, Turbine, Compose UI tests, and Roborazzi. Name fast JVM tests `*Test.kt`, local integration tests `*IT.kt`, and screenshot tests `*ScreenshotTest.kt`. Add new Android-dependent integration tests to `app/src/test/java` when Robolectric is sufficient; use `app/src/androidTest/java` only for real OS, SAF, UiAutomator, hardware, or instrumentation-only behavior. Refresh `app/coverage-baselines/gmd_smoke.ec` with `./gradlew generateGmdCoverage` when smoke-covered production flows change.

## Commit & Pull Request Guidelines

Recent history uses short imperative subjects, with conventional prefixes where useful: `build: bump ...`, `docs: add ...`, or plain feature summaries such as `Add contact option to settings (#102)`. Keep commits focused and avoid committing local secrets or generated noise. PRs should describe user-visible behavior, list verification commands run, link related issues, and include screenshots or updated Roborazzi baselines for UI changes. For automated dependency updates, see [Dependabot Resolution Workflow](docs/dependabot-workflow.md).

## Security & Configuration Tips

Do not commit `local.properties` or real signing credentials. Use `app/keystore.properties.template` for release signing setup and local `sentry.dsn.*` values for Sentry configuration. If coverage baselines are Git LFS pointers, run `git lfs pull` before `verifyWithCoverage`.

</INSTRUCTIONS>

<environment_context>
  <cwd>/home/Messier82/projects/service-book</cwd>
  <shell>bash</shell>
  <current_date>2026-05-23</current_date>
  <timezone>Etc/UTC</timezone>
</environment_context>


# Collaboration Mode: Default

You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active.

Your active mode changes only when new developer instructions with a different `<collaboration_mode>...</collaboration_mode>` change it; user requests or tool descriptions do not change mode by themselves. Known mode names are Default and Plan.

## request_user_input availability

Use the `request_user_input` tool only when it is listed in the available tools for this turn.

In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message.

# Repository Guidelines

## Project Structure & Module Organization

Yorvana is a single-module Android app. Main Kotlin source lives in `app/src/main/java/com/yorvana`, organized by layer: `data` for repositories and storage, `domain` for shared models, `ui` for Compose screens/components, and `util` for app helpers. Android resources are in `app/src/main/res`. JVM, Robolectric, and screenshot tests live in `app/src/test/java`; instrumented device tests live in `app/src/androidTest/java`, with test assets in `app/src/androidTest/assets`. Project docs are in `docs/`, build configuration is in `build.gradle.kts`, `app/build.gradle.kts`, `gradle/libs.versions.toml`, and quality rules are in `config/detekt/`.

## Build, Test, and Development Commands

- `./gradlew assembleDebug`: build a debug APK.
- `./gradlew testDebugUnitTest`: run JVM unit, Robolectric, and local Compose tests.
- `./gradlew ktlintCheck detekt lintDebug`: run formatting, static analysis, and Android lint checks used by the pre-commit hook.
- `./gradlew verifyRoborazziDebug`: verify screenshot baselines.
- `./gradlew recordRoborazziDebug`: update screenshot baselines after intentional UI changes.
- `./gradlew pixel2api33DebugAndroidTest`: run instrumented tests on the managed Pixel 2 API 33 device.
- `./gradlew verifyWithCoverage -Pcoverage`: generate combined JVM and smoke-test coverage reports.

## Coding Style & Naming Conventions

Use Kotlin with Java 17 targets and Jetpack Compose Material 3. Follow ktlint defaults and the detekt rules in `config/detekt/detekt.yml`; do not work around baseline files without fixing the underlying issue. Keep package paths under `com.yorvana`. Name Compose screen files `FeatureScreen.kt`, ViewModels `FeatureViewModel.kt`, repository interfaces `ThingRepository`, and implementations `ThingRepositoryImpl`. Prefer immutable UI state exposed through `StateFlow`.

## Testing Guidelines

Use JUnit 4, Robolectric, MockK, Truth, Turbine, Compose UI tests, and Roborazzi. Name fast JVM tests `*Test.kt`, local integration tests `*IT.kt`, and screenshot tests `*ScreenshotTest.kt`. Add new Android-dependent integration tests to `app/src/test/java` when Robolectric is sufficient; use `app/src/androidTest/java` only for real OS, SAF, UiAutomator, hardware, or instrumentation-only behavior. Refresh `app/coverage-baselines/gmd_smoke.ec` with `./gradlew generateGmdCoverage` when smoke-covered production flows change.

## Commit & Pull Request Guidelines

Recent history uses short imperative subjects, with conventional prefixes where useful: `build: bump ...`, `docs: add ...`, or plain feature summaries such as `Add contact option to settings (#102)`. Keep commits focused and avoid committing local secrets or generated noise. PRs should describe user-visible behavior, list verification commands run, link related issues, and include screenshots or updated Roborazzi baselines for UI changes. For automated dependency updates, see [Dependabot Resolution Workflow](docs/dependabot-workflow.md).

## Security & Configuration Tips

Do not commit `local.properties` or real signing credentials. Use `app/keystore.properties.template` for release signing setup and local `sentry.dsn.*` values for Sentry configuration. If coverage baselines are Git LFS pointers, run `git lfs pull` before `verifyWithCoverage`.


Theres a review comment


I’ll pull the current PR review state first, then patch whatever the latest comment is calling out and push back to the existing branch.


gh pr view 123 --json url,headRefName,baseRefName,comments,reviews,statusCheckRollup


Output:
Chunk ID: 16817e
Wall time: 0.1273 seconds
Process exited with code 0
Original token count: 17
Output:
## issue-114-regression-infra...origin/issue-114-regression-infra


Output:
Chunk ID: 4a82bf
Wall time: 0.7286 seconds
Process exited with code 0
Original token count: 4218
Output:
{"baseRefName":"master","comments":[{"id":"IC_kwDOR2Hmq88AAAABDWGo2Q","author":{"login":"Mesya82"},"authorAssociation":"MEMBER","body":"## Code review\n\n### Overview\nPhase 1 of the regression scaffold: five JUnit marker annotations (`Smoke`, `Regression`, `RegressionFull`, `BillingSandbox`, `Quarantined`), four Gradle aggregates (`regressionCheck`, `regressionFullNightlyCoreCheck`, `regressionFullExtendedCheck`, `regressionFullCheck`) implemented as `GradleBuild` child invocations against `pixel2api33DebugAndroidTest`, a workflow pair (`regression.yml` + `regression-label.yml`) mirroring smoke, `androidTest` helpers (`Permissions`, `Camera`, `SystemPicker`, `FixtureAssetsRule`) + a minimal `test_receipt.pdf`, and a JVM `RegressionTierScannerTest` that source-scans `regression/` for tier-annotation invariants. `SmokeTest` gains `@Smoke`; the smoke-device runner-arg now applies `annotation = Smoke` unless an explicit annotation is passed.\n\n### Correctness\n\n**1. Nightly schedule runs the non-quarantined regression set twice.** `regression.yml` invokes `regressionCheck regressionFullCheck`, and `regressionFullCheck` already depends on `regressionFullNightlyCoreCheck` whose `annotation=Regression` filter is a superset of `regressionCheck`'s `Regression && !Quarantined`. Because each `GradleBuild` is a fresh nested invocation, this is real wall-clock + CI-minute waste — not a free fail-fast (the child build re-configures from scratch). Either drop `regressionCheck` from the schedule line, or keep it and comment the fail-fast intent.\n\n**2. `isExplicitAnnotationRun` is captured eagerly at outer-build configuration time** (`app/build.gradle.kts:222`). It's a `Boolean`, not a `Provider<Boolean>`, so `providers.gradleProperty(...).isPresent` resolves when outer `afterEvaluate` runs. Fine for child `GradleBuild` invocations (they configure fresh) and fine for direct `-P` overrides. Worth a one-line note so the next reader doesn't try to switch it to a provider chain.\n\n**3. `gradle.startParameter.projectProperties + runnerArguments.mapKeys{…}`** (build.gradle.kts:282) makes the hard-coded annotation/notAnnotation filters win over any outer `-P…annotation=Foo`. Almost certainly the intent — but silent override is a footgun for anyone trying to narrow a run from the CLI. A doc-comment on the task descriptions would suffice.\n\n### Conventions\n\n**4. `regressionFullNightlyCoreCheck` bakes a CI cadence into a build-graph identifier.** What it *is* is \"the core regression tier, including quarantined\". A consumer who runs it manually from `gradlew tasks` will be misled. Suggest `regressionFullCoreCheck`; let the workflow file own the \"nightly\" framing.\n\n**5. `RegressionTierScanner` parses Kotlin with line-oriented regex.** It handles the present fixtures, but several common shapes will trip it: multi-line class headers, primary-constructor parameter lists that span lines, generic type parameters before the supertype, nested classes (`pendingAnnotations` resets on inner-class declarations and leaks across siblings since there's no brace tracking). Phase 1 has zero files under `regression/`, so the production scan is a no-op today — but when Phase 2 lands real test classes the scanner will silently miss violations rather than fail loudly. Two cheap mitigations: (a) header comment naming the supported shapes; (b) one fixture that asserts an unsupported shape currently *passes*, so the limit is documented in tests rather than folklore.\n\n### Workflows\n- `regression.yml` carrying `workflow_dispatch` + `workflow_call` + `schedule` in one file is fine; the `schedule` only fires from `master`, consistent with GitHub's behavior.\n- `regression-label.yml` and `smoke-label.yml` are byte-for-byte structural twins. Acceptable.\n\n### Tests / fixtures\n- `RegressionTierScannerTest` covers the four main violation paths (zero-tier, two-tier, orphan-quarantine, inherited tier) and the forward-compat annotation-argument case. Good coverage of the parser's actual contract.\n- `FixtureAssetsRule.copyToCache` re-appends to `cacheFiles` if called twice with the same asset; cleanup is idempotent, so harmless.\n- `Permissions.acceptIfShown` worst-case is 4 × 2s; `Camera.captureAndAccept` is up to 5 × 10s. Worth knowing when a test hangs.\n\n### Docs\n**6. `plan-regression-tests.md` lists \"one small PDF and one small JPG\"**, but only `test_receipt.pdf` was added. If the JPG is intentionally Phase-2, change the doc to \"one small PDF (JPG follows in Phase 2)\"; otherwise add the JPG now.\n\n### Security\nNo surface-area concerns. Workflows are `contents: read`; the label workflow's `pull-requests: read` is the minimum needed for `pull_request` events. No new secrets, no network calls outside the existing GMD pipeline.","createdAt":"2026-05-22T14:12:51Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/yorvana/android/pull/123#issuecomment-4519471321","viewerDidAuthor":true},{"id":"IC_kwDOR2Hmq88AAAABDXOquA","author":{"login":"Mesya82"},"authorAssociation":"MEMBER","body":"## Re-review after `6076aef`\n\n### Delta vs prior 6 concerns\n\n| # | Prior issue | Status |\n|---|---|---|\n| 1 | Cron ran `regressionCheck` *and* `regressionFullCheck` (Regression set executed twice) | ✅ Fixed — cron now runs only `regressionFullCheck` (`.github/workflows/regression.yml:55`) |\n| 2 | `isExplicitAnnotationRun` evaluation timing unexplained | ✅ Comment added (`app/build.gradle.kts:233-234`); behaviour is correct given GradleBuild child builds see their own `startParameter.projectProperties` |\n| 3 | Silent `-P` override in regression child builds | ✅ Comment added (`app/build.gradle.kts:282-283`) |\n| 4 | Naming bleed — `regressionFullNightlyCoreCheck` ran outside the nightly path | ✅ Renamed to `regressionFullCoreCheck`, propagated to plan doc |\n| 5 | Regex Kotlin parser fragility | ✅ Documented in scanner comment + pinned with `unsupportedBacktickTestNamesAreNotScanned` fixture |\n| 6 | Plan-doc bundled-JPG mismatch | ✅ Doc now says \"one small PDF in Phase 1; JPG follows in Phase 2 if needed\" |\n\n### New issues from the full sweep\n\n1. **`FixtureAssetsRule.copyToDownloads` leaks MediaStore rows on partial failure** (`app/src/androidTest/java/com/yorvana/testsupport/FixtureAssetsRule.kt:46-60`). The Uri is appended to `mediaStoreUris` *after* the `openOutputStream`/copy/`IS_PENDING=0` update. If any step in that window throws, the row stays in MediaStore with `IS_PENDING=1` and `after()` won't clean it. Move `mediaStoreUris += uri` to immediately follow the `resolver.insert(...)` line so the cleanup list always reflects what was inserted.\n\n2. **Live-source scan is a no-op today** (`RegressionTierScannerTest.kt:7-15`). The `regression/` directory doesn't exist yet, and `scanDirectory` returns `emptyList()` for a missing root, so `androidTestRegressionSourcesHaveValidTierAnnotations` passes vacuously — only the in-memory fixtures actually exercise the parser. The path from `File(\"src/androidTest/.../regression\")` to the real source tree is unverified until Phase 2 ships a regression test. Worth either (a) seeding one minimal `@Regression`-annotated class under the regression package now so the live scan actually runs against real source, or (b) failing the test when the root is missing rather than treating absence as success.\n\n3. **`bySimpleName` collisions in inheritance walk** (`RegressionTierScannerTest.kt:202-204`). `inheritedClassScopes` keys the supertype lookup by simple name (`classes.associateBy { it.simpleName }`), so two classes sharing a simple name across packages silently mask each other — the walk follows whichever entry wins `associateBy`'s last-write semantics. Phase 1 doesn't hit it, but a future regression base class named the same as another test class would behave nondeterministically. Key by `fqName` (or document the limitation alongside the backtick caveat).\n\n### Clean across the rest of the diff\n- Workflows: `regression.yml`/`regression-label.yml` mirror `smoke.yml`/`smoke-label.yml` exactly — same KVM udev rule, same Gradle/JDK setup, same concurrency-group/cancel-in-progress shape, same `if-no-files-found: warn` artifact pattern. Distinct artifact name (`gmd-regression-test-reports`) avoids collision when both flows run.\n- `isTaskForDevice` refactor correctly tightens the previous `contains(..., ignoreCase = true)` matches; the exact-name check makes the smoke-annotation injection device-scoped.\n- `BillingSandbox` annotation exists but is intentionally unwired in Phase 1 (deferred to Phase 5 per plan doc); fine.\n- `@Smoke` added at class level on `SmokeTest` — pure annotation, no test-logic change.\n- `Camera`, `Permissions`, `SystemPicker` helpers are stubs as the plan promised; fallback selector chains are reasonable for the GMD target.\n- `RegressionTierScannerTest` fixture coverage (zero tier, two tier, orphan quarantine, inherited class, annotation-args + unknown-sibling, unsupported backticks) matches the documented contract.","createdAt":"2026-05-22T16:33:40Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/yorvana/android/pull/123#issuecomment-4520651448","viewerDidAuthor":true},{"id":"IC_kwDOR2Hmq88AAAABDakIkg","author":{"login":"Mesya82"},"authorAssociation":"MEMBER","body":"## Re-review after `25d1844`\n\nDelta against the prior sweep on `6076aef`.\n\n### Prior sweep issues — status\n\n| Prior sweep issue | Status | Verification |\n|---|---|---|\n| `mediaStoreUris += uri` ordering leak | ✅ Fixed | `FixtureAssetsRule.kt:50` — append now happens immediately after `resolver.insert(...)`, before the copy/`IS_PENDING` window that can throw |\n| Live-source scan vacuous (no `regression/` dir) | ✅ Fixed | New `RegressionScaffoldTest.kt` lands a `@Regression class` in `com.yorvana.regression`, so `scanDirectory(...)` now has at least one source to walk |\n| `bySimpleName` collisions on inherited supers | ✅ Fixed | Renamed to `byFqName`; `String.toFqName(packageName)` normalizes bare super names; `ParsedClass.superClassFqName` carries the qualified key; new `inheritedClassTiersUsePackageQualifiedLookup` fixture pins it |\n\n### Notes on the new code\n\n- **`toFqName` resolution rule** — for an unqualified super (`class Child : Base`), the scanner assumes `Base` lives in the child's package. Correct for the same-package case the fixture exercises, but doesn't model Kotlin imports: a `: Base` whose `Base` is `import com.yorvana.shared.Base` would resolve to `current.pkg.Base`, miss the lookup, and silently drop the inherited scope. The failure mode is a *false-positive* violation (test fails the guard with \"0 tier annotations\") rather than a silent pass, so the direction is safe — but worth a one-line fixture pinning the behavior so a future contributor doesn't quietly start relying on it. The scanner comment at `RegressionTierScannerTest.kt:192-195` covers the spirit of unsupported shapes but not this specific shape.\n- **`RegressionScaffoldTest.scaffoldIsDiscoverable() = Unit`** — placeholder serves a dual purpose (target for the live-source scanner, and at least one runtime match for `pixel2api33DebugAndroidTest -P…annotation=…tiers.Regression`). Reasonable for Phase 1.\n- **Cleanup ordering on `copyToDownloads` throw** — with the URI tracked before the write, a thrown `error(...)` from `openOutputStream` leaves `IS_PENDING=1`, but `after()`'s `resolver.delete(uri, …)` removes the pending row cleanly. ✓\n- **Fixture proves the fix** — `inheritedClassTiersUsePackageQualifiedLookup` would fail under the pre-fix `bySimpleName + associateBy` because the second `Base` (in `com.yorvana.other`) overwrites the first, dropping the inherited `@Regression`. With FQ keys it resolves deterministically. ✓\n\n### Verdict\n\nDelta is clean — all three prior items resolved, no new regressions, only an optional fixture to pin the cross-package-import limitation.","createdAt":"2026-05-23T04:25:54Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/yorvana/android/pull/123#issuecomment-4524148882","viewerDidAuthor":true},{"id":"IC_kwDOR2Hmq88AAAABDa9FqA","author":{"login":"Mesya82"},"authorAssociation":"MEMBER","body":"## Fresh sweep on `efc9cf11`\n\nNo new commits since the prior sweep — re-read the full PR with fresh eyes (workflows, gradle wiring, helpers, scanner, plan doc).\n\n### One small consistency gap\n\n`FixtureAssetsRule.copyToCache` (lines 25–32) hasn't received the same ordering fix that `copyToDownloads` got in `25d1844`:\n\n```kotlin\nfun copyToCache(assetName: String): File {\n    val target = File(context.cacheDir, assetName)\n    context.assets.open(assetName).use { input ->\n        target.outputStream().use { output -> input.copyTo(output) }\n    }\n    cacheFiles += target    // tracked only after the write completes\n    return target\n}\n```\n\nSame shape as the previously-fixed MediaStore leak: if `target.outputStream()` succeeds (file exists on disk) and then `copyTo` or either `use{}` close throws, `cacheFiles += target` never runs and `after()` leaks the file. Smaller blast radius than the MediaStore version — `cacheDir` is app-private — but the leak crosses tests within the same instrumentation run. Mirror of the existing fix: move `cacheFiles += target` immediately after `val target = File(...)`.\n\n### Everything else\n\n- **Workflows** (`regression.yml`, `regression-label.yml`): clean mirror of the smoke pair — same KVM setup, same `swiftshader_indirect`, same upload-artifact path, same `pull_request:[labeled]` opt-in. Cron runs `regressionFullCheck`; manual/label/reusable runs `regressionCheck`. Matches the plan doc.\n- **`app/build.gradle.kts`**: the `isTaskForDevice` refactor cleanly replaces every `contains(..., ignoreCase = true)` with exact `equals` and applies in both the `deviceDirName` branch and the args map. `isExplicitAnnotationRun` is captured at config time on purpose — the comment spells out why (`GradleBuild` children re-configure, so the property is re-read per build). The pixel2api33 branch's \"future Play/staging billing task must not inherit the smoke annotation filter\" note ties directly to the exact-match refactor; reads well.\n- **`registerRegressionDeviceTask`** + the three siblings + `regressionFullCheck` aggregator match §2/Phase 1 of the plan. `regressionFullCoreCheck` (no `notAnnotation`) and `regressionFullExtendedCheck` are wired; sandbox + macrobenchmark correctly deferred.\n- **Tier annotations**: all five carry `@Inherited` with `CLASS, FUNCTION` targets and `RUNTIME` retention — matches the scanner's superclass walk model and the runner filter.\n- **`SmokeTest.kt`**: just adds `@Smoke` to the class. Inherited applies correctly to subclassed cases (none today).\n- **UiAutomator helpers** (`Camera.kt`, `Permissions.kt`, `SystemPicker.kt`): expected stub-quality fallback selectors; `pickFolder`'s hardcoded \"Use this folder\" string is locale-dependent but matches the test plan's en-US assumption.\n- **`RegressionTierScannerTest.kt`**: scanner reasoning still holds — FQ-name keying, cycle protection in `inheritedClassScopes`, `toFqName` fallback to the child's package, documented limitations covered by `importedSuperclassTiersAreUnsupported` and `unsupportedBacktickTestNamesAreNotScanned`. Effective-scope semantics (closest scope with a tier wins; quarantine must pair at the same scope) match the plan's pairing rule.\n- **`docs/plan-regression-tests.md`**: rename to `regressionFullCoreCheck`, Phase 1 PDF-only fixture, and the split workflow description all line up with the actual code.\n\n**Verdict:** the only item worth touching is the `copyToCache` ordering mirror — no other regressions, no other inconsistencies.","createdAt":"2026-05-23T07:15:07Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/yorvana/android/pull/123#issuecomment-4524557736","viewerDidAuthor":true}],"headRefName":"issue-114-regression-infra","reviews":[],"statusCheckRollup":[{"__typename":"CheckRun","completedAt":"2026-05-23T05:06:55Z","conclusion":"SKIPPED","detailsUrl":"https://github.com/yorvana/android/actions/runs/26324200435/job/77498648248","name":"ci","startedAt":"2026-05-23T05:06:55Z","status":"COMPLETED","workflowName":"CI (Manual Label)"},{"__typename":"CheckRun","completedAt":"2026-05-23T05:07:44Z","conclusion":"SUCCESS","detailsUrl":"https://github.com/yorvana/android/actions/runs/26324200434/job/77498647952","name":"Semgrep Scan","startedAt":"2026-05-23T05:06:57Z","status":"COMPLETED","workflowName":"Semgrep"}],"url":"https://github.com/yorvana/android/pull/123"}


The latest comment is narrowly about `FixtureAssetsRule.copyToCache`: track the cache file immediately after computing the target so cleanup runs even if the write or close fails. I’m making that small ordering change, then I’ll run the focused checks and push.


sed -n '1,120p' app/src/androidTest/java/com/yorvana/testsupport/FixtureAssetsRule.kt


Output:
Chunk ID: 66cd48
Wall time: 0.1937 seconds
Process exited with code 0
Original token count: 653
Output:
package com.yorvana.testsupport

import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.rules.ExternalResource
import java.io.File

class FixtureAssetsRule : ExternalResource() {
    private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
    private val resolver = context.contentResolver
    private val mediaStoreUris = mutableListOf<Uri>()
    private val cacheFiles = mutableListOf<File>()

    override fun after() {
        mediaStoreUris.forEach { resolver.delete(it, null, null) }
        cacheFiles.forEach { it.delete() }
        mediaStoreUris.clear()
        cacheFiles.clear()
    }

    fun copyToCache(assetName: String): File {
        val target = File(context.cacheDir, assetName)
        context.assets.open(assetName).use { input ->
            target.outputStream().use { output -> input.copyTo(output) }
        }
        cacheFiles += target
        return target
    }

    fun copyToDownloads(
        assetName: String,
        displayName: String = assetName,
        mimeType: String = mimeTypeFor(assetName),
    ): Uri {
        val values =
            ContentValues().apply {
                put(MediaStore.Downloads.DISPLAY_NAME, displayName)
                put(MediaStore.Downloads.MIME_TYPE, mimeType)
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    put(MediaStore.Downloads.IS_PENDING, 1)
                }
            }
        val uri =
            resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
                ?: error("Unable to create Downloads fixture: $displayName")
        mediaStoreUris += uri
        context.assets.open(assetName).use { input ->
            resolver.openOutputStream(uri)?.use { output -> input.copyTo(output) }
                ?: error("Unable to write Downloads fixture: $displayName")
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            resolver.update(
                uri,
                ContentValues().apply { put(MediaStore.Downloads.IS_PENDING, 0) },
                null,
                null,
            )
        }
        return uri
    }

    private fun mimeTypeFor(assetName: String): String =
        when (assetName.substringAfterLast('.', missingDelimiterValue = "").lowercase()) {
            "jpg", "jpeg" -> "image/jpeg"
            "pdf" -> "application/pdf"
            else -> "application/octet-stream"
        }
}


Output:
Chunk ID: 83bb13
Wall time: 0.1956 seconds
Process exited with code 0
Original token count: 64
Output:
efc9cf1 Pin regression scanner import limitation
25d1844 Address regression scaffold review
6076aef Address regression review feedback
ca5ae95 Add regression test infrastructure
cdd19cc docs: reconcile regression plan with phase issues #114-#120 (#122)


Input:
*** Begin Patch
*** Update File: /home/Messier82/projects/service-book/app/src/androidTest/java/com/yorvana/testsupport/FixtureAssetsRule.kt
@@
     fun copyToCache(assetName: String): File {
         val target = File(context.cacheDir, assetName)
+        cacheFiles += target
         context.assets.open(assetName).use { input ->
             target.outputStream().use { output -> input.copyTo(output) }
         }
-        cacheFiles += target
         return target
     }
*** End Patch


Success. Updated the following files:
M /home/Messier82/projects/service-book/app/src/androidTest/java/com/yorvana/testsupport/FixtureAssetsRule.kt

@@ -26,2 +26,3 @@
         val target = File(context.cacheDir, assetName)
+        cacheFiles += target
         context.assets.open(assetName).use { input ->
@@ -29,3 +30,2 @@
         }
-        cacheFiles += target
         return target


Output:
Exit code: 0
Wall time: 0.8 seconds
Output:
Success. Updated the following files:
M /home/Messier82/projects/service-book/app/src/androidTest/java/com/yorvana/testsupport/FixtureAssetsRule.kt


The code change is in place. I’m running the same style checks that cover this source set, plus a diff check before committing.


./gradlew compileDebugAndroidTestKotlin ktlintCheck detekt lintDebug


Output:
Chunk ID: 8455ef
Wall time: 1.0012 seconds
Process running with session ID 4199
Original token count: 13
Output:
Picked up JAVA_TOOL_OPTIONS: -Xlog:os+container=off


Output:
Chunk ID: 2ba306
Wall time: 0.1369 seconds
Process exited with code 0
Original token count: 0
Output: