From 6e1970553a4dba29c9d54aeebb3d9fc54a2ec3be Mon Sep 17 00:00:00 2001 From: Darkaxt Date: Sun, 30 Aug 2026 00:00:02 +0200 Subject: [PATCH 1/2] fix: activate CRC-less RetroArch sessions safely Co-Authored-By: Claude --- .../setup/RetroArchSetupCoordinator.kt | 72 ++++++++++++------- .../darkaxt/dualdex/setup/SessionEpochGate.kt | 6 ++ .../dualdex/setup/SessionEpochGateTest.kt | 22 ++++++ .../dualdex/retroarch/RomSessionResolver.kt | 9 +++ .../retroarch/RomSessionResolverTest.kt | 23 ++++++ 5 files changed, 106 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/darkaxt/dualdex/setup/RetroArchSetupCoordinator.kt b/app/src/main/java/com/darkaxt/dualdex/setup/RetroArchSetupCoordinator.kt index 581148b9a..84a0f0fa4 100644 --- a/app/src/main/java/com/darkaxt/dualdex/setup/RetroArchSetupCoordinator.kt +++ b/app/src/main/java/com/darkaxt/dualdex/setup/RetroArchSetupCoordinator.kt @@ -308,7 +308,7 @@ class RetroArchSetupCoordinator( return false } val entry = activeEntry.get() ?: return false - val token = sessionEpoch.capture(entry.sessionIdentity()) ?: return false + val token = sessionEpoch.capture(entry.currentSessionIdentity()) ?: return false if (!activationGate.retry(entry.sourceId)) return false activate(entry, token) return true @@ -316,7 +316,7 @@ class RetroArchSetupCoordinator( fun selectSave(documentId: String): Boolean { val entry = activeEntry.get() ?: return false - val token = sessionEpoch.capture(entry.sessionIdentity()) ?: return false + val token = sessionEpoch.capture(entry.currentSessionIdentity()) ?: return false if (lastSaveCandidates.get().none { it.id == documentId }) return false val selected = saveMonitor.select(entry.sha256, documentId) { commit -> sessionEpoch.commitIfCurrent(token, commit) @@ -599,39 +599,39 @@ class RetroArchSetupCoordinator( ?: SessionResolution.NoContent val connected = session.connection != RetroArchConnection.DISCONNECTED val restartVerified = restartVerifier.observe(session.connection) - val resolvedEntry = (resolution as? SessionResolution.Resolved)?.entry - val nextAuthorizedEntry = resolvedEntry?.takeIf { connected } - val catalogCancellation = if (activeEntry.get() != nextAuthorizedEntry) { + val sourceVerificationEntry = RomSessionResolver.sourceVerificationCandidate(resolution) + ?.takeIf { connected } + val catalogCancellation = if (activeEntry.get() != sourceVerificationEntry) { runtime.cancelPendingCatalogLoadForAuthorityTransition() } else { null } val token = sessionEpoch.observe( - nextAuthorizedEntry?.sessionIdentity(), + sourceVerificationEntry?.sessionIdentity(resolution), ) catalogCancellation?.complete() - val authorizedEntry = nextAuthorizedEntry?.takeIf { token != null } - val previousEntry = activeEntry.getAndSet(authorizedEntry) - if (previousEntry != authorizedEntry) { + val candidateEntry = sourceVerificationEntry?.takeIf { token != null } + val previousEntry = activeEntry.getAndSet(candidateEntry) + if (previousEntry != candidateEntry) { previousEntry?.let { activationGate.cancel(it.sourceId) } restoredSaveRom.set(null) lastSaveCandidates.set(emptyList()) discoveredSaveRom.set(null) discoveredSaveBasename.set(null) } - val active = authorizedEntry != null && + val active = candidateEntry != null && token != null && activationCoordinator.isVerified(token) && - runtime.catalogHash() == authorizedEntry.sha256 - val loading = authorizedEntry != null && token != null && - activationCoordinator.isLoading(authorizedEntry.sourceId, token) - val failed = authorizedEntry != null && token != null && - activationCoordinator.isFailed(authorizedEntry.sourceId, token) + runtime.catalogHash() == candidateEntry.sha256 + val loading = candidateEntry != null && token != null && + activationCoordinator.isLoading(candidateEntry.sourceId, token) + val failed = candidateEntry != null && token != null && + activationCoordinator.isFailed(candidateEntry.sourceId, token) val publishBattleSession = { battleMemory.updateSession( connected = connected && active, systemId = status?.systemId, - romIdentity = authorizedEntry?.sha256, + romIdentity = candidateEntry?.sha256, ) } if (token != null) { @@ -648,9 +648,9 @@ class RetroArchSetupCoordinator( systemId = status?.systemId, gameBasename = status?.gameBasename, contentCrc32 = status?.crc32, - contentSha256 = authorizedEntry?.sha256?.takeIf { active }, + contentSha256 = candidateEntry?.sha256?.takeIf { active }, sessionEpoch = token?.epoch?.takeIf { active }, - activeSource = authorizedEntry?.sourceName?.takeIf { active }, + activeSource = candidateEntry?.sourceName?.takeIf { active }, savefileDirectory = session.savefileDirectory, resolution = when (resolution) { SessionResolution.NoContent -> "NO_CONTENT" @@ -660,17 +660,22 @@ class RetroArchSetupCoordinator( failed -> "FAILED" else -> "RESOLVED" } - is SessionResolution.Unverified -> "UNVERIFIED" + is SessionResolution.Unverified -> when { + active -> "ACTIVE" + loading -> "LOADING" + failed -> "FAILED" + else -> "UNVERIFIED" + } is SessionResolution.Ambiguous -> "AMBIGUOUS" is SessionResolution.NotFound -> "NOT_FOUND" }, message = session.error ?: when { - connected && active -> "Opened ${resolvedEntry.sourceName}." + connected && active -> "Opened ${sourceVerificationEntry.sourceName}." connected && loading -> "Opening the SHA-256-verified active catalog…" connected && failed -> current.message connected && resolution is SessionResolution.Resolved -> "Active content matched; verifying its SHA-256." connected && resolution is SessionResolution.Unverified -> - "A matching filename was found, but RetroArch did not provide content identity. Live features are paused." + "Active filename matched; opening and hashing the exact granted ROM source." connected && resolution is SessionResolution.Ambiguous -> "Multiple granted sources match the active content. Select the ROM manually." connected && resolution is SessionResolution.NotFound -> resolution.reason connected -> "RetroArch Network Commands verified." @@ -684,11 +689,11 @@ class RetroArchSetupCoordinator( } else if (!closed) { publishSessionView() } - if (authorizedEntry != null && token != null) { - activate(authorizedEntry, token) + if (candidateEntry != null && token != null) { + activate(candidateEntry, token) if (active) { - restorePersistedSave(authorizedEntry, token) - pollSave(authorizedEntry, token) + restorePersistedSave(candidateEntry, token) + pollSave(candidateEntry, token) } } } @@ -1072,9 +1077,24 @@ class RetroArchSetupCoordinator( context.contentResolver.takePersistableUriPermission(uri, flags) } - private fun RomIndexEntry.sessionIdentity() = VerifiedSessionIdentity( + private fun RomIndexEntry.currentSessionIdentity() = VerifiedSessionIdentity( + romSha256 = sha256.lowercase(), + sourceId = sourceId, + evidence = if (view.get().contentCrc32 == null) { + SessionIdentityEvidence.BASENAME_DISCOVERY + } else { + SessionIdentityEvidence.RETROARCH_CRC + }, + ) + + private fun RomIndexEntry.sessionIdentity(resolution: SessionResolution) = VerifiedSessionIdentity( romSha256 = sha256.lowercase(), sourceId = sourceId, + evidence = when (resolution) { + is SessionResolution.Resolved -> SessionIdentityEvidence.RETROARCH_CRC + is SessionResolution.Unverified -> SessionIdentityEvidence.BASENAME_DISCOVERY + else -> error("non-candidate session resolution") + }, ) private fun connectionOf(value: String): RetroArchConnection = diff --git a/app/src/main/java/com/darkaxt/dualdex/setup/SessionEpochGate.kt b/app/src/main/java/com/darkaxt/dualdex/setup/SessionEpochGate.kt index 2787973d8..7b52580df 100644 --- a/app/src/main/java/com/darkaxt/dualdex/setup/SessionEpochGate.kt +++ b/app/src/main/java/com/darkaxt/dualdex/setup/SessionEpochGate.kt @@ -5,9 +5,15 @@ import java.util.concurrent.ExecutionException import java.util.concurrent.Executors import java.util.concurrent.FutureTask +internal enum class SessionIdentityEvidence { + RETROARCH_CRC, + BASENAME_DISCOVERY, +} + internal data class VerifiedSessionIdentity( val romSha256: String, val sourceId: String, + val evidence: SessionIdentityEvidence = SessionIdentityEvidence.RETROARCH_CRC, ) internal data class SessionWorkToken( diff --git a/app/src/test/java/com/darkaxt/dualdex/setup/SessionEpochGateTest.kt b/app/src/test/java/com/darkaxt/dualdex/setup/SessionEpochGateTest.kt index 1dcb0e963..b3849e231 100644 --- a/app/src/test/java/com/darkaxt/dualdex/setup/SessionEpochGateTest.kt +++ b/app/src/test/java/com/darkaxt/dualdex/setup/SessionEpochGateTest.kt @@ -90,6 +90,28 @@ class SessionEpochGateTest { assertFalse(activation.isVerified(reconnect)) } + @Test + fun losingCrcEvidenceForTheSameSourceRequiresFreshVerification() { + val gate = SessionEpochGate() + val activation = SessionActivationCoordinator(gate) + val crcBacked = requireNotNull(gate.observe(first)) + assertTrue(activation.begin(crcBacked, first.sourceId) {}) + assertTrue(activation.finish(crcBacked, first.sourceId) {}) + + val discovered = first.copy(evidence = SessionIdentityEvidence.BASENAME_DISCOVERY) + val crcLess = requireNotNull(gate.observe(discovered)) + + assertNotEquals(crcBacked, crcLess) + assertFalse(activation.isVerified(crcLess)) + assertTrue( + activation.requiresSourceVerification( + crcLess, + activeCatalogSha256 = first.romSha256, + expectedSha256 = first.romSha256, + ), + ) + } + @Test fun reconnectingTheSameIdentityRequiresANewVerificationToken() { val gate = SessionEpochGate() diff --git a/retroarch-session/src/main/kotlin/com/darkaxt/dualdex/retroarch/RomSessionResolver.kt b/retroarch-session/src/main/kotlin/com/darkaxt/dualdex/retroarch/RomSessionResolver.kt index d0068d6ff..99b2465b9 100644 --- a/retroarch-session/src/main/kotlin/com/darkaxt/dualdex/retroarch/RomSessionResolver.kt +++ b/retroarch-session/src/main/kotlin/com/darkaxt/dualdex/retroarch/RomSessionResolver.kt @@ -32,6 +32,15 @@ object RomSessionResolver { fun verifySha(entry: RomIndexEntry, actualSha256: String): Boolean = entry.sha256.matches(Regex("[0-9a-fA-F]{64}")) && entry.sha256.equals(actualSha256, ignoreCase = true) + fun sourceVerificationCandidate(resolution: SessionResolution): RomIndexEntry? = when (resolution) { + is SessionResolution.Resolved -> resolution.entry + is SessionResolution.Unverified -> resolution.entry + is SessionResolution.Ambiguous, + is SessionResolution.NotFound, + SessionResolution.NoContent, + -> null + } + private fun resolveRunning(status: RetroArchStatus.Running, entries: List): SessionResolution { val platforms = compatiblePlatforms(status.systemId) if (platforms.isEmpty()) return SessionResolution.NotFound("unsupported RetroArch system: ${status.systemId}") diff --git a/retroarch-session/src/test/kotlin/com/darkaxt/dualdex/retroarch/RomSessionResolverTest.kt b/retroarch-session/src/test/kotlin/com/darkaxt/dualdex/retroarch/RomSessionResolverTest.kt index f6bb90127..9d01c7133 100644 --- a/retroarch-session/src/test/kotlin/com/darkaxt/dualdex/retroarch/RomSessionResolverTest.kt +++ b/retroarch-session/src/test/kotlin/com/darkaxt/dualdex/retroarch/RomSessionResolverTest.kt @@ -82,6 +82,29 @@ class RomSessionResolverTest { assertEquals(SessionResolution.Unverified(emerald), result) } + @Test + fun crcLessUniqueMatchCanOnlyProceedToFreshSourceVerification() { + val resolution = RomSessionResolver.resolve( + RetroArchStatus.Running(false, "game_boy_advance", emerald.gameBasename, null), + listOf(emerald), + ) + + assertEquals(SessionResolution.Unverified(emerald), resolution) + assertEquals(emerald, RomSessionResolver.sourceVerificationCandidate(resolution)) + } + + @Test + fun ambiguousCrcLessMatchCannotProceedToSourceVerification() { + val second = emerald.copy(sourceId = "different", sha256 = "b".repeat(64)) + val resolution = RomSessionResolver.resolve( + RetroArchStatus.Running(false, "game_boy_advance", emerald.gameBasename, null), + listOf(emerald, second), + ) + + assertTrue(resolution is SessionResolution.Ambiguous) + assertEquals(null, RomSessionResolver.sourceVerificationCandidate(resolution)) + } + @Test fun aReportedCrcMismatchNeverFallsBackToBasename() { val result = RomSessionResolver.resolve( From e547397e53ad1d777980e44549603d0f69cd8f8f Mon Sep 17 00:00:00 2001 From: Darkaxt Date: Sun, 30 Aug 2026 00:02:32 +0200 Subject: [PATCH 2/2] release: prepare RC78 hotfix 5 Co-Authored-By: Claude --- docs/reports/qa-hardening/stage-07-closure.md | 2 +- docs/reports/qa-hardening/stage-08-closure.md | 2 +- release/RELEASE_NOTES_1.1.0-rc.78-hotfix.5.md | 59 +++++++++++++++++++ release/compatibility-evidence.json | 2 +- 4 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 release/RELEASE_NOTES_1.1.0-rc.78-hotfix.5.md diff --git a/docs/reports/qa-hardening/stage-07-closure.md b/docs/reports/qa-hardening/stage-07-closure.md index 9f63f1802..8477554ee 100644 --- a/docs/reports/qa-hardening/stage-07-closure.md +++ b/docs/reports/qa-hardening/stage-07-closure.md @@ -56,7 +56,7 @@ A post-run inventory comparison found the same 333 eligible names and one intent | Current release/governance gate after closure packaging and APK privacy correction | 85 Node tests passed. | | Fresh corpus summarization | Streaming raw-report hash, source/generator lineage, canonical multiset, terminal outcomes, and persistence/reopen checks passed. | -The expensive parser and consolidated Gradle gates ran once after parser/catalog product-source stabilization. They were not repeated for downstream release packaging, acceptance-test stabilization, the later replacement of two Android API-33-only bounded reads with the shared API-30-compatible reader, repository-policy alignment with the established single-maintainer signing process, correction of GitHub policy reads to use public nonsecret endpoints instead of an integration token lacking administration scope, binding the comparison range to the cache decision's prior parser schema, tightening the Windows-path detector so arbitrary compressed APK bytes cannot be misclassified as a private path, or preserving area-title sign labels in the Local-map projection while suppressing only the duplicate Area Guide drawer text. Focused checkpoint/save tests, the source-contract regression, Android test compilation, app lint, the PR managed-device suite, the candidate workflows' complete unsigned build and packaged Android gates, the reproduced APK privacy regression, the complete Area Guide suite, the exact Modern Emerald sign control, and the release-governance tests passed after those corrections. `NONPARSER_REUSE` is explicit and does not permit parser, catalog, build, wrapper, or corpus-execution changes. +The expensive parser and consolidated Gradle gates ran once after parser/catalog product-source stabilization. They were not repeated for downstream release packaging, acceptance-test stabilization, the later replacement of two Android API-33-only bounded reads with the shared API-30-compatible reader, repository-policy alignment with the established single-maintainer signing process, correction of GitHub policy reads to use public nonsecret endpoints instead of an integration token lacking administration scope, binding the comparison range to the cache decision's prior parser schema, tightening the Windows-path detector so arbitrary compressed APK bytes cannot be misclassified as a private path, or preserving area-title sign labels in the Local-map projection while suppressing only the duplicate Area Guide drawer text, or restoring CRC-less RetroArch live activation through fresh granted-source SHA verification and evidence-bound session epochs. Focused checkpoint/save tests, the source-contract regression, Android test compilation, app lint, the PR managed-device suite, the candidate workflows' complete unsigned build and packaged Android gates, the reproduced APK privacy regression, the complete Area Guide suite, the exact Modern Emerald sign control, the focused CRC-less RetroArch resolver/session-authority/activation regressions, and the release-governance tests passed after those corrections. `NONPARSER_REUSE` is explicit and does not permit parser, catalog, build, wrapper, or corpus-execution changes. ## Missing-feature classification diff --git a/docs/reports/qa-hardening/stage-08-closure.md b/docs/reports/qa-hardening/stage-08-closure.md index ecd867ecd..bae9369e3 100644 --- a/docs/reports/qa-hardening/stage-08-closure.md +++ b/docs/reports/qa-hardening/stage-08-closure.md @@ -54,7 +54,7 @@ The denominator correction does not waive an input. The audited physical invento | Final corpus | 333/333 eligible inputs terminal; 0 parser/catalog/compatibility/persistence errors; 278/278 selected catalogs persisted and reopened | `PASS` | | Downstream evidence hardening | Bounded 1.63 GB streaming summary, corrected denominator, duplicate-aware canonical contract, promotion/readiness/privacy regressions | `PASS` | -The consolidated Gradle and hours-long parser gates ran once after parser/catalog product-source stabilization. They were not repeated for downstream release packaging, deterministic acceptance-test corrections, the replacement of two Android API-33-only read calls with the already-tested API-30-compatible bounded reader, repository-policy alignment with the established single-maintainer signing process, correction of GitHub policy reads to public nonsecret endpoints, binding the release comparison range to the cache decision's prior parser schema, tightening the Windows-path detector so arbitrary compressed APK bytes cannot be misclassified as a private path, or preserving area-title sign labels in the Local-map projection while suppressing only the duplicate Area Guide drawer text. Focused checkpoint/save tests, source contracts, Android test compilation, app lint, the 7/7 PR managed-device suite, the candidate workflows' complete unsigned build and packaged Android gates, the reproduced APK privacy regression, the complete Area Guide suite, the exact Modern Emerald sign control, and the release-governance suite passed afterward. The evidence validator rejects reuse if parser/catalog sources, build logic, wrapper, or corpus-execution tooling changes. +The consolidated Gradle and hours-long parser gates ran once after parser/catalog product-source stabilization. They were not repeated for downstream release packaging, deterministic acceptance-test corrections, the replacement of two Android API-33-only read calls with the already-tested API-30-compatible bounded reader, repository-policy alignment with the established single-maintainer signing process, correction of GitHub policy reads to public nonsecret endpoints, binding the release comparison range to the cache decision's prior parser schema, tightening the Windows-path detector so arbitrary compressed APK bytes cannot be misclassified as a private path, or preserving area-title sign labels in the Local-map projection while suppressing only the duplicate Area Guide drawer text, or restoring CRC-less RetroArch live activation through fresh granted-source SHA verification and evidence-bound session epochs. Focused checkpoint/save tests, source contracts, Android test compilation, app lint, the 7/7 PR managed-device suite, the candidate workflows' complete unsigned build and packaged Android gates, the reproduced APK privacy regression, the complete Area Guide suite, the exact Modern Emerald sign control, the focused CRC-less RetroArch resolver/session-authority/activation regressions, and the release-governance suite passed afterward. The evidence validator rejects reuse if parser/catalog sources, build logic, wrapper, or corpus-execution tooling changes. No ad hoc emulator, ADB gesture, physical-device action, credential inspection, signing-material inspection, signing, tagging, or publication was performed for this closure. diff --git a/release/RELEASE_NOTES_1.1.0-rc.78-hotfix.5.md b/release/RELEASE_NOTES_1.1.0-rc.78-hotfix.5.md new file mode 100644 index 000000000..f4e819e64 --- /dev/null +++ b/release/RELEASE_NOTES_1.1.0-rc.78-hotfix.5.md @@ -0,0 +1,59 @@ +# DualDex 1.1.0-rc.78-hotfix.5 + +RC78 hotfix 5 is the release candidate for the completed project-wide QA hardening program. It closes the remaining Android setup, parser/catalog, runtime authority, companion transport, privacy, and release-governance gaps, then binds them to the final source-bound compatibility corpus. The hotfix qualifier preserves the immutable earlier RC78 tags while keeping cache validation bound to the correct prior parser-schema baseline, validating public assets without binary false positives, restoring area-title sign labels to their real Local-map coordinates, and restoring live activation when RetroArch omits CRC32 from an otherwise valid running-content response. + +## Android setup and recovery + +- Recover cleanly when a guide cannot be loaded instead of crashing the app or leaving the guide surface in a stale state. +- Reconcile direct-storage and folder-picker access without discarding the last valid index during a failed rescan. +- Quarantine revoked folder grants, route package-specific settings safely, and explain protected `Android/data` and `Android/obb` limitations accurately. +- Deliver overlay picker results exactly once across cold starts, new intents, retries, and activity recreation. +- Keep Area Guide projection failures local to that optional module. + +## Parser and catalog resilience + +- Bound complete-ROM probes, archive extraction, catalog payloads, concurrent corpus work, and detached Gen I species discovery. +- Add cancellation checks to long parser passes and replenish ordered corpus work after any completion so one slow input cannot stall unrelated inputs. +- Verify catalog identity and canonical content digests before activation, quarantine invalid snapshots, and fail closed on malformed optional data. +- Invalidate pre-hardening parser catalogs through schema revision 46 and retain seeded rebuild coverage. + +## Runtime authority and companion safety + +- Require verified ROM identity and monotonic session epochs before live memory or SaveRAM state can become authoritative. +- Fence queued mapper, socket, command, delayed-reply, and checkpoint work against stale sessions. +- Recover RetroArch configuration and command sockets transactionally while bounding memory reads, UDP drains, and retained snapshots. +- When RetroArch omits CRC32, use only an unambiguous system-and-basename match as a discovery candidate, then reopen and hash the exact granted ROM source before activating its catalog or live readers. +- Keep CRC-backed and basename-discovered session evidence in distinct epochs so loss of CRC cannot inherit prior verified authority; reported CRC mismatches and ambiguous basename matches still fail closed. +- Bound Android and desktop companion transport, fence navigation/state/media responses, preserve structured retries, and isolate optional feature failures. +- Keep an area-title sign label at its real Local-map coordinate while suppressing only the redundant same-name entry in the Area Guide drawer; Organic discovery behavior remains unchanged. +- Remove private paths, reversible player-state fingerprints, raw failures, stacks, workspace identifiers, and device identifiers from normal diagnostics and public evidence. + +## Source-bound QA closure + +- Audit 334 supported-extension files while evaluating all 333 scanner-eligible mainline and hack inputs; one known spin-off remains intentionally excluded by scanner policy. +- Reach terminal parser outcomes for 333/333 inputs: 278 selected, 2 ambiguous, 53 without a family match, and 0 parser errors. +- Record 20 complete, 302 partial, and 11 unresolved data-compatibility outcomes with 0 compatibility errors. +- Materialize, persist, close, reopen, and decode all 278 selected catalogs with 0 catalog or persistence errors. +- Close QA Stages 7 and 8 with zero blockers and zero referrals. + +## Measured validation + +- Post-remediation Kotlin and Android gate: 65 tasks passed in 40m36s across parser, catalog, CLI, runtime, companion, and app unit suites. +- Companion web gate: 32 Vitest files and 268 tests passed; the TypeScript/Vite production build passed. +- Portable Chromium acceptance: 3/3 Playwright tests passed. +- Release and governance gate: 85/85 Node tests passed. +- Public QA evidence: 7/7 assets passed structural privacy validation. +- PR Android acceptance: 7/7 managed-device tests passed; min-SDK-30 app lint and focused bounded-read tests passed after the final compatibility correction. +- The complete Area Guide builder suite passed with the exact Modern Emerald v3.5 control; its Littleroot sign remained labeled at tile `(15, 13)` while the duplicate drawer label stayed suppressed. +- Physical diagnosis confirmed that RetroArch 1.22.2 reported Modern Emerald as `PLAYING` while omitting CRC32; DualDex correctly received the connection but had previously left the session `UNVERIFIED`. +- Focused regression gates passed: 10/10 resolver tests, 8/8 session-epoch tests, and 6/6 guide-activation tests, including CRC-evidence downgrade invalidation and ambiguity fail-closed behavior. +- Public nonsecret repository-policy reads were verified against the live tag and environment configuration without querying signing material. +- Cache validation selects the latest prior tag whose parser schema matches the explicit cache decision, so failed immutable delivery tags cannot replace the evidence baseline. +- Public-asset validation still rejects complete private paths embedded in binary payloads while ignoring isolated drive-prefix byte sequences in compressed APK data. +- Protected release managed-device acceptance, signing, and Thor validation remain mandatory before candidate promotion. + +## Delivery + +- This candidate uses Android version code `1010083`. +- The candidate is built and signed only through the protected GitHub Actions environment; production signing material is never exposed to the repository or local workspace. +- DualDex remains read-only and sends no game commands or emulator-memory writes. diff --git a/release/compatibility-evidence.json b/release/compatibility-evidence.json index 4aeb2d2c2..d245db1b5 100644 --- a/release/compatibility-evidence.json +++ b/release/compatibility-evidence.json @@ -12,7 +12,7 @@ }, "scopeDecision": { "type": "NONPARSER_REUSE", - "attestation": "Fresh raw parser evidence was generated at the named source commit; downstream release packaging, acceptance tests, API-30-compatible Android bounded reads, repository-policy alignment, public nonsecret GitHub policy access, schema-bound comparison selection, binary-safe public-asset privacy validation, and the Area Guide projection correction changed afterward without changing parser, catalog, build, wrapper, or corpus-execution source." + "attestation": "Fresh raw parser evidence was generated at the named source commit; downstream release packaging, acceptance tests, API-30-compatible Android bounded reads, repository-policy alignment, public nonsecret GitHub policy access, schema-bound comparison selection, binary-safe public-asset privacy validation, the Area Guide projection correction, and CRC-less RetroArch session activation with evidence-bound epochs changed afterward without changing parser, catalog, build, wrapper, or corpus-execution source." }, "cacheDecision": { "type": "BUMP_REQUIRED",