Obtainium import/export (#444 ask 1/3)#502
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
WalkthroughAdds Obtainium import/export: new serializable domain models, mappers for conversion, repository export/import with format detection, enriched ImportResult, UI wiring (actions/state/viewmodel), ImportSummarySheet UI, localized strings, and release-note updates. ChangesObtainium Export/Import Feature
Sequence DiagramsequenceDiagram
participant UI as UI (Compose)
participant VM as AppsViewModel
participant Repo as AppsRepository
participant Mapper as ObtainiumImportMapper
participant Share as ShareManager
UI->>VM: OnExportObtainium
VM->>Repo: exportObtainium()
Repo->>Mapper: map InstalledApp -> ObtainiumApp
Mapper-->>Repo: ObtainiumExport JSON
Repo-->>VM: JSON string
VM->>Share: share(filename, JSON)
Share-->>VM: success/failure
VM-->>UI: emit ShowError / share completed
UI->>VM: OnImportApps(json)
VM->>Repo: importApps(json)
Repo->>Repo: detectFormat(json)
alt OBTAINIUM
Repo->>Mapper: parse ObtainiumExport -> toExportedAppOrSkip()
Mapper-->>Repo: ExportedApp or skip labels
Repo->>Repo: linkAppToRepo(...) per ExportedApp
else NATIVE
Repo->>Repo: decode native ExportedAppList
Repo->>Repo: linkAppToRepo(...) per item
else UNKNOWN
Repo-->>VM: ImportResult(source=UNKNOWN, preview)
end
Repo-->>VM: ImportResult (detailed)
VM-->>UI: update state.importSummary / emit ImportComplete
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 5/8 reviews remaining, refill in 17 minutes and 47 seconds.Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (3)
core/presentation/src/commonMain/composeResources/files/whatsnew/ko/16.json (1)
14-15: ⚡ Quick winConsider aligning the “What’s New” wording with the updated UI labels.
The new bullet uses “Obtainium 가져오기/내보내기”, but the PR objectives mention updated UI copy semantics like “Import from file” and “Export library”. If those distinctions matter for users (import source vs export destination/scope), consider tweaking the Korean bullet to match the same intent/terminology as the app’s action labels.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core/presentation/src/commonMain/composeResources/files/whatsnew/ko/16.json` around lines 14 - 15, The Korean bullet "Obtainium 가져오기/내보내기" should be split/reworded to match the app UI semantics: replace that single string with wording that distinguishes import source and export scope, e.g. use "파일에서 Obtainium 가져오기" to reflect "Import from file" and "Obtainium 형식으로 라이브러리 내보내기" to reflect "Export library"; update the JSON entry that currently contains "Obtainium 가져오기/내보내기" accordingly so users see the same intent/terminology as the UI.core/presentation/src/commonMain/composeResources/files/whatsnew/tr/16.json (1)
14-15: ⚡ Quick winTURKISH copy: “Obtainium içe/dışa aktarma” ifadesini biraz daha netleştirin.
Mevcut metin (“...tek dokunuşla getirin...”) “içe aktar” karşılığını tam taşımıyor; ayrıca “içe/dışa aktarma” başlığıyla daha tutarlı bir fiil kullanılırsa kullanıcı için anlaşılabilirlik artar. Örn. “getirin” yerine “içe aktarın” gibi.
🛠️ Önerilen metin güncellemesi
- "Obtainium içe/dışa aktarma — Obtainium kütüphanenizi tek dokunuşla getirin ya da istediğiniz zaman Obtainium biçiminde dışa aktarın." + "Obtainium içe/dışa aktarma — Obtainium kütüphanenizi tek dokunuşla içe aktarın ya da istediğiniz zaman Obtainium biçiminde dışa aktarın."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core/presentation/src/commonMain/composeResources/files/whatsnew/tr/16.json` around lines 14 - 15, Update the Turkish copy string that currently reads "Obtainium içe/dışa aktarma — Obtainium kütüphanenizi tek dokunuşla getirin ya da istediğiniz zaman Obtainium biçiminde dışa aktarın." to use consistent, clearer verbs for import/export (e.g., replace "getirin" with "içe aktarın") so the header "Obtainium içe/dışa aktarma" aligns with the action descriptions; ensure the revised sentence mirrors the header structure such as "Obtainium içe/dışa aktarma — Obtainium kütüphanenizi tek dokunuşla içe aktarın veya istediğiniz zaman Obtainium biçiminde dışa aktarın."feature/apps/domain/src/commonMain/kotlin/zed/rainxch/apps/domain/repository/AppsRepository.kt (1)
76-78: ⚡ Quick winConsider documenting the Obtainium export contract (schema + stability + unsupported fields).
Right now the interface exposes
exportObtainium(): Stringbut doesn’t say what schema version/shape callers can expect or what’s intentionally omitted for unsupported Obtainium fields. A short KDoc on the method would prevent downstream misuse/regressions when the schema evolves.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/apps/domain/src/commonMain/kotlin/zed/rainxch/apps/domain/repository/AppsRepository.kt` around lines 76 - 78, Add KDoc to AppsRepository.exportObtainium(): String describing the Obtainium export contract: state the schema version (e.g., "v1"), the expected JSON shape/fields (required vs optional), which fields are intentionally omitted or unsupported, stability/compatibility guarantees (what may change and how), and any consumer expectations (encoding, size limits). Reference the exportObtainium() method in the comment, include examples or a link to the formal schema if available, and note how breaking changes will be communicated so callers don’t misinterpret future exports.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/ObtainiumImportMapper.kt`:
- Around line 86-91: The current mapping treats invertFilter (derived from
apkFilterRegEx/invertAPKFilter) as no filter by setting effectiveFilter = null
which silently broadens assetFilterRegex; instead detect when invertFilter is
true and surface it explicitly by failing the import or returning a clear
sentinel/error value rather than null—update the logic around
invertFilter/effectiveFilter in ObtainiumImportMapper (where apkFilterRegEx and
invertAPKFilter are parsed) so that inverted filters are reported as unsupported
(e.g., throw a descriptive exception or return an explicit "inverted filter
unsupported" result alongside fallback) instead of returning a null
assetFilterRegex.
In `@core/presentation/src/commonMain/composeResources/files/whatsnew/bn/16.json`:
- Around line 14-15: Update the second bullet string so it explicitly reads
"Import from file" in Bengali: find the JSON entry currently starting with
"Obtainium ইম্পোর্ট/এক্সপোর্ট — এক ট্যাপে Obtainium থেকে লাইব্রেরি আনুন, বা
যেকোনো সময় Obtainium ফরম্যাটে এক্সপোর্ট করুন।" and change the import phrase to
include "ফাইল থেকে" (e.g., "Obtainium ইম্পোর্ট/এক্সপোর্ট — একটি ক্লিকে ফাইল থেকে
Obtainium লাইব্রেরি আনুন, বা যেকোনো সময় Obtainium ফরম্যাটে এক্সপোর্ট করুন।") so
the wording matches the UI label renaming to “Import from file.”
In `@core/presentation/src/commonMain/composeResources/files/whatsnew/fr/16.json`:
- Around line 14-15: Replace the misspelled product name "Dhizuku" with
"Shizuku" in the French whatsnew entry that starts with "Installation
silencieuse via Dhizuku : contournez les fenêtres d’installation des OEM..." so
it matches the other mention of "Shizuku" in the same file (the string value in
the JSON array for fr/16.json).
In
`@feature/apps/data/src/commonMain/kotlin/zed/rainxch/apps/data/repository/AppsRepositoryImpl.kt`:
- Around line 275-280: exportObtainium() currently serializes an empty library
as {"apps":[]} which detectFormat() treats as UNKNOWN because it sniff-parses
the first app; add a stable top-level discriminator to the Obtainium export
(e.g. a format or version field on the ObtainiumExport data class) and populate
it from exportObtainium() when serializing, then update detectFormat() to check
that top-level discriminator (accepting an empty "apps" array if the
discriminator matches) so empty exports round-trip reliably.
- Around line 307-318: detectFormat currently only inspects apps.firstOrNull(),
causing mixed files to be misclassified; update detectFormat to iterate over the
apps JsonArray and evaluate each app JsonObject: if any app contains "repoOwner"
and "repoName" return ImportFormat.NATIVE, else if any app has the Obtainium
shape (contains "id" and "url" and the url jsonPrimitive contains "github.com")
return ImportFormat.OBTAINIUM, otherwise return ImportFormat.UNKNOWN; reference
the detectFormat function, the apps JsonArray, firstApp check, and
importObtainium path when making the change.
In
`@feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/ImportSummarySheet.kt`:
- Around line 279-283: The IconButton's Icon currently sets contentDescription
to null, making the expand/collapse control inaccessible; update the Icon in the
ImportSummarySheet component to provide a localized label derived from the
expanded boolean (e.g., use stringResource(R.string.collapse) when expanded is
true and stringResource(R.string.expand) when false) and pass that string as the
Icon's contentDescription so screen readers announce the control; reference the
IconButton/Icon and the expanded variable to locate where to inject the
localized string resources.
- Around line 102-121: The accessibility announcement currently builds a
hardcoded-English string in ImportSummarySheet.kt (the announcement variable
created via buildString and assigned to the Text semantics
contentDescription/liveRegion), which must be replaced with a localized string;
update the code to obtain a formatted, localized resource from the
core/presentation 13-language localization system (use the appropriate string
key that accepts imported, skipped, nonGitHubSkipped and failed counts, handle
the optional nonGitHubSkipped segment via plural/optional formatting in the
resource), format it with the numeric values, and assign that localized result
to the semantics contentDescription (instead of the hardcoded buildString) so
TalkBack/VoiceOver follows the active locale.
---
Nitpick comments:
In `@core/presentation/src/commonMain/composeResources/files/whatsnew/ko/16.json`:
- Around line 14-15: The Korean bullet "Obtainium 가져오기/내보내기" should be
split/reworded to match the app UI semantics: replace that single string with
wording that distinguishes import source and export scope, e.g. use "파일에서
Obtainium 가져오기" to reflect "Import from file" and "Obtainium 형식으로 라이브러리 내보내기" to
reflect "Export library"; update the JSON entry that currently contains
"Obtainium 가져오기/내보내기" accordingly so users see the same intent/terminology as
the UI.
In `@core/presentation/src/commonMain/composeResources/files/whatsnew/tr/16.json`:
- Around line 14-15: Update the Turkish copy string that currently reads
"Obtainium içe/dışa aktarma — Obtainium kütüphanenizi tek dokunuşla getirin ya
da istediğiniz zaman Obtainium biçiminde dışa aktarın." to use consistent,
clearer verbs for import/export (e.g., replace "getirin" with "içe aktarın") so
the header "Obtainium içe/dışa aktarma" aligns with the action descriptions;
ensure the revised sentence mirrors the header structure such as "Obtainium
içe/dışa aktarma — Obtainium kütüphanenizi tek dokunuşla içe aktarın veya
istediğiniz zaman Obtainium biçiminde dışa aktarın."
In
`@feature/apps/domain/src/commonMain/kotlin/zed/rainxch/apps/domain/repository/AppsRepository.kt`:
- Around line 76-78: Add KDoc to AppsRepository.exportObtainium(): String
describing the Obtainium export contract: state the schema version (e.g., "v1"),
the expected JSON shape/fields (required vs optional), which fields are
intentionally omitted or unsupported, stability/compatibility guarantees (what
may change and how), and any consumer expectations (encoding, size limits).
Reference the exportObtainium() method in the comment, include examples or a
link to the formal schema if available, and note how breaking changes will be
communicated so callers don’t misinterpret future exports.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1ec37ac9-a5e5-4e21-9d9c-68570de46091
📒 Files selected for processing (25)
core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/ObtainiumExportMapper.ktcore/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/ObtainiumImportMapper.ktcore/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ObtainiumExport.ktcore/presentation/src/commonMain/composeResources/files/whatsnew/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ar/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/bn/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/es/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/fr/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/hi/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/it/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ja/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ko/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/pl/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ru/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/tr/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/16.jsoncore/presentation/src/commonMain/composeResources/values/strings.xmlfeature/apps/data/src/commonMain/kotlin/zed/rainxch/apps/data/repository/AppsRepositoryImpl.ktfeature/apps/domain/src/commonMain/kotlin/zed/rainxch/apps/domain/model/ImportResult.ktfeature/apps/domain/src/commonMain/kotlin/zed/rainxch/apps/domain/repository/AppsRepository.ktfeature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.ktfeature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.ktfeature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsState.ktfeature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.ktfeature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/ImportSummarySheet.kt
| "Dhizuku সাইলেন্ট ইনস্টল — Device Owner-এর মাধ্যমে Xiaomi, OPPO, vivo, Huawei ডিভাইসে OEM ইনস্টল প্রম্পট এড়িয়ে যান।", | ||
| "Obtainium ইম্পোর্ট/এক্সপোর্ট — এক ট্যাপে Obtainium থেকে লাইব্রেরি আনুন, বা যেকোনো সময় Obtainium ফরম্যাটে এক্সপোর্ট করুন।" |
There was a problem hiding this comment.
Make the “Obtainium import” bullet explicitly “from file” to match the UI label change.
The added bullet says “এক ট্যাপে Obtainium থেকে লাইব্রেরি আনুন…”, which may read like importing from “Obtainium” generally, rather than from a file—whereas the PR intent is specifically to rename the UI to “Import from file”.
💡 Suggested wording tweak
- "Obtainium ইম্পোর্ট/এক্সপোর্ট — এক ট্যাপে Obtainium থেকে লাইব্রেরি আনুন, বা যেকোনো সময় Obtainium ফরম্যাটে এক্সপোর্ট করুন।"
+ "Obtainium ইম্পোর্ট/এক্সপোর্ট — এক ট্যাপে ফাইল থেকে Obtainium লাইব্রেরি আনুন, বা যেকোনো সময় Obtainium ফরম্যাটে এক্সপোর্ট করুন।"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "Dhizuku সাইলেন্ট ইনস্টল — Device Owner-এর মাধ্যমে Xiaomi, OPPO, vivo, Huawei ডিভাইসে OEM ইনস্টল প্রম্পট এড়িয়ে যান।", | |
| "Obtainium ইম্পোর্ট/এক্সপোর্ট — এক ট্যাপে Obtainium থেকে লাইব্রেরি আনুন, বা যেকোনো সময় Obtainium ফরম্যাটে এক্সপোর্ট করুন।" | |
| "Dhizuku সাইলেন্ট ইনস্টল — Device Owner-এর মাধ্যমে Xiaomi, OPPO, vivo, Huawei ডিভাইসে OEM ইনস্টল প্রম্পট এড়িয়ে যান।", | |
| "Obtainium ইম্পোর্ট/এক্সপোর্ট — এক ট্যাপে ফাইল থেকে Obtainium লাইব্রেরি আনুন, বা যেকোনো সময় Obtainium ফরম্যাটে এক্সপোর্ট করুন।" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@core/presentation/src/commonMain/composeResources/files/whatsnew/bn/16.json`
around lines 14 - 15, Update the second bullet string so it explicitly reads
"Import from file" in Bengali: find the JSON entry currently starting with
"Obtainium ইম্পোর্ট/এক্সপোর্ট — এক ট্যাপে Obtainium থেকে লাইব্রেরি আনুন, বা
যেকোনো সময় Obtainium ফরম্যাটে এক্সপোর্ট করুন।" and change the import phrase to
include "ফাইল থেকে" (e.g., "Obtainium ইম্পোর্ট/এক্সপোর্ট — একটি ক্লিকে ফাইল থেকে
Obtainium লাইব্রেরি আনুন, বা যেকোনো সময় Obtainium ফরম্যাটে এক্সপোর্ট করুন।") so
the wording matches the UI label renaming to “Import from file.”
| "Installation silencieuse via Dhizuku : contournez les fenêtres d’installation des OEM (Xiaomi, OPPO, vivo, Huawei) grâce au statut Propriétaire de l’appareil.", | ||
| "Import/Export Obtainium : récupérez votre bibliothèque depuis Obtainium en un toucher, ou exportez vers Obtainium quand vous voulez." |
There was a problem hiding this comment.
Fix likely typo/inconsistent naming: “Dhizuku” → “Shizuku”.
The new French bullet uses “Dhizuku”, but this same file already mentions “Shizuku” in the FIXED section (line ~29). If the intended product is “Shizuku”, this is a user-facing text bug that should be corrected for consistency and clarity.
Suggested diff
- "Installation silencieuse via Dhizuku : contournez les fenêtres d’installation des OEM (Xiaomi, OPPO, vivo, Huawei) grâce au statut Propriétaire de l’appareil.",
+ "Installation silencieuse via Shizuku : contournez les fenêtres d’installation des OEM (Xiaomi, OPPO, vivo, Huawei) grâce au statut Propriétaire de l’appareil.",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "Installation silencieuse via Dhizuku : contournez les fenêtres d’installation des OEM (Xiaomi, OPPO, vivo, Huawei) grâce au statut Propriétaire de l’appareil.", | |
| "Import/Export Obtainium : récupérez votre bibliothèque depuis Obtainium en un toucher, ou exportez vers Obtainium quand vous voulez." | |
| "Installation silencieuse via Shizuku : contournez les fenêtres d'installation des OEM (Xiaomi, OPPO, vivo, Huawei) grâce au statut Propriétaire de l'appareil.", | |
| "Import/Export Obtainium : récupérez votre bibliothèque depuis Obtainium en un toucher, ou exportez vers Obtainium quand vous voulez." |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@core/presentation/src/commonMain/composeResources/files/whatsnew/fr/16.json`
around lines 14 - 15, Replace the misspelled product name "Dhizuku" with
"Shizuku" in the French whatsnew entry that starts with "Installation
silencieuse via Dhizuku : contournez les fenêtres d’installation des OEM..." so
it matches the other mention of "Shizuku" in the same file (the string value in
the JSON array for fr/16.json).
…ction, stamp export discriminator, localize summary a11y
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/ObtainiumImportMapper.kt`:
- Around line 34-38: The current code in ObtainiumImportMapper sets a blank
package id into nonGitHubLabel which is wrong; instead route it to the failure
bucket. In the block that computes packageName (in ObtainiumImportMapper.kt)
replace the ObtainiumMapResult construction so exported = null, nonGitHubLabel =
null, and unsupportedFailureLabel = "$owner/$repo (missing package id)" (i.e.,
move the "(missing package id)" message from nonGitHubLabel into
unsupportedFailureLabel) so GitHub-hosted entries with a blank id are treated as
failures not "non-GitHub".
- Around line 68-88: The code stores the original rawUrl into repoUrl which can
include query strings, fragments or extra path segments; instead, after calling
parseGithubOwnerRepo(rawUrl) (the function defined in ObtainiumImportMapper),
reconstruct a canonical repository URL using the parsed owner and repo (e.g.
"https://github.com/{owner}/{repo}") and assign that canonical URL to repoUrl;
ensure this replacement happens only when parseGithubOwnerRepo returns a
non-null Pair and keep existing validation (owner/repo length checks) intact so
exported/open-in-browser links always point to the repo root.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: dbb57672-090a-4b48-bf0a-33d51300e28c
📒 Files selected for processing (4)
core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/ObtainiumImportMapper.ktcore/presentation/src/commonMain/composeResources/values/strings.xmlfeature/apps/data/src/commonMain/kotlin/zed/rainxch/apps/data/repository/AppsRepositoryImpl.ktfeature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/ImportSummarySheet.kt
🚧 Files skipped from review as they are similar to previous changes (1)
- feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/ImportSummarySheet.kt
First of three PRs for #444. Adds Obtainium JSON support to the existing import/export flow plus a richer summary sheet. See plan in
roadmap/444_A_OBTAINIUM_IMPORT_EXPORT.md.What's in here
LiveRegion. Sheet dismisses on button press, never on a timer.Tradeoffs
apps[]shape only. Could be stricter (Obtainium hasadditionalSettings/overrideSourcediscriminators) but URL-pattern + key-shape catches the real-world cases without false positives on truncated v4 exports.id,url,author,name,additionalSettings,overrideSource: "GitHub"). If Obtainium breaks the schema, recovery is one mapper edit.installerPackageNamefield. That's an explicit decoupling — the upcoming installer-attribution feature (Plan C) ships independently. Documented in plan A out-of-scope.Test plan
Summary by CodeRabbit
New Features
UI / Text