Skip to content

refactor(store): readonly-domain-types (stack 5/12, re-split #307) - #378

Closed
YosefHayim wants to merge 1 commit into
refactor/types/readonly-stack-04-providersfrom
refactor/types/readonly-stack-05-store
Closed

refactor(store): readonly-domain-types (stack 5/12, re-split #307)#378
YosefHayim wants to merge 1 commit into
refactor/types/readonly-stack-04-providersfrom
refactor/types/readonly-stack-05-store

Conversation

@YosefHayim

@YosefHayim YosefHayim commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Stack 5/12 of re-split HOLD #307

Domain: store
Base: refactor/types/readonly-stack-04-providers
Full green tip: refactor/foundation/readonly-types-full

Land stack in order. Intermediate PRs may not typecheck alone.


Summary by cubic

Make store domain types readonly and add MutableDeep where local mutation is needed, improving immutability and type safety across listing, offers, Game Center, Play, and reconcile flows. No functional changes expected; merges now avoid mutating shared arrays.

  • Refactors
    • Switched many inputs/collections to readonly (screenshots, offers, privacy, availability, team roles, sync jobs, etc.).
    • Introduced MutableDeep for constructing mutable payloads (listings, planned actions, Play releases/products/subscriptions, ASC requests).
    • Added mergeAppleLocale to apply drafts safely and copy keywords into a new array.
    • Updated reconcile helpers: actions are mutable handles; summarize now accepts readonly.
    • Minor test and call-site updates to match new types.

Written for commit 7edfce6. Summary will update on new commits.

Review in cubic

@codeant-ai

codeant-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Reviewed your PR 7edfce6 Aug 07, 2026 · 11:06 11:09

@codeant-ai

codeant-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@changeset-bot

changeset-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7edfce6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@codeant-ai codeant-ai Bot added the size:L This PR changes 100-499 lines, ignoring generated files label Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cd9dd465-a1ab-4f01-9ae2-19d7678fb08a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Refactor store domain consumers for readonly types

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Update store/listing/privacy APIs to accept readonly arrays and readonly-first domain types.
• Use MutableDeep and array copies when constructing objects from readonly inputs.
• Avoid mutating action arrays by returning merged copies in sync reporting.
Diagram

graph TD
  extTypes{{"Readonly domain types"}} --> mut["src/core/types/mutable.ts"] --> listing["Listing (apply/generator)"]
  mut --> store["Store reconcilers"] --> reconcile["store/reconcile.ts"] --> sync["Sync run (syncRun/syncJobs)"]
  mut --> privacy["Privacy (parse/reconcile)"]
  store --> tests["offers.test.ts"]

  subgraph Legend
    direction LR
    _ext{{"External / type change"}} ~~~ _file["File / module"]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use targeted `as` casts instead of MutableDeep
  • ➕ Less new type plumbing; fewer generic types introduced
  • ➖ Pushes unsoundness into many call sites
  • ➖ Easier to accidentally mutate readonly values without copying
2. Introduce builder/factory helpers per domain object
  • ➕ Encapsulates copying rules (arrays/records) in one place
  • ➕ Reduces repeated spread/copy boilerplate across modules
  • ➖ More up-front API design and additional files
  • ➖ May be overkill for a transitional stack landing
3. Relax readonly domain types for action/config shapes
  • ➕ Minimizes consumer churn and avoids mutable wrappers
  • ➖ Undermines the goal of readonly-by-default domain types
  • ➖ Harder to reason about unintended mutations long-term

Recommendation: Current approach (readonly signatures + MutableDeep for construction + explicit array copying) is the best fit for a readonly-by-default migration: it keeps mutation localized to intentional construction points and avoids widespread casting. If repetition grows, consider adding small builder helpers for common merges (e.g., locale merge, action list merges).

Files changed (21) +142 / -78

Refactor (20) +140 / -76
apply.tsMake listing draft helpers readonly-safe and copy keyword arrays +40/-8

Make listing draft helpers readonly-safe and copy keyword arrays

• Updates keyword helpers to accept readonly arrays and uses MutableDeep when building draft/brief objects. Introduces a dedicated Apple locale merge that copies keywords into a mutable array to avoid readonly violations, and updates applyDraft to use it.

src/core/listing/apply.ts

generator.tsUse MutableDeep when constructing DraftListing from generated content +2/-1

Use MutableDeep when constructing DraftListing from generated content

• Adjusts draft parsing to build DraftListing via MutableDeep to accommodate readonly domain typing while still conditionally assigning fields.

src/core/listing/generator.ts

parse.tsAccept readonly arrays in uniqueness helper +1/-1

Accept readonly arrays in uniqueness helper

• Changes the local 'unique' utility to accept 'readonly string[]' while preserving behavior.

src/core/privacy/parse.ts

reconcile.tsMake privacy report builder accept readonly scanned paths +1/-1

Make privacy report builder accept readonly scanned paths

• Updates 'buildPrivacyReport' to take 'readonly string[]' for scanned inputs, matching readonly domain typing.

src/core/privacy/reconcile.ts

accessibility.tsNormalize accessibility support via MutableDeep builder object +2/-1

Normalize accessibility support via MutableDeep builder object

• Uses MutableDeep when constructing the normalized accessibility support map to avoid assigning into a readonly-typed object.

src/core/store/accessibility.ts

appEvents.tsBuild ASC app event payloads using MutableDeep +3/-2

Build ASC app event payloads using MutableDeep

• Switches event and localization attribute objects to MutableDeep to allow incremental field assignment under readonly domain types.

src/core/store/appEvents.ts

ascScreenshots.tsMake screenshot reconciliation accept readonly collections +5/-5

Make screenshot reconciliation accept readonly collections

• Updates internal helpers and reconcile functions to take readonly arrays for screenshots, previews, and review screenshots without changing behavior.

src/core/store/ascScreenshots.ts

ascSync.tsTreat desired purchase arrays as readonly in ASC sync pipeline +2/-2

Treat desired purchase arrays as readonly in ASC sync pipeline

• Changes desired in-app purchase collections and reconcile function parameters to 'readonly' arrays, aligning with readonly catalog types.

src/core/store/ascSync.ts

availability.tsAccept readonly territories list for normalization +1/-1

Accept readonly territories list for normalization

• Updates territory normalization to take 'readonly string[]' while preserving trimming/uppercasing behavior.

src/core/store/availability.ts

gameCenter.tsReadonly-safe Game Center reconciliation inputs and action mutation +6/-5

Readonly-safe Game Center reconciliation inputs and action mutation

• Updates reconcile inputs (achievements/leaderboards arrays) to readonly, and allows localization action updates by typing the planned action as MutableDeep.

src/core/store/gameCenter.ts

offers.tsReadonly-safe offers reconciliation and mutable create payloads +19/-15

Readonly-safe offers reconciliation and mutable create payloads

• Changes offer-related config arrays and price arrays to readonly across helpers and reconcilers. Uses MutableDeep for create payload objects that are built incrementally during reconciliation.

src/core/store/offers.ts

playProducts.tsBuild Play product resource via MutableDeep for incremental assignment +2/-1

Build Play product resource via MutableDeep for incremental assignment

• Types the desired Play product payload as MutableDeep to allow property assignment while keeping exported types readonly.

src/core/store/playProducts.ts

playSubscriptions.tsReadonly-safe Play subscription reconciliation and deep copies for resendable payloads +20/-17

Readonly-safe Play subscription reconciliation and deep copies for resendable payloads

• Updates listing/base plan/offer inputs to readonly arrays and uses MutableDeep to build resendable base plans. Ensures arrays like 'regionalConfigs' and 'offerTags' are copied when reconstructing patch payloads.

src/core/store/playSubscriptions.ts

playTracks.tsBuild Play release payload via MutableDeep +2/-1

Build Play release payload via MutableDeep

• Types the constructed Play release object as MutableDeep to support incremental construction with copied version codes.

src/core/store/playTracks.ts

reconcile.tsMake reconcile actions mutable handles while exposing readonly summaries +17/-5

Make reconcile actions mutable handles while exposing readonly summaries

• Changes ReconcileContext to store mutable PlannedAction handles (MutableDeep) so status/error can be updated. Adjusts 'plan' to return a mutable handle and 'summarize' to accept a readonly action list.

src/core/store/reconcile.ts

reportsCommand.tsUse MutableDeep for report query payload objects +3/-2

Use MutableDeep for report query payload objects

• Updates sales/finance report query objects to MutableDeep to allow property assignment under readonly type constraints.

src/core/store/reportsCommand.ts

syncJobs.tsMake app selection and job building accept/return readonly app lists +3/-3

Make app selection and job building accept/return readonly app lists

• Updates sync job selection/build APIs to accept readonly app descriptor arrays and returns a readonly list from selection to avoid accidental mutation.

src/core/store/syncJobs.ts

syncRun.tsAvoid mutating readonly action arrays when adding asset actions +9/-3

Avoid mutating readonly action arrays when adding asset actions

• Changes SyncAppReport actions to readonly and updates reconcileJob to return a new report with concatenated actions instead of pushing into the existing array.

src/core/store/syncRun.ts

team.tsAccept readonly role fragments for normalization +1/-1

Accept readonly role fragments for normalization

• Updates role normalization helper to take 'readonly string[]' without changing parsing/normalization logic.

src/core/store/team.ts

walletIds.tsMake wallet ID reconcile inputs readonly +1/-1

Make wallet ID reconcile inputs readonly

• Updates wallet ID reconciliation to accept readonly declared config arrays, aligning with readonly domain config typing.

src/core/store/walletIds.ts

Tests (1) +2 / -2
offers.test.tsAlign test fake method signatures with readonly arrays +2/-2

Align test fake method signatures with readonly arrays

• Updates the offers API fake to accept readonly ordered IDs and stores 'reorderedTo' as a readonly array reference.

src/core/store/offers.test.ts

Comment thread src/core/store/syncRun.ts
const report = yield* reconcileApp(client, reconcileInput);
report.actions.push(...(yield* reconcileAssetActions(client, job, dryRun, allowDestructive)));
return { job, report };
const assetActions = yield* reconcileAssetActions(client, job, dryRun, allowDestructive);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: If reconcileApp fails with a catalog precondition or API error, this call is never reached and the surrounding catchAll returns only { error }. Consequently, screenshots and previews are neither reconciled nor reported, even though reconcileAssetActions is explicitly designed to be an isolated pass whose failures must not discard other work. Run the asset pass independently of catalog reconciliation and combine both outcomes. [incomplete implementation]

Severity Level: Major ⚠️
- ❌ Sync reports only catalog errors, omitting asset actions.
- ⚠️ Asset planning is skipped after catalog API failures.
- ❌ Apply cannot process assets from failed catalog plans.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/core/store/syncRun.ts
**Line:** 190:190
**Comment:**
	*Incomplete Implementation: If `reconcileApp` fails with a catalog precondition or API error, this call is never reached and the surrounding `catchAll` returns only `{ error }`. Consequently, screenshots and previews are neither reconciled nor reported, even though `reconcileAssetActions` is explicitly designed to be an isolated pass whose failures must not discard other work. Run the asset pass independently of catalog reconciliation and combine both outcomes.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. ReconcileContext outside core/types.ts 📘 Rule violation ⌂ Architecture
Description
This PR modifies exported domain-shape types (ReconcileContext, SyncAppReport.actions) in
feature/logic modules instead of centralizing them in src/core/types.ts as required. This makes
shared shapes harder to reuse consistently and violates the required type-centralization policy.
Code

src/core/store/reconcile.ts[R6-9]

/** Mutable state for one reconciliation pass. */
export type ReconcileContext = {
-  actions: PlannedAction[];
+  actions: MutableDeep<PlannedAction>[];
  dryRun: boolean;
Evidence
The checklist requires modified domain shapes to be defined in src/core/types.ts. The diff shows
exported type shapes being modified directly in src/core/store/reconcile.ts
(ReconcileContext.actions) and src/core/store/syncRun.ts (SyncAppReport.actions) instead of
being centralized.

Rule 1028638: Domain shapes and provider interfaces must be defined in src/core/types.ts
src/core/store/reconcile.ts[6-10]
src/core/store/syncRun.ts[37-42]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR changes exported domain-shape types in non-type modules (e.g., `src/core/store/reconcile.ts`, `src/core/store/syncRun.ts`) instead of defining them in `src/core/types.ts`.

## Issue Context
Compliance requires new/modified domain shapes to live in `src/core/types.ts` (not in feature/logic modules).

## Fix Focus Areas
- src/core/store/reconcile.ts[6-10]
- src/core/store/syncRun.ts[37-42]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Manual locale merge drift 🐞 Bug ⚙ Maintainability
Description
mergeAppleLocale manually copies each AppleLocaleInfo field, so any future additions to
AppleLocaleInfo that exist in persisted configs will be silently dropped when a locale is updated
via applyDraft. This is a forward-compatibility data-loss risk compared to the prior generic
spread merge behavior.
Code

src/core/listing/apply.ts[R140-143]

+const mergeAppleLocale = (
+  existingLocale: AppleLocaleInfo | undefined,
+  listingDraft: DraftListing,
+): AppleLocaleInfo => {
Evidence
mergeAppleLocale constructs a new locale object and copies only a fixed set of known
AppleLocaleInfo properties; it does not preserve any other properties that may exist on the
persisted object. AppleLocaleInfo is defined as a plain object type with multiple optional fields,
so adding a new field later would not automatically be preserved by this manual merge.

src/core/listing/apply.ts[139-169]
src/core/store/storeConfig.ts[16-32]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`mergeAppleLocale` enumerates `AppleLocaleInfo` fields one-by-one. If `AppleLocaleInfo` gains new optional fields later (and existing configs contain them), calling `applyDraft` for a locale will rebuild that locale object without the new fields, effectively deleting them.

### Issue Context
The old approach of spreading the existing locale preserved all enumerable runtime properties. The new approach requires manual synchronization with `AppleLocaleInfo`.

### Fix Focus Areas
- src/core/listing/apply.ts[139-169]

### Suggested fix
Refactor `mergeAppleLocale` to preserve all existing locale properties via a spread, and then only special-case `keywords` to ensure it is cloned into a new mutable array.

For example:
- Start with `const mergedLocale: MutableDeep<AppleLocaleInfo> = { ...(existingLocale ?? {}) }` (preserves any future fields).
- If `existingLocale?.keywords` exists, set `mergedLocale.keywords = [...existingLocale.keywords]`.
- Overlay the draft fields (`title`, `subtitle`, `description`, `promotionalText`, and `keywords`), cloning `keywords` from the draft when present.

This keeps the runtime “preserve unknown fields” behavior while still avoiding aliasing the keywords array.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ Compliance rules (platform): 48 rules

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines 6 to 9
/** Mutable state for one reconciliation pass. */
export type ReconcileContext = {
actions: PlannedAction[];
actions: MutableDeep<PlannedAction>[];
dryRun: boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. reconcilecontext outside core/types.ts 📘 Rule violation ⌂ Architecture

This PR modifies exported domain-shape types (ReconcileContext, SyncAppReport.actions) in
feature/logic modules instead of centralizing them in src/core/types.ts as required. This makes
shared shapes harder to reuse consistently and violates the required type-centralization policy.
Agent Prompt
## Issue description
The PR changes exported domain-shape types in non-type modules (e.g., `src/core/store/reconcile.ts`, `src/core/store/syncRun.ts`) instead of defining them in `src/core/types.ts`.

## Issue Context
Compliance requires new/modified domain shapes to live in `src/core/types.ts` (not in feature/logic modules).

## Fix Focus Areas
- src/core/store/reconcile.ts[6-10]
- src/core/store/syncRun.ts[37-42]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/core/listing/apply.ts
Comment on lines +140 to +143
const mergeAppleLocale = (
existingLocale: AppleLocaleInfo | undefined,
listingDraft: DraftListing,
): AppleLocaleInfo => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Manual locale merge drift 🐞 Bug ⚙ Maintainability

mergeAppleLocale manually copies each AppleLocaleInfo field, so any future additions to
AppleLocaleInfo that exist in persisted configs will be silently dropped when a locale is updated
via applyDraft. This is a forward-compatibility data-loss risk compared to the prior generic
spread merge behavior.
Agent Prompt
### Issue description
`mergeAppleLocale` enumerates `AppleLocaleInfo` fields one-by-one. If `AppleLocaleInfo` gains new optional fields later (and existing configs contain them), calling `applyDraft` for a locale will rebuild that locale object without the new fields, effectively deleting them.

### Issue Context
The old approach of spreading the existing locale preserved all enumerable runtime properties. The new approach requires manual synchronization with `AppleLocaleInfo`.

### Fix Focus Areas
- src/core/listing/apply.ts[139-169]

### Suggested fix
Refactor `mergeAppleLocale` to preserve all existing locale properties via a spread, and then only special-case `keywords` to ensure it is cloned into a new mutable array.

For example:
- Start with `const mergedLocale: MutableDeep<AppleLocaleInfo> = { ...(existingLocale ?? {}) }` (preserves any future fields).
- If `existingLocale?.keywords` exists, set `mergedLocale.keywords = [...existingLocale.keywords]`.
- Overlay the draft fields (`title`, `subtitle`, `description`, `promotionalText`, and `keywords`), cloning `keywords` from the draft when present.

This keeps the runtime “preserve unknown fields” behavior while still avoiding aliasing the keywords array.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@YosefHayim

Copy link
Copy Markdown
Owner Author

Superseded by land of tip stack #386 (same 12 domain commits).

@YosefHayim YosefHayim closed this Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant