diff --git a/companion-core/src/main/kotlin/com/enrpau/dualscreendex/companion/map/AreaGuideBuilder.kt b/companion-core/src/main/kotlin/com/enrpau/dualscreendex/companion/map/AreaGuideBuilder.kt index f6c8c8e9..ce25b78b 100644 --- a/companion-core/src/main/kotlin/com/enrpau/dualscreendex/companion/map/AreaGuideBuilder.kt +++ b/companion-core/src/main/kotlin/com/enrpau/dualscreendex/companion/map/AreaGuideBuilder.kt @@ -70,7 +70,9 @@ object AreaGuideBuilder { AreaGuidePointCategory.SERVICE, AreaGuidePointCategory.UNKNOWN -> { outputBudget.retain() - placesAndServices += point + placesAndServices += point.copy( + label = point.label?.takeUnless { it.equals(name, ignoreCase = true) }, + ) } AreaGuidePointCategory.AVAILABLE_ITEM, AreaGuidePointCategory.COLLECTED_ITEM -> { @@ -150,8 +152,8 @@ object AreaGuideBuilder { val label = when { !identified -> null category == AreaGuidePointCategory.AVAILABLE_ITEM || category == AreaGuidePointCategory.COLLECTED_ITEM -> - normalizeText(poi.item?.displayName, trainer?.name, names[poi.baseAreaId]) - else -> normalizeText(poiName(poi, trainer?.gender), trainer?.name, names[poi.baseAreaId]) + normalizeText(poi.item?.displayName, trainer?.name, null) + else -> normalizeText(poiName(poi, trainer?.gender), trainer?.name, null) } outputBudget?.retain() add(AreaGuidePoint( diff --git a/companion-core/src/test/kotlin/com/enrpau/dualscreendex/companion/map/AreaGuideBuilderRealControlTest.kt b/companion-core/src/test/kotlin/com/enrpau/dualscreendex/companion/map/AreaGuideBuilderRealControlTest.kt new file mode 100644 index 00000000..ad4a566d --- /dev/null +++ b/companion-core/src/test/kotlin/com/enrpau/dualscreendex/companion/map/AreaGuideBuilderRealControlTest.kt @@ -0,0 +1,48 @@ +package com.enrpau.dualscreendex.companion.map + +import com.enrpau.dualscreendex.companion.model.AppSnapshot +import com.enrpau.dualscreendex.companion.model.CompanionSettings +import com.enrpau.dualscreendex.companion.model.KnowledgeMode +import com.enrpau.dualscreendex.parser.catalog.CatalogParser +import com.enrpau.dualscreendex.parser.io.RomImage +import java.nio.file.Files +import java.nio.file.Path +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assume.assumeTrue +import org.junit.Test + +class AreaGuideBuilderRealControlTest { + @Test + fun `Modern Emerald keeps the Littleroot town sign on the map without duplicating the guide heading`() { + val configured = System.getenv("DUALDEX_MODERN_EMERALD_ROM") + assumeTrue("set DUALDEX_MODERN_EMERALD_ROM to run this real-ROM control", !configured.isNullOrBlank()) + val path = Path.of(requireNotNull(configured)) + assumeTrue("real ROM does not exist: $path", Files.isRegularFile(path)) + val rom = RomImage(Files.readAllBytes(path)) + assertEquals(MODERN_EMERALD_SHA, rom.sha256) + val catalog = requireNotNull(CatalogParser.parseCatching(rom).catalog).getOrThrow() + + val projection = AreaGuideBuilder.project( + catalog, + AppSnapshot( + liveAreaBaseId = LITTLEROOT_TOWN, + settings = CompanionSettings(knowledgeMode = KnowledgeMode.DISCOVERED), + ), + ) + val sign = projection.points.single { + it.baseAreaId == LITTLEROOT_TOWN && it.tileX == 15 && it.tileY == 13 + } + + assertEquals("Littleroot Town", sign.label) + assertNull( + projection.guide.areas.single { it.baseAreaId == LITTLEROOT_TOWN } + .placesAndServices.single { it.key == sign.key }.label, + ) + } + + private companion object { + const val LITTLEROOT_TOWN = 0x0009 + const val MODERN_EMERALD_SHA = "21a0306c4e5b5dc15ca70b74e713e3140612c1045aa298072993a6c5dd8d6895" + } +} diff --git a/companion-core/src/test/kotlin/com/enrpau/dualscreendex/companion/map/AreaGuideBuilderTest.kt b/companion-core/src/test/kotlin/com/enrpau/dualscreendex/companion/map/AreaGuideBuilderTest.kt index 8c7da8f6..22423250 100644 --- a/companion-core/src/test/kotlin/com/enrpau/dualscreendex/companion/map/AreaGuideBuilderTest.kt +++ b/companion-core/src/test/kotlin/com/enrpau/dualscreendex/companion/map/AreaGuideBuilderTest.kt @@ -203,6 +203,35 @@ class AreaGuideBuilderTest { assertNull(unnamed.label) } + @Test + fun areaTitleSignRemainsLabeledOnMapWithoutAddingDuplicateGuideText() { + val catalog = catalog().let { original -> + val areaTitleSign = original.localMaps.pois.single { it.key == GENERIC } + .copy(displayName = "Quiet Corner\nA longer description") + original.copy( + localMaps = original.localMaps.copy( + pois = original.localMaps.pois.map { poi -> + if (poi.key == GENERIC) areaTitleSign else poi + }, + ), + ) + } + + val projection = AreaGuideBuilder.project( + catalog, + AppSnapshot( + liveAreaBaseId = EMPTY, + settings = CompanionSettings(knowledgeMode = KnowledgeMode.DISCOVERED), + ), + ) + + assertEquals("Quiet Corner", projection.points.single { it.key == GENERIC }.label) + assertNull( + projection.guide.areas.single { it.baseAreaId == EMPTY } + .placesAndServices.single { it.key == GENERIC }.label, + ) + } + @Test fun objectivesAreAttachedOnlyToTheirKnowledgeVisibleArea() { val objective = AreaGuideObjective("open-road", "Open Road") diff --git a/docs/reports/qa-hardening/stage-07-closure.md b/docs/reports/qa-hardening/stage-07-closure.md index a2f91025..9f63f180 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, or tightening the Windows-path detector so arbitrary compressed APK bytes cannot be misclassified as a private path. Focused checkpoint/save tests, the source-contract regression, Android test compilation, app lint, the PR managed-device suite, the failed candidate's complete unsigned build and packaged Android gates, the reproduced APK privacy regression, 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. 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. ## Missing-feature classification diff --git a/docs/reports/qa-hardening/stage-08-closure.md b/docs/reports/qa-hardening/stage-08-closure.md index 38392d59..ecd867ec 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, or tightening the Windows-path detector so arbitrary compressed APK bytes cannot be misclassified as a private path. Focused checkpoint/save tests, source contracts, Android test compilation, app lint, the 7/7 PR managed-device suite, the failed candidate's complete unsigned build and packaged Android gates, the reproduced APK privacy regression, 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. 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. 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.4.md b/release/RELEASE_NOTES_1.1.0-rc.78-hotfix.4.md new file mode 100644 index 00000000..0585a36a --- /dev/null +++ b/release/RELEASE_NOTES_1.1.0-rc.78-hotfix.4.md @@ -0,0 +1,55 @@ +# DualDex 1.1.0-rc.78-hotfix.4 + +RC78 hotfix 4 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, and restoring area-title sign labels to their real Local-map coordinates. + +## 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. +- 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. +- 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 `1010082`. +- 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 b7e416ff..4aeb2d2c 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, and binary-safe public-asset privacy validation 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, and the Area Guide projection correction changed afterward without changing parser, catalog, build, wrapper, or corpus-execution source." }, "cacheDecision": { "type": "BUMP_REQUIRED",