### Architecture/Apple Ads Api Support # Apple Ads Campaign Management API v5 Support (Historical Scope) Status: Implemented; retained as the v5 compatibility reference Research date: May 31, 2026 Original target API: Apple Ads Campaign Management API 5.5 Historical command root: `asc ads` Release target: 4.4.0 Current API addition: Apple Ads Platform API v1 Preferred API in CLI 4.4.0: Apple Ads Platform API v1 under direct `asc ads` ## Historical goal This note records the existing Campaign Management API v5 surface and the 4.4.0 addition of the Apple Ads Platform API v1 surface. Direct `asc ads` resource commands use v1. The incompatible, retiring v5 surface moves under `asc ads v5`; the intermediate nested prototype is removed before merge. Users should not need a raw HTTP client for supported workflows. The v1 and v5 request contracts remain separate because they use different hosts, contexts, payloads, and response envelopes. Existing v5 commands move under the explicit `v5` group instead of being silently retargeted. The original implementation preserved the local CLI style: - `ffcli` commands with `shared.DefaultUsageFunc` - explicit long flags - TTY-aware output defaults through `shared.BindOutputFlags` - non-interactive behavior only - `--confirm` for deletes and bulk deletes - `--paginate` for multi-page list/search commands - JSON output that preserves Apple response envelopes for agents - no new third-party dependencies ## Historical v5 sources Apple sources used for the May 2026 v5 implementation: - Apple Ads root: https://developer.apple.com/documentation/apple_ads - Platform API v1: https://developer.apple.com/documentation/apple-ads-platform-api - OAuth: https://developer.apple.com/documentation/apple_ads/implementing-oauth-for-the-apple-search-ads-api - Calling the API: https://developer.apple.com/documentation/apple_ads/calling-the-apple-search-ads-api - API functionality: https://developer.apple.com/documentation/apple_ads/using-apple-search-ads-api-functionality - API 5 changelog: https://developer.apple.com/documentation/apple_ads/apple-search-ads-campaign-management-api-5 At the original research date, API 5 was Apple's current Campaign Management API and API 5.5 had been released in February 2026. Apple later made Platform API v1 available. CLI 4.4.0 implements that API under direct `asc ads` resource paths and keeps the original v5 commands under `asc ads v5` as warning-producing compatibility paths until Apple's January 26, 2027 retirement date. At the research date, deprecated `Creative Sets` were not included as commands because Apple's v5 documentation marked the collection as deprecated and exposed no active v5 endpoint under that page. The `includeDeletedCreativeSetAssets` query parameter on `GET /v5/creatives/{creativeId}` was included. AdServices Attribution API is out of scope. It is not part of the Apple Ads Campaign Management API command surface and has different caller requirements. ## Platform API v1 in 4.4.0 The cumulative 4.4.0 implementation stack is intended to add all 99 documented operations under direct `asc ads` resource paths: this foundation layer registers 13 operations, the campaign layer adds 41, Maps and assets add 21, and reports and optimization add 24. At each intermediate PR, only the operations registered by that layer are available; the cumulative stack is what provides the complete surface. Ad-account context is carried in `X-AP-Context: adAccountId=;` for account-scoped requests. The `--ad-account` flag, `ASC_ADS_AD_ACCOUNT_ID`, and the selected profile's `ad_account_id` are independent of the legacy v5 `--org`/`org_id` context. The `/v1/ad-accounts` collection is method-dependent: | Operation | Context behavior | | --- | --- | | `POST /v1/ad-accounts` | Creates an account without `X-AP-Context`; the account context does not exist until the response supplies its ID. | | `GET /v1/ad-accounts/{id}` | Requires `X-AP-Context: adAccountId=;`; the header account must match the path ID. | | `PUT /v1/ad-accounts/{id}` | Requires `X-AP-Context: adAccountId=;`; the header account must match the path ID. | Authentication commands use the Platform API v1 transport: | Command | Validation/discovery request | | --- | --- | | `asc ads auth login --network` | OAuth token exchange, then Platform API v1 `GET /v1/me`. | | `asc ads auth status --validate` | For each stored credential, OAuth token exchange, then Platform API v1 `GET /v1/me`. | | `asc ads auth discover` | Platform API v1 `GET /v1/me` and `GET /v1/acls`, returning user and ad-account access. | All three commands avoid an ad-account context for these requests. The token exchange remains `POST https://appleid.apple.com/auth/oauth2/token` with the `searchadsorg` scope; a supplied access token skips that exchange. Platform API v1 users should call `asc ads me view` and `asc ads acls list` when they need the v1 user or ACL resources. ## Historical v5 command placement The original implementation added this command root; in 4.4.0 the historical v5 leaves move beneath an explicit version group: ```text asc ads v5 [flags] ``` Common endpoint flags: - Every Apple Ads endpoint leaf command accepts `--ads-profile NAME`. - Every org-scoped endpoint leaf command accepts `--org ORG_ID`. - `asc ads v5 me view`, `asc ads v5 acls list`, and `asc ads auth ...` do not require `--org`. - Examples in the endpoint matrix omit `--org` and `--ads-profile` unless those flags are materially relevant to the endpoint. The implementation still adds them to the command. - Resource groups with a natural list endpoint execute list by default: `asc ads v5 campaigns`, `asc ads v5 budget-orders`, `asc ads v5 ad-groups`, `asc ads v5 creatives`, and `asc ads v5 impression-share-reports`. These rules describe the deprecated v5 tree. Platform v1 commands live under direct `asc ads`, use `--ad-account` instead of `--org`, and follow the v1 payload and response contracts documented in `docs/design/apple-ads-platform-api-v1.md`. Root help placement: - Add `ads` to `cmd/root_usage.go` under `ANALYTICS & FINANCE COMMANDS` after `performance`. This is acquisition/marketing rather than App Store Connect resource management, but this is the closest existing command group. - Register `ads.AdsCommand()` in `internal/cli/registry/registry.go`. - Add `ads` to registry/root help tests. Package layout: ```text internal/appleads/ auth.go auth_store.go client.go client_test.go endpoints.go endpoints_test.go errors.go pagination.go internal/cli/ads/ api.go root.go auth.go endpoints.go endpoints_test.go resolve.go ``` Use `internal/appleads`, not `internal/asc`, because the base URL, auth flow, error envelope, and pagination model are different from App Store Connect. Do not implement this as 73 bespoke command functions and 73 bespoke client methods. Add one `EndpointSpec` table in `internal/appleads/endpoints.go` and generic command/client builders that consume it. `EndpointSpec` must include: ```go type EndpointSpec struct { Name string Method string Path string CommandPath []string BodyKind shared.JSONPayloadKind BodyType string ResponseType string RequiresOrg bool RequiresConfirm bool PathParams []ParamSpec QueryParams []ParamSpec SupportsPaginate bool DefaultListAlias bool } type ParamSpec struct { Name string Flag string Type ParamType Required bool Max int Allowed []string } ``` Acceptance check: adding a newly documented Apple Ads endpoint requires one spec row plus docs/tests, not a new hand-written request method. ## Authentication Contract Apple Ads OAuth is separate from App Store Connect JWT auth. OAuth facts from Apple: - Token URL: `https://appleid.apple.com/auth/oauth2/token` - Grant type: `client_credentials` - Scope: `searchadsorg` - Client secret: ES256 JWT - JWT header: `alg=ES256`, `kid=` - JWT claims: - `iss=` - `iat=` - `exp=` - `aud=https://appleid.apple.com` - `sub=` - Token response contains `access_token`, `token_type=Bearer`, `expires_in=3600`, and `scope=searchadsorg`. - Generate the client secret on demand for each token refresh with a 10-minute lifetime and 30-second refresh skew. Do not persist the client secret. - Send the token request as `application/x-www-form-urlencoded` form values: `grant_type`, `client_id`, `client_secret`, and `scope`. Implement this command group: ```text asc ads auth login --name NAME --client-id CLIENT_ID --team-id TEAM_ID --key-id KEY_ID --private-key PATH [--org ORG_ID] [--ad-account AD_ACCOUNT_ID] [--network] [--skip-validation] [--bypass-keychain] [--local] asc ads auth status [--verbose] [--validate] [--output table|json] asc ads auth discover [--ads-profile NAME] [--org ORG_ID] [--ad-account AD_ACCOUNT_ID] [--output table|json] asc ads auth switch --name NAME asc ads auth token --confirm [--output text|json] asc ads auth doctor [--output text|json] asc ads auth logout [--all --confirm | --name NAME] ``` Mirror the existing `asc auth` behavior: - `--name`, `--key-id`, and `--private-key` use the same meaning and validation style as `asc auth login`. - `--client-id` is Apple Ads OAuth `client_id`. - `--team-id` is the JWT issuer (`iss`). - `--org` stores a default Apple Ads org ID for API calls. - `--ad-account` stores a default Platform API v1 ad-account ID and, on `auth discover`, marks that account as active in the output. It is separate from `--org`. - `--private-key` accepts the EC P-256 PEM Apple documents for Ads. Reuse the existing private-key parsing helpers because they already support ES256 keys. - `--network` requests an access token and calls Platform API v1 `GET /v1/me`. - `--skip-validation` skips JWT and network validation. - `--network` and `--skip-validation` are mutually exclusive. - `--local` requires keychain bypass, exactly like `asc auth login`. - Keychain is preferred; config fallback is allowed when bypassing keychain. - `auth status` supports `--verbose` and `--validate`, matching `asc auth status`. - `auth status --validate` validates each stored credential through Platform API v1 `GET /v1/me` and reports failures after rendering the status output. - `auth discover` calls Platform API v1 `/v1/me` and `/v1/acls` to show the active Ads user and available ad accounts without printing access tokens. Each ACL row includes its honest `ad_account_id`; `--ad-account` controls which row is marked active. - Platform API v1 user and ACL discovery is also available directly through `asc ads me view` and `asc ads acls list`. - `auth logout` supports `--all` and `--name`. It requires one of those flags so bare `asc ads auth logout` does not clear every stored Ads profile, and `--all` requires `--confirm`. Add config fields without changing existing App Store Connect credentials: ```go type AdsCredential struct { Name string `json:"name"` ClientID string `json:"client_id"` TeamID string `json:"team_id"` KeyID string `json:"key_id"` PrivateKeyPath string `json:"private_key_path"` OrgID string `json:"org_id,omitempty"` AdAccountID string `json:"ad_account_id,omitempty"` } type AdsKeychainMetadata struct { Name string `json:"name"` ClientID string `json:"client_id"` TeamID string `json:"team_id"` KeyID string `json:"key_id"` OrgID string `json:"org_id,omitempty"` AdAccountID string `json:"ad_account_id,omitempty"` ModifiedAt string `json:"modified_at,omitempty"` } type AdsConfig struct { DefaultKeyName string `json:"default_key_name,omitempty"` Keys []AdsCredential `json:"keys,omitempty"` KeychainMetadata []AdsKeychainMetadata `json:"keychain_metadata,omitempty"` OrgID string `json:"org_id,omitempty"` AdAccountID string `json:"ad_account_id,omitempty"` } type Config struct { // existing fields... Ads AdsConfig `json:"ads,omitempty"` } ``` Do not reuse the existing App Store Connect keychain item prefixes. Ads storage uses: ```text asc:ads-credential: asc:ads-metadata: ``` Reuse only private-key parsing/validation helpers from `internal/auth`. Environment variables: ```text ASC_ADS_ACCESS_TOKEN ASC_ADS_CLIENT_ID ASC_ADS_TEAM_ID ASC_ADS_KEY_ID ASC_ADS_PRIVATE_KEY_PATH ASC_ADS_PRIVATE_KEY ASC_ADS_PRIVATE_KEY_B64 ASC_ADS_ORG_ID ASC_ADS_AD_ACCOUNT_ID ASC_ADS_PROFILE ASC_ADS_STRICT_AUTH ASC_ADS_BYPASS_KEYCHAIN ``` Resolution order: 1. Explicit `--ads-profile` if present. 2. `ASC_ADS_PROFILE` if present. 3. `ASC_ADS_ACCESS_TOKEN` if no profile is selected; it bypasses token exchange but still needs `--org`, `ASC_ADS_ORG_ID`, or stored profile org for org-scoped calls. 4. Complete Ads env credential tuple. 5. Default Ads keychain/config profile. If `ASC_ADS_STRICT_AUTH` is true, fail when Ads credentials are split across multiple sources, matching the existing strict-auth principle. If a profile is selected by `--ads-profile` or `ASC_ADS_PROFILE`, ignore `ASC_ADS_ACCESS_TOKEN` unless `ASC_ADS_STRICT_AUTH` is true; in strict mode, selected profile plus access token is a mixed-source error. Org ID resolution is independent from token resolution: 1. `--org` 2. `ASC_ADS_ORG_ID` 3. selected Ads profile `org_id` 4. `ads.org_id` in config only when authentication is profile-less Persist the org ID both on the selected credential and in `ads.org_id` when Ads login receives an org ID. This lets `ASC_ADS_ACCESS_TOKEN` users reuse a configured default org without storing Ads private key material in the active environment. A named profile never inherits either root Ads context when its own profile omits the value. Switching or removing the default clears both root context values. Profile-less access-token or environment authentication can still use the standalone root values. Platform API v1 ad-account resolution is independent from org resolution: 1. `--ad-account` 2. `ASC_ADS_AD_ACCOUNT_ID` 3. selected Ads profile `ad_account_id` 4. `ads.ad_account_id` in config when authentication does not select a named profile Persist an ad-account ID supplied to Ads login alongside the selected profile and root config. A named profile never inherits the root ad-account ID from another profile. ## Campaign Management API v5 HTTP Client Contract This section applies only to the deprecated Campaign Management API v5 commands under `asc ads v5`. Platform API v1 has a separate transport contract below. Base URL: ```text https://api.searchads.apple.com/api/ ``` Headers: ```text Authorization: Bearer Accept: application/json Content-Type: application/json X-AP-Context: orgId= ``` Do not send `X-AP-Context` for these two endpoints: - `GET /v5/acls` - `GET /v5/me` For all other endpoints, resolve org ID from `--org`, `ASC_ADS_ORG_ID`, the selected Ads profile, or `ads.org_id` in config. If it is still empty, fail with: ```text Error: --org is required (or set ASC_ADS_ORG_ID or an Ads profile org_id) ``` Use `shared.ContextWithTimeout` for all Apple Ads API requests so `ASC_TIMEOUT` continues to apply. Use the existing retry/backoff approach for GET and HEAD requests and for 429 responses. Redact `Authorization`, `client_secret`, private keys, and access tokens in `ASC_DEBUG=api` logging. Apple Ads error envelope: ```json { "error": { "errors": [ { "field": "name", "message": "message", "messageCode": "CODE" } ] } } ``` Add `appleads.APIError` with HTTP status, field, message, and messageCode. If the response is not this envelope, sanitize and return the raw detail the same way `internal/asc` does. ## Platform API v1 HTTP Client Contract Base URL: ```text https://api.ads.apple.com/v1/ ``` Platform API v1 uses the same bearer authorization and JSON content headers as v5. Account-scoped requests add: ```text X-AP-Context: adAccountId=; ``` The ad-account ID is resolved independently from the legacy organization ID. `--ad-account`, `ASC_ADS_AD_ACCOUNT_ID`, the selected profile's `ad_account_id`, and root `ads.ad_account_id` are the supported sources. The CLI rejects control characters and semicolons before authentication or network work so an ID cannot inject additional context fields. The following requests are context-free: `GET /v1/me`, `GET /v1/acls`, `GET /v1/orgs/{id}`, `GET /v1/advertiser-resources`, and `POST /v1/ad-accounts`. For `GET` and `PUT /v1/ad-accounts/{id}`, the context account must match the path ID. Other endpoint context requirements are declared by the v1 endpoint metadata. Platform API v1 retains the shared timeout, retry, rate-limit, pagination, and sanitized error handling behavior of the client, while selecting the v1 base URL and endpoint-specific response envelope. ## Payload Contract Every Apple Ads endpoint with an HTTP body uses `--file`. Reason: Apple Ads request objects are large, nested, API-specific JSON payloads. Using `--file` is already established in this repo for complex JSON payloads such as Xcode Cloud workflows. It also preserves full API support without mapping every Apple Ads nested object into unstable CLI flags. Rules: - The CLI sends the file content as the HTTP body without wrapping it. - Object endpoints require a JSON object file. - Array endpoints require a JSON array file. - `(CustomProductPageCreative | DefaultProductPageCreative)` accepts a JSON object file. - `[int64]` delete-bulk endpoints accept a JSON array of numbers. - Do not implement `--file -` in this PR. Extend `internal/cli/shared/json_payload.go`: ```go type JSONPayloadKind string const ( JSONPayloadObject JSONPayloadKind = "object" JSONPayloadArray JSONPayloadKind = "array" JSONPayloadAny JSONPayloadKind = "any" ) func ReadJSONFilePayloadKind(path string, kind JSONPayloadKind) (json.RawMessage, error) ``` Keep `ReadJSONFilePayload(path)` as the object-only compatibility wrapper. Required errors: ```text payload path must be a file payload file is empty invalid JSON: ... payload must be a JSON object payload must be a JSON array ``` ## Pagination Contract Apple Ads uses offset pagination, not App Store Connect `links.next`. List/search commands with `limit`/`offset` support: ```text --limit INT --offset INT --paginate ``` Do not add `--next` to Apple Ads commands. Validate: - `--limit` must be `1..1000` except `GET v5/custom-reports`, where it must be `1..50`. - `--offset` must be `>=0`. - `--paginate` starts at `--offset` when provided. - `--paginate` uses page size `--limit` when provided, otherwise `1000` except `GET v5/custom-reports`, where the default page size is `50`. - Continue until `pagination.startIndex + pagination.itemsPerPage >= pagination.totalResults` or a page returns no data. Do not add `--paginate` to `find` or report endpoints that carry pagination inside a `Selector` or `ReportingRequest` JSON body. Those endpoints keep pagination fully manual in the payload file. The CLI must not mutate payload files or rewrite body JSON to advance selector pagination in this PR. The generic paginated response shape must preserve the Apple envelope: ```json { "data": [], "pagination": { "itemsPerPage": 1000, "startIndex": 0, "totalResults": 0 } } ``` ## Output Contract All endpoint commands bind: ```go output := shared.BindOutputFlags(fs) ``` Return Apple Ads response JSON exactly as Apple returns it, including `data`, `pagination`, and `error` envelopes. JSON is the canonical agent output. Use the existing output-registry fallback for Apple Ads raw response types: `table` and `markdown` print the same JSON envelope as `json` until a dedicated renderer is added in a later PR. Do not add custom Apple Ads table/markdown renderers in this PR. Represent successful Apple Ads responses as `json.RawMessage` or a dedicated raw envelope type that is not registered with the output registry. ## Historical v5 endpoint-to-command matrix This matrix records the 100% v5 coverage implemented from Apple's May 2026 documentation. Every row remains a runnable deprecated compatibility command; it is not the preferred Platform v1 command inventory. | CLI command | HTTP endpoint | Body | Notes | | --- | --- | --- | --- | | `asc ads v5 acls list` | `GET v5/acls` | none | No `--org` header. | | `asc ads v5 me view` | `GET v5/me` | none | No `--org` header. | | `asc ads v5 apps search --query QUERY [--limit N --offset N --paginate --return-owned-apps]` | `GET v5/search/apps` | none | `query` is required. | | `asc ads v5 apps view --adam-id ADAM_ID` | `GET v5/apps/{adamId}` | none | `adamId` is int64. | | `asc ads v5 apps localized-details --adam-id ADAM_ID` | `GET v5/apps/{adamId}/locale-details` | none | | | `asc ads v5 apps eligibility find --adam-id ADAM_ID --file selector.json` | `POST v5/apps/{adamId}/eligibilities/find` | `Selector` object | | | `asc ads v5 apps assets find --adam-id ADAM_ID --file selector.json` | `POST v5/apps/{adamId}/assets/find` | `Selector` object | | | `asc ads v5 product-pages list --adam-id ADAM_ID [--name NAME --states STATES]` | `GET v5/apps/{adamId}/product-pages` | none | Forward `states` as raw query string. | | `asc ads v5 product-pages view --adam-id ADAM_ID --product-page PRODUCT_PAGE_ID` | `GET v5/apps/{adamId}/product-pages/{productPageId}` | none | | | `asc ads v5 product-pages locales list --adam-id ADAM_ID --product-page PRODUCT_PAGE_ID [--device-classes VALUE --language-codes VALUE --languages VALUE --expand]` | `GET v5/apps/{adamId}/product-pages/{productPageId}/locale-details` | none | Forward query strings exactly. | | `asc ads v5 product-pages countries list [--countries-or-regions VALUE]` | `GET v5/countries-or-regions` | none | | | `asc ads v5 product-pages devices list` | `GET v5/creativeappmappings/devices` | none | | | `asc ads v5 budget-orders list [--limit N --offset N --paginate]` | `GET v5/budgetorders` | none | | | `asc ads v5 budget-orders create --file budget-order-create.json --confirm` | `POST v5/budgetorders` | `BudgetOrderCreate` object | Require `--confirm`. | | `asc ads v5 budget-orders view --budget-order BUDGET_ORDER_ID` | `GET v5/budgetorders/{boId}` | none | | | `asc ads v5 budget-orders update --budget-order BUDGET_ORDER_ID --file budget-order-update.json --confirm` | `PUT v5/budgetorders/{boId}` | `BudgetOrderUpdate` object | Require `--confirm`. | | `asc ads v5 campaigns list [--limit N --offset N --paginate]` | `GET v5/campaigns` | none | `asc ads v5 campaigns` aliases list. | | `asc ads v5 campaigns find --file selector.json` | `POST v5/campaigns/find` | `Selector` object | | | `asc ads v5 campaigns view --campaign CAMPAIGN_ID` | `GET v5/campaigns/{campaignId}` | none | | | `asc ads v5 campaigns create --file campaign.json --confirm` | `POST v5/campaigns` | `Campaign` object | Require `--confirm`. | | `asc ads v5 campaigns update --campaign CAMPAIGN_ID --file campaign-update.json --confirm` | `PUT v5/campaigns/{campaignId}` | `UpdateCampaignRequest` object | Require `--confirm`; campaign update uses Apple's campaign envelope. | | `asc ads v5 campaigns delete --campaign CAMPAIGN_ID --confirm` | `DELETE v5/campaigns/{campaignId}` | none | Require `--confirm`. | | `asc ads v5 ad-groups list --campaign CAMPAIGN_ID [--limit N --offset N --paginate]` | `GET v5/campaigns/{campaignId}/adgroups` | none | `asc ads v5 ad-groups` aliases list. | | `asc ads v5 ad-groups find --campaign CAMPAIGN_ID --file selector.json` | `POST v5/campaigns/{campaignId}/adgroups/find` | `Selector` object | | | `asc ads v5 ad-groups find-org --file selector.json` | `POST v5/adgroups/find` | `Selector` object | Org-level find. | | `asc ads v5 ad-groups view --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID` | `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}` | none | | | `asc ads v5 ad-groups create --campaign CAMPAIGN_ID --file ad-group.json --confirm` | `POST v5/campaigns/{campaignId}/adgroups` | `AdGroup` object | Require `--confirm`. | | `asc ads v5 ad-groups update --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file ad-group-update.json --confirm` | `PUT v5/campaigns/{campaignId}/adgroups/{adgroupId}` | `AdGroupUpdate` object | Require `--confirm`. | | `asc ads v5 ad-groups delete --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --confirm` | `DELETE v5/campaigns/{campaignId}/adgroups/{adgroupId}` | none | Require `--confirm`. | | `asc ads v5 ads list --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID` | `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}/ads` | none | | | `asc ads v5 ads find --campaign CAMPAIGN_ID --file selector.json` | `POST v5/campaigns/{campaignId}/ads/find` | `Selector` object | Campaign-level find. | | `asc ads v5 ads find-org --file selector.json` | `POST v5/ads/find` | `Selector` object | Org-level find. | | `asc ads v5 ads view --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --ad AD_ID` | `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}/ads/{adId}` | none | | | `asc ads v5 ads create --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file ad-create.json --confirm` | `POST v5/campaigns/{campaignId}/adgroups/{adgroupId}/ads` | `AdCreate` object | Require `--confirm`. | | `asc ads v5 ads update --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --ad AD_ID --file ad-update.json --confirm` | `PUT v5/campaigns/{campaignId}/adgroups/{adgroupId}/ads/{adId}` | `AdUpdate` object | Require `--confirm`. | | `asc ads v5 ads delete --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --ad AD_ID --confirm` | `DELETE v5/campaigns/{campaignId}/adgroups/{adgroupId}/ads/{adId}` | none | Require `--confirm`. | | `asc ads v5 targeting-keywords list --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID [--limit N --offset N --paginate]` | `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}/targetingkeywords` | none | | | `asc ads v5 targeting-keywords find --campaign CAMPAIGN_ID --file selector.json` | `POST v5/campaigns/{campaignId}/adgroups/targetingkeywords/find` | `Selector` object | Campaign-level find across ad groups. | | `asc ads v5 targeting-keywords view --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --keyword KEYWORD_ID` | `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}/targetingkeywords/{keywordId}` | none | | | `asc ads v5 targeting-keywords create-bulk --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file keywords.json --confirm` | `POST v5/campaigns/{campaignId}/adgroups/{adgroupId}/targetingkeywords/bulk` | `[Keyword]` array | Require `--confirm`. | | `asc ads v5 targeting-keywords update-bulk --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file keywords-update.json --confirm` | `PUT v5/campaigns/{campaignId}/adgroups/{adgroupId}/targetingkeywords/bulk` | `[KeywordUpdateRequest]` array | Require `--confirm`. | | `asc ads v5 targeting-keywords delete --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --keyword KEYWORD_ID --confirm` | `DELETE v5/campaigns/{campaignId}/adgroups/{adgroupId}/targetingkeywords/{keywordId}` | none | Require `--confirm`. | | `asc ads v5 targeting-keywords delete-bulk --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file keyword-ids.json --confirm` | `POST v5/campaigns/{campaignId}/adgroups/{adgroupId}/targetingkeywords/delete/bulk` | `[int64]` array | Require `--confirm`. | | `asc ads v5 campaign-negative-keywords list --campaign CAMPAIGN_ID [--limit N --offset N --paginate]` | `GET v5/campaigns/{campaignId}/negativekeywords` | none | | | `asc ads v5 campaign-negative-keywords find --campaign CAMPAIGN_ID --file selector.json` | `POST v5/campaigns/{campaignId}/negativekeywords/find` | `Selector` object | | | `asc ads v5 campaign-negative-keywords view --campaign CAMPAIGN_ID --negative-keyword KEYWORD_ID` | `GET v5/campaigns/{campaignId}/negativekeywords/{keywordId}` | none | | | `asc ads v5 campaign-negative-keywords create-bulk --campaign CAMPAIGN_ID --file negative-keywords.json --confirm` | `POST v5/campaigns/{campaignId}/negativekeywords/bulk` | `[NegativeKeyword]` array | Require `--confirm`. | | `asc ads v5 campaign-negative-keywords update-bulk --campaign CAMPAIGN_ID --file negative-keywords.json --confirm` | `PUT v5/campaigns/{campaignId}/negativekeywords/bulk` | `[NegativeKeyword]` array | Require `--confirm`. | | `asc ads v5 campaign-negative-keywords delete-bulk --campaign CAMPAIGN_ID --file keyword-ids.json --confirm` | `POST v5/campaigns/{campaignId}/negativekeywords/delete/bulk` | `[int64]` array | Require `--confirm`. | | `asc ads v5 ad-group-negative-keywords list --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID [--limit N --offset N --paginate]` | `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}/negativekeywords` | none | | | `asc ads v5 ad-group-negative-keywords find --campaign CAMPAIGN_ID --file selector.json` | `POST v5/campaigns/{campaignId}/adgroups/negativekeywords/find` | `Selector` object | Campaign-level find across ad groups. | | `asc ads v5 ad-group-negative-keywords view --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --negative-keyword KEYWORD_ID` | `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}/negativekeywords/{keywordId}` | none | | | `asc ads v5 ad-group-negative-keywords create-bulk --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file negative-keywords.json --confirm` | `POST v5/campaigns/{campaignId}/adgroups/{adgroupId}/negativekeywords/bulk` | `[NegativeKeyword]` array | Require `--confirm`. | | `asc ads v5 ad-group-negative-keywords update-bulk --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file negative-keywords.json --confirm` | `PUT v5/campaigns/{campaignId}/adgroups/{adgroupId}/negativekeywords/bulk` | `[NegativeKeyword]` array | Require `--confirm`. | | `asc ads v5 ad-group-negative-keywords delete-bulk --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file keyword-ids.json --confirm` | `POST v5/campaigns/{campaignId}/adgroups/{adgroupId}/negativekeywords/delete/bulk` | `[int64]` array | Require `--confirm`. | | `asc ads v5 geo search [--country-code CC --entity ENTITY --query QUERY --limit N --offset N --paginate]` | `GET v5/search/geo` | none | API default query is `*:*`. | | `asc ads v5 geo resolve --file geo-requests.json [--limit N --offset N --paginate]` | `POST v5/search/geo` | `[GeoRequest]` array | | | `asc ads v5 creatives list [--limit N --offset N --paginate]` | `GET v5/creatives` | none | | | `asc ads v5 creatives find --file selector.json` | `POST v5/creatives/find` | `Selector` object | | | `asc ads v5 creatives view --creative CREATIVE_ID [--include-deleted-creative-set-assets]` | `GET v5/creatives/{creativeId}` | none | Include deprecated creative set assets query. | | `asc ads v5 creatives create --file creative.json` | `POST v5/creatives` | `CustomProductPageCreative` or `DefaultProductPageCreative` object | | | `asc ads v5 rejection-reasons find --file selector.json` | `POST v5/product-page-reasons/find` | `Selector` object | | | `asc ads v5 rejection-reasons view --reason PRODUCT_PAGE_REASON_ID` | `GET v5/product-page-reasons/{productPageReasonId}` | none | | | `asc ads v5 reports campaigns --file reporting-request.json` | `POST v5/reports/campaigns` | `ReportingRequest` object | | | `asc ads v5 reports ad-groups --campaign CAMPAIGN_ID --file reporting-request.json` | `POST v5/reports/campaigns/{campaignId}/adgroups` | `ReportingRequest` object | | | `asc ads v5 reports keywords --campaign CAMPAIGN_ID --file reporting-request.json` | `POST v5/reports/campaigns/{campaignId}/keywords` | `ReportingRequest` object | | | `asc ads v5 reports search-terms --campaign CAMPAIGN_ID --file reporting-request.json` | `POST v5/reports/campaigns/{campaignId}/searchterms` | `ReportingRequest` object | | | `asc ads v5 reports ads --campaign CAMPAIGN_ID --file reporting-request.json` | `POST v5/reports/campaigns/{campaignId}/ads` | `ReportingRequest` object | | | `asc ads v5 reports ad-group-keywords --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file reporting-request.json` | `POST v5/reports/campaigns/{campaignId}/adgroups/{adgroupId}/keywords` | `ReportingRequest` object | | | `asc ads v5 reports ad-group-search-terms --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file reporting-request.json` | `POST v5/reports/campaigns/{campaignId}/adgroups/{adgroupId}/searchterms` | `ReportingRequest` object | | | `asc ads v5 impression-share-reports list [--field FIELD --sort-order ORDER --limit N --offset N --paginate]` | `GET v5/custom-reports` | none | | | `asc ads v5 impression-share-reports create --file custom-report-request.json` | `POST v5/custom-reports` | `CustomReportRequest` object | | | `asc ads v5 impression-share-reports view --report REPORT_ID` | `GET v5/custom-reports/{reportId}` | none | | The `EndpointSpec` query parameter metadata matched Apple's v5 documentation at the research date for every row. Required query parameters were: | HTTP endpoint | Query flags | | --- | --- | | `GET v5/search/apps` | `--query` required, `--limit`, `--offset`, `--return-owned-apps` | | `GET v5/search/geo` | `--country-code`, `--entity`, `--query`, `--limit`, `--offset` | | `POST v5/search/geo` | `--limit`, `--offset` | | `GET v5/campaigns` | `--limit`, `--offset` | | `GET v5/budgetorders` | `--limit`, `--offset` | | `GET v5/campaigns/{campaignId}/adgroups` | `--limit`, `--offset` | | `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}/targetingkeywords` | `--limit`, `--offset` | | `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}/negativekeywords` | `--limit`, `--offset` | | `GET v5/campaigns/{campaignId}/negativekeywords` | `--limit`, `--offset` | | `GET v5/creatives` | `--limit`, `--offset` | | `GET v5/custom-reports` | `--field`, `--sort-order`, `--limit`, `--offset` | | `GET v5/apps/{adamId}/product-pages` | `--name`, `--states` | | `GET v5/apps/{adamId}/product-pages/{productPageId}/locale-details` | `--device-classes`, `--expand`, `--language-codes`, `--languages` | | `GET v5/countries-or-regions` | `--countries-or-regions` | | `GET v5/creatives/{creativeId}` | `--include-deleted-creative-set-assets` | Apple's general partial-fetch `fields` query parameter was not exposed in the original v5 implementation because those endpoint pages did not list endpoint-specific `fields[...]` query parameters. Add it only if the compatibility implementation extracts a documented endpoint-specific query parameter from Apple docs. Add this debug/forward-compatibility command after the named endpoints: ```text asc ads v5 api request --method METHOD --path v5/... [--file payload.json] [--org ORG_ID] [--confirm] ``` Rules: - `--path` accepts only relative `v5/...` paths or full URLs under `https://api.searchads.apple.com/api/`. - Full URLs for other hosts are rejected. - `DELETE` requires `--confirm`. - This command is not a substitute for any named endpoint above; all rows above must still exist. ## Request Body Reference The following body types are accepted through `--file` and must be validated only for JSON shape, not for every semantic field. Semantic validation belongs to Apple Ads. | Body type | JSON shape | Required top-level fields from Apple docs | | --- | --- | --- | | `Selector` | object | none; common fields are `conditions`, `fields`, `orderBy`, `pagination` | | `Condition` | object | none; common fields are `field`, `operator`, `values`, `ignoreCase` | | `BudgetOrderCreate` | object | none documented as top-level required | | `BudgetOrderUpdate` | object | none documented as top-level required | | `Campaign` | object | `adamId`, `adChannelType`, `billingEvent`, `countriesOrRegions`, `dailyBudgetAmount`, `name`, `supplySources` | | `UpdateCampaignRequest` | object | none documented as top-level required; payload uses `campaign` envelope | | `AdGroup` | object | `defaultBidAmount`, `name`, `pricingModel`, `startTime` | | `AdGroupUpdate` | object | none documented as top-level required | | `Keyword` | object array item | `bidAmount`, `matchType`, `text` | | `KeywordUpdateRequest` | object array item | `matchType`, `text` | | `NegativeKeyword` | object array item | `matchType`, `text` | | `AdCreate` | object | `creativeId`, `name`, `status` | | `AdUpdate` | object | none documented as top-level required | | `CustomProductPageCreative` | object | `productPageId`, `adamId`, `name`, `type` | | `DefaultProductPageCreative` | object | `adamId`, `name`, `type` | | `ReportingRequest` | object | `startTime`, `endTime`, `selector` | | `CustomReportRequest` | object | `name` | | `GeoRequest` | object array item | `entity`, `id` | | `[int64]` | array | integer IDs | Payload examples: ```json { "conditions": [ { "field": "campaignId", "operator": "EQUALS", "values": ["1234567890"] } ], "pagination": { "limit": 1000, "offset": 0 } } ``` ```json { "adamId": 123456789, "adChannelType": "SEARCH", "billingEvent": "TAPS", "countriesOrRegions": ["US"], "dailyBudgetAmount": { "amount": "25", "currency": "USD" }, "name": "US Search Campaign", "status": "ENABLED", "supplySources": ["APPSTORE_SEARCH_RESULTS"] } ``` ```json [ { "text": "example keyword", "matchType": "BROAD", "bidAmount": { "amount": "1.25", "currency": "USD" }, "status": "ACTIVE" } ] ``` ```json { "startTime": "2026-05-01", "endTime": "2026-05-31", "granularity": "DAILY", "selector": { "pagination": { "limit": 1000, "offset": 0 } }, "timeZone": "UTC" } ``` ## Implementation Sequence The PR implemented the work in this order: 1. Added `internal/appleads` auth, token, client, error, and pagination tests. 2. Extended `shared.ReadJSONFilePayload` with array/object/any shape support while preserving the existing object-only wrapper. 3. Added `internal/cli/ads` root, auth commands, and registry/root help wiring. 4. Added read-only endpoints first: `me`, `acls`, app search/details, product pages, countries, devices, list/view commands. 5. Added request-body commands with `--file` and shape validation. 6. Added deletes and bulk deletes with `--confirm`. 7. Added reports and impression-share reports. 8. Added `ads api request`. 9. Added command docs and generated command docs. Do not add ergonomic create/update flags in the first PR. They would duplicate large Apple request schemas and weaken the guarantee that every API field can be used by agents immediately. Ergonomic wrappers can be a later additive PR. ## Test Plan Use TDD for implementation. Start with failing tests. Required unit tests: - Apple Ads client secret JWT includes `alg=ES256`, `kid`, `iss`, `iat`, `exp`, `aud=https://appleid.apple.com`, and `sub=`. - Token request uses `POST https://appleid.apple.com/auth/oauth2/token` with `grant_type=client_credentials`, `client_id`, `client_secret`, and `scope=searchadsorg`. - Access token cache refreshes before expiry. - `ASC_ADS_ACCESS_TOKEN` bypasses token exchange. - `X-AP-Context` is present for org-scoped endpoints and absent for `me`/`acls`. - Missing org fails before auth resolution. - Apple Ads error envelopes parse into `appleads.APIError`. - Offset pagination aggregates `data` and stops at `totalResults`. - JSON payload helper accepts object, array, and any modes; rejects wrong shapes. Required CLI tests under `internal/cli/cmdtest`: - Every command in the matrix is registered and has help output. - Every leaf command validates missing required flags with exit code `2` and a concrete stderr message. - Every command with `--file` rejects missing file, empty file, invalid JSON, and wrong object/array shape. - Every delete and bulk delete rejects missing `--confirm` with exit code `2`. - Every list/search command rejects invalid `--limit`, invalid `--offset`, and invalid boolean flags. - Query parameters are encoded exactly as documented. - Body payloads are forwarded byte-for-byte except for JSON validation. - `--output json` returns parseable JSON for representative success responses. - `--pretty` is accepted only for JSON. - `--paginate` makes multiple requests with increasing `offset`. - `ads api request` rejects non-Apple Ads hosts and requires `--confirm` for `DELETE`. Required client tests: - One table-driven test row for every `EndpointSpec` row that asserts method, path, path variables, query variables, body shape, and response passthrough. - One generated inventory test that compares each `EndpointSpec` path, method, body kind, and query parameter names against the Apple docs snapshot used in the test fixture. - 401, 403, 404, 429, and 500 response handling. - Token endpoint errors do not print secrets. Black-box verification: ```bash go build -o /tmp/asc . ASC_BYPASS_KEYCHAIN=1 /tmp/asc ads --help ASC_BYPASS_KEYCHAIN=1 /tmp/asc ads v5 campaigns list --org 123 --output json ASC_BYPASS_KEYCHAIN=1 /tmp/asc ads v5 campaigns delete --campaign 1 --org 123 ASC_BYPASS_KEYCHAIN=1 /tmp/asc ads v5 campaigns delete --campaign 1 --org 123 --confirm ``` Repository checks before PR: ```bash make format make check-command-docs make lint ASC_BYPASS_KEYCHAIN=1 make test ``` Live Apple Ads smoke tests are not required for PR completion when Ads credentials are unavailable. When Ads credentials are configured locally, run only these read-only smoke tests: ```bash ASC_BYPASS_KEYCHAIN=1 asc ads v5 me view --output json ASC_BYPASS_KEYCHAIN=1 asc ads v5 acls list --output json ASC_BYPASS_KEYCHAIN=1 asc ads v5 campaigns list --org "$ASC_ADS_ORG_ID" --limit 1 --output json ``` Do not create spend-bearing Apple Ads campaigns in live smoke tests unless the user explicitly approves the exact org and payload. ## Documentation Updates Updated: - `commands/ads.mdx` - `authentication.mdx` with Apple Ads auth subsection - `configuration/environment-variables.mdx` with `ASC_ADS_*` variables - `docs.json` navigation for the new command page - `docs/COMMANDS.md` via `make generate-command-docs` - `README.md` feature list Docs teach `--file` payloads with copy-pasteable JSON examples and state that Apple Ads credentials are separate from App Store Connect API keys. ### Docs placement `commands/ads.mdx` is the user guide. Keep it focused on auth, org context, payload files, pagination rules, endpoint groups, and raw requests. `configuration/environment-variables.mdx` owns the full `ASC_ADS_*` reference and credential precedence. This architecture note owns endpoint coverage, auth internals, generated command strategy, test requirements, and live-smoke limits. ## Definition of Done - All 73 current v5 endpoints in the matrix have named commands. - `asc ads v5 api request` exists for debugging and newly added Apple fields. - Apple Ads OAuth login/status/token/doctor/logout are implemented. - All org-scoped commands require/respect `--org`. - All body endpoints use `--file` and support the correct JSON shape. - Deletes and bulk deletes require `--confirm`. - Offset pagination works with `--paginate`. - JSON output preserves Apple response envelopes. - Generated command docs are updated. - `make format`, `make check-command-docs`, `make lint`, and `ASC_BYPASS_KEYCHAIN=1 make test` pass. --- ### Architecture/Publishing Process Simplification # Publishing Process Simplification ## Goal Collapse App Store publishing guidance to one canonical answer while keeping older automation working during the migration window. ## Canonical command map - `asc publish appstore` - canonical App Store publish path - `asc release stage` - canonical pre-submit preparation path - `asc publish testflight` - canonical TestFlight publish path - `asc validate` - canonical App Store submission readiness path - `asc submit status|cancel` - lower-level submission lifecycle tools - `asc review ...` - raw review-submission resource management ## What changed Two App Store-facing command paths were causing avoidable confusion: - `asc release run` - `asc submit create` Both still work as deprecated compatibility paths, but neither should be taught as the primary answer to "how do I publish to the App Store?" The primary high-level answer is `asc publish appstore`. ## Use-when guidance ### Use `asc publish appstore` when - you want to publish an App Store release - you have an IPA or build-oriented App Store publish flow - you want one deterministic command that can upload, ensure the version exists, apply metadata, attach the build, validate readiness, and submit for review - you want the command agents and humans should reach for first ### Use `asc release stage` when - you want the same high-level preparation flow - you are not ready to submit yet - you need to stage metadata and build attachment before a later manual or automated approval step ### Use `asc publish testflight` when - you are distributing to TestFlight - you want an IPA-first high-level flow for beta delivery ### Use `asc submit ...` when - you want submission status or cancellation commands - you are debugging review state - you are maintaining an older direct-submit script and have not migrated off `asc submit create` yet ### Use `asc release run` when - you are maintaining older automation that still shells out to the legacy release command group - you need a compatibility path during the migration window while moving to `asc publish appstore` `asc submit preflight` remains available as a deprecated compatibility wrapper for older scripts that still expect the legacy preflight-style output. ### Use `asc review ...` when - you need direct access to review-submission resources, items, attachments, or history - you are doing advanced or API-shaped review workflow debugging ## Why `publish appstore` is the best App Store command - It matches the real user intent: publish an App Store release. - It uses the same `publish` taxonomy as TestFlight, so the top-level mental model stays consistent. - It includes the surrounding steps users routinely forget when they jump straight to submission. - It aligns the CLI with the documentation and with agent expectations. - It preserves `release run` as a migration shim instead of the primary learning path. - It preserves `submit` for lifecycle/tooling duties instead of overloading it as both a publish command and a submission-debug command. ## Migration policy - Keep `asc release run` runnable with a deprecation warning. - Keep `asc submit create` runnable with a deprecation warning. - Hide deprecated App Store entry points from primary discovery where practical. - Prefer `asc publish appstore` in help text, templates, migration hints, examples, and CI docs. --- ### Architecture/Runtime Migration Q2 2026 # Incremental Rewrite Plan (Q2 2026) Status: Draft Window: April 1, 2026 to June 30, 2026 Primary owner: Rudrank Riyam Scope: `asc` CLI architecture, workflow-critical command families, shared runtime concerns, and selected `internal/asc` cleanup ## Why This Exists This file is the source of truth for the Q2 2026 effort to rewrite the execution model of the CLI without doing a flag-day rewrite. The problem is not that the repo is "bad" across the board. The problem is that a few cross-cutting layers have become overloaded while command surface area kept growing quickly: - `79` root subcommands in [internal/cli/registry/registry.go](../../internal/cli/registry/registry.go) - `1,383` tracked Go files - `440,790` total Go lines - `230,251` non-test Go lines - `210,539` test Go lines - [internal/cli/shared/shared.go](../../internal/cli/shared/shared.go) at `1,482` lines - [internal/asc/client_options.go](../../internal/asc/client_options.go) at `3,590` lines - high-churn workflow families in `status`, `release`, `submit`, `builds`, `testflight`, `metadata`, and `apps` The architectural goal for Q2 is not "make every file pretty." The goal is to replace the foundation that future work keeps re-accumulating on: `shared`, `root`, registry wiring, and giant client option files. ## Rewrite Framing Think about Q2 as if we were rebuilding `asc` from scratch, but keeping the existing user-facing command contract stable wherever practical. That means: - preserve the best parts of the current CLI contract - replace the execution foundation underneath it - prefer rebuilding command families on the new foundation over wrapping old internals forever - treat migration scaffolding as demolition scaffolding, not architecture - end the quarter with the new system as the default and the old system mostly gone The question for each family is not "how do we move this code with the fewest edits?" The question is "if we were building this command family today, what foundation would we want it standing on?" ## Q2 Outcomes By the end of Q2 2026, we want all of the following to be true: 1. Root/runtime concerns are carried through an explicit runtime object instead of process-wide shared state. 2. The monolithic `shared` package is retired as a real dependency owner, and any temporary bridge wrappers used during migration are removed before Q2 closes. 3. Every root command family is constructed against the new runtime by the end of the quarter. 4. Workflow-critical commands are re-implemented cleanly on the new foundation before lower-value long-tail cleanup absorbs the schedule. 5. Command taxonomy churn is constrained by rules instead of growing organically. 6. `internal/asc` is easier to navigate, especially option and query definitions. 7. The repo feels like a better version of `asc`, not old `asc` with a new helper layer taped onto it. ## Non-Goals - Doing a single flag-day rewrite that breaks the repo for days - Switching away from `ffcli` - Replacing the entire `internal/asc` client in one quarter - Deleting all deprecated paths immediately - Chasing line-count reduction as the main success metric - Redesigning user-facing command contracts just for aesthetics while the architectural rewrite is in flight - Pretending a temporary facade is a valid end-state for the quarter ## Guiding Principles ### 1. Migrate by vertical slice Each meaningful migration unit should be a command family, not "some helpers." Good: - `status now uses runtime` - `submit now resolves auth/output/client through runtime` Bad: - `moved 12 helper functions out of shared` ### 2. Rewrite the foundation, migrate the surface incrementally New runtime code must live beside existing code first. Old paths can delegate into new paths until enough of the tree is migrated, but the destination is replacement, not coexistence. ### 3. Temporary bridge code must expire Bridge wrappers are allowed only to keep the repo moving while large slices are ported. They are not architecture. If a bridge still matters at the end of Q2, the migration is incomplete. ### 4. Freeze surface expansion where possible If a new top-level command family does not materially improve release, review, metadata, build, signing, or CI workflows, it should usually wait until after Q2. ### 5. Keep compatibility work explicit Deprecated aliases and migration shims must be inventoried, owned, and removed on a schedule. No "temporary" alias should be allowed to become indefinite by default. ## Architectural Target Introduce a runtime package that owns cross-cutting CLI behavior, and rebuild command construction around it. Proposed shape: - `internal/cli/runtime` - `Runtime` - `ClientFactory` - `AuthResolver` - `OutputResolver` - `Timeouts` - `ProfileState` - `LoggingConfig` - `IO` or terminal/output capabilities The runtime should be constructed once near [cmd/root.go](../../cmd/root.go) and passed into command constructors. Q2 rewrite posture: - re-implement the execution foundation under the existing command contract - port the entire root surface to that foundation over the quarter - allow shallow ports for low-priority families - require deeper redesign for workflow-critical families - delete migration scaffolding before quarter close If we were starting greenfield, the target shape would look like this: - a small root assembly layer - one explicit runtime object - thin command constructors - workflow/domain logic separated from CLI wiring - dedicated packages for auth, output, usage, and compatibility instead of one monolithic `shared` - `internal/asc` request-shape code organized by domain rather than accumulated in umbrella files Q2 does not need to reach a perfect greenfield state, but every phase should move the repo toward that shape rather than preserving current coupling. Target command constructor style: ```go func StatusCommand(rt *runtime.Runtime) *ffcli.Command func ReleaseCommand(rt *runtime.Runtime) *ffcli.Command func SubmitCommand(rt *runtime.Runtime) *ffcli.Command ``` Test-friendly target style for logic-heavy commands: ```go func StatusCommand(rt *runtime.Runtime, runF func(context.Context, *StatusOptions) error) *ffcli.Command ``` ## Baseline Hotspots ### Cross-cutting layers - [cmd/root.go](../../cmd/root.go) - [internal/cli/shared/shared.go](../../internal/cli/shared/shared.go) - [internal/cli/shared/compat_aliases.go](../../internal/cli/shared/compat_aliases.go) - [internal/cli/registry/registry.go](../../internal/cli/registry/registry.go) ### CLI packages with the most non-test weight - `gamecenter`: `15,717` - `web`: `12,874` - `subscriptions`: `7,938` - `testflight`: `7,851` - `shared`: `7,722` - `apps`: `5,953` - `builds`: `5,879` - `iap`: `5,772` - `metadata`: `4,830` - `submit`: `2,486` non-test, `6,552` total with tests ### `internal/asc` hotspots - [internal/asc/client_options.go](../../internal/asc/client_options.go) - [internal/asc/client_queries.go](../../internal/asc/client_queries.go) - [internal/asc/client.go](../../internal/asc/client.go) - [internal/asc/client_http.go](../../internal/asc/client_http.go) - the resource-specific `client_*` files with mixed query, request, and transport concerns ## Workstreams ### Workstream A: Runtime Foundation Owner outcome: - introduce `internal/cli/runtime` - stop adding new cross-cutting behavior directly to `shared.go` - route auth, output, profile selection, timeout resolution, and client creation through runtime - establish the replacement foundation that the rest of the quarter ports onto Primary targets: - [cmd/root.go](../../cmd/root.go) - [internal/cli/shared/shared.go](../../internal/cli/shared/shared.go) ### Workstream B: Workflow-Critical Vertical Slices Owner outcome: - rebuild the commands that define the product story first Primary families: - `status` - `release` - `submit` - `validate` - `builds` - `testflight` - `metadata` - `publish` ### Workstream C: Client Layer Simplification Owner outcome: - reduce the maintenance burden in `internal/asc` - split giant option/query files by domain or resource - keep transport and option plumbing from becoming a single catch-all layer - make the client layer look more like deliberate domain code and less like sediment Primary targets: - [internal/asc/client_options.go](../../internal/asc/client_options.go) - [internal/asc/client_queries.go](../../internal/asc/client_queries.go) - resource-specific option/query helpers ### Workstream D: Taxonomy and Compatibility Discipline Owner outcome: - stop root command churn from spreading migration cost across the tree - inventory deprecated aliases and remove the expired ones - keep canonical surfaces stable Primary targets: - [internal/cli/registry/registry.go](../../internal/cli/registry/registry.go) - [internal/cli/shared/compat_aliases.go](../../internal/cli/shared/compat_aliases.go) - command-specific deprecated wrappers ### Workstream E: Testing and Guardrails Owner outcome: - make architecture migration observable and safe - add targeted runtime and command-construction tests - prevent new shared-state regressions Primary targets: - `internal/cli/cmdtest` - runtime package tests - root/registry wiring tests ## Phase Plan ## Phase 0: Baseline and Rewrite Contract Window: April 1 to April 10 Goals: - land this document - define the runtime package shape - inventory compatibility aliases and long-lived shims - freeze non-essential top-level command expansion during Q2 - decide which experimental surfaces are explicitly deferred - write down the greenfield target so migrations are judged against an end-state, not just against current code Deliverables: - this roadmap committed - alias inventory table added to this file or a linked appendix - initial runtime package skeleton - root wiring plan approved - explicit statement that bridge code is temporary and scheduled for deletion Exit criteria: - a runtime type exists, even if thin - every new architecture PR links back to this document ## Phase 1: Rebuild the Foundation Window: April 13 to May 1 Goals: - create `internal/cli/runtime` - route the following through runtime: - profile selection - auth resolution - ASC client creation - default output resolution - timeout resolution - root logging overrides - convert `shared` from owner of state into owner of compatibility wrappers where needed - make new construction patterns attractive enough that future code naturally lands on them Primary files: - [cmd/root.go](../../cmd/root.go) - [internal/cli/shared/shared.go](../../internal/cli/shared/shared.go) Exit criteria: - root command constructs a runtime object once - new code paths no longer call process-wide mutable setters for profile or output state - a thin shim layer exists for legacy callers - the foundation is good enough that Phase 2 can rebuild workflow families on top of it instead of compensating for it ## Phase 2: Workflow Core Migration Window: May 4 to May 22 Goals: - rebuild the highest-value command families on runtime-backed construction and execution Priority families: 1. `status` 2. `release` 3. `submit` 4. `validate` Why these first: - they define the Q2 product story - they already orchestrate multiple lower-level operations - they currently lean hardest on shared cross-cutting behavior Exit criteria: - these families resolve auth, client, output, and timeout behavior through runtime - constructors are testable without depending on shared global state - command-level tests remain green - these families look like the template for the rest of Q2, not like one-off ports ## Phase 3: Build, TestFlight, Metadata, and Client Layer Window: May 25 to June 12 Goals: - migrate the next set of workflow-heavy families - split `internal/asc` option/query sprawl along resource boundaries - start the bulk tree-wide import migration away from the monolithic `shared` package - replace "dumping ground" files with domain ownership that would still make sense in a cleaner repo Priority families: 1. `builds` 2. `testflight` 3. `metadata` 4. `publish` 5. `apps` 6. `reviews` 7. `versions` 8. `localizations` Client cleanup targets: - split [internal/asc/client_options.go](../../internal/asc/client_options.go) into domain-specific files - split query helpers where resource or workflow ownership is obvious - keep transport/core client code distinct from request-shape catalogs Exit criteria: - major workflow families use the same runtime path - `client_options.go` is no longer the default destination for unrelated additions - the bulk of root command families no longer import the monolithic `shared` package - the new architecture is visibly winning in everyday development, not just in diagrams ## Phase 4: Long Tail, Deletions, and Release Hardening Window: June 15 to June 30 Goals: - finish the whole-tree port to the new foundation - remove temporary migration shims and expired compatibility layers - delete or nearly delete the old monolithic `shared.go` - move long-tail families onto runtime-backed construction even where deep domain cleanup is deferred - leave Q3 with feature-level follow-ups, not architecture debt from this rewrite - make the repo feel like it has crossed a boundary, not like it is still mid-migration Priority actions: - port remaining root families and stragglers - remove low-value wrappers made obsolete by runtime migration - prune deprecated aliases whose replacement paths are stable - remove bridge helpers and imports that exist only because the rewrite was staged Exit criteria: - runtime is the default pattern for all root command families - the monolithic `shared` package is no longer a meaningful dependency surface - compatibility inventory has owners and dates - Q3 backlog is explicit and feature-oriented rather than "finish the migration" - there is no plausible reason to route new code through the old execution model ## Week-by-Week Execution Plan This is the operating schedule for Q2 2026. Treat each week as a real delivery slice with a visible artifact, not just a status label. Weekly operating rules: - each week must have one primary architecture objective - each week should end with at least one mergeable PR or a clearly reviewable stack - every Friday, update the measurement table and tracking template in this file - if schedule pressure appears, protect workflow-core migration and deletion work before protecting low-value long-tail cleanup | Week | Dates | Primary objective | Expected output | Exit gate | | --- | --- | --- | --- | --- | | 1 | April 1 to April 3, 2026 | Lock the rewrite contract | land this roadmap, define greenfield target shape, freeze non-essential root-family expansion | architecture work is now judged against this plan rather than ad hoc instinct | | 2 | April 6 to April 10, 2026 | Baseline the rewrite | alias inventory, shared-responsibility inventory, runtime package skeleton, root wiring design note | runtime package exists and baseline metrics are captured in-repo | | 3 | April 13 to April 17, 2026 | Rewire root construction | `cmd/root.go` constructs runtime once; root tests added or updated around construction and help/output behavior | runtime is the single assembly point for root execution | | 4 | April 20 to April 24, 2026 | Move core cross-cutting concerns | auth resolution, default output resolution, timeout handling, and logging overrides routed through runtime | new code no longer needs process-wide setters for these concerns | | 5 | April 27 to May 1, 2026 | Finish foundation slice | ASC client creation and profile state moved behind runtime; first focused `shared` splits land | Phase 1 exits with a usable replacement foundation, not just scaffolding | | 6 | May 4 to May 8, 2026 | Rewrite `status` | `status` constructor and execution path rebuilt on runtime; tests updated around output and behavior | `status` becomes the first real template for the new architecture | | 7 | May 11 to May 15, 2026 | Rewrite `release` and `validate` | `release` and `validate` moved to runtime-backed construction, with shared-state reads removed where touched | two more workflow-critical families prove the pattern is repeatable | | 8 | May 18 to May 22, 2026 | Rewrite `submit` and harden workflow core | `submit` moved to runtime; workflow-core edge cases, docs, and tests tightened | Phase 2 exits with `status`, `release`, `submit`, and `validate` all on the new foundation | | 9 | May 25 to May 29, 2026 | Rewrite `builds` and `testflight` | both families moved to runtime-backed construction; shared build-selection logic cleaned up where needed | build-oriented workflow surface is now on the same execution model | | 10 | June 1 to June 5, 2026 | Rewrite `metadata` and `publish` | both families moved to runtime; related `versions` or `localizations` work starts where tightly coupled | metadata and publish flows stop depending on old foundations | | 11 | June 8 to June 12, 2026 | Split `internal/asc` and port adjacent families | meaningful `client_options.go` split lands; `apps`, `reviews`, `versions`, and `localizations` move onto runtime where practical | Phase 3 exits with workflow-heavy families migrated and client sprawl shrinking visibly | | 12 | June 15 to June 19, 2026 | Port long-tail batch one | `screenshots`, `videopreviews`, signing families, device/user families, and xcode-oriented families move to runtime | the remaining work is now mostly demolition and stragglers, not major foundation uncertainty | | 13 | June 22 to June 26, 2026 | Port long-tail batch two and delete old paths | `web`, `gamecenter`, `iap`, `subscriptions`, and remaining root families ported; obsolete wrappers and aliases removed aggressively | every root family is now runtime-backed and shared-import burn-down is near zero | | 14 | June 29 to June 30, 2026 | Stabilize and close Q2 | final bridge deletions, final metric snapshot, doc updates, Q3 carryover list limited to domain polish | Q2 definition of done is either met or any miss is explicit, narrow, and justified | ### Weekly Review Rhythm Use the same cadence every week: - Monday: pick the week slice, confirm scope, and note the target PR set - Wednesday: verify the migration is reducing shared/global state instead of only moving code around - Friday: update metrics, update the tracking table, and decide whether the next week keeps the same family or advances to the next slice ### Slip Policy If the quarter slips, cut in this order: 1. deepest cleanup in low-value long-tail families 2. optional `internal/asc` generator exploration 3. non-essential taxonomy tidy-up that does not remove real migration debt Do not cut: - runtime foundation completion - workflow-core migration - deletion of temporary bridge code - whole-root-family porting by June 30, 2026 ## Command Family Triage This table is the "whole codebase" view for Q2. Nothing in the root command tree is deferred from the foundation rewrite. The only thing that may be deferred is deep domain cleanup after the port is done. In other words: every family gets onto the new house this quarter, even if not every room is renovated to the same depth. | Area | Q2 treatment | Phase | Notes | | --- | --- | --- | --- | | `auth` | Foundation rewrite | 1 | Must move with runtime first. | | `shared` | Full split and retirement | 1-4 | Temporary bridge code allowed only during migration; delete it by quarter end. | | `registry` | Rewrite root construction | 1-4 | Registry becomes runtime-aware and stops carrying migration debt indirectly. | | `status` | Deep rewrite | 2 | Anchor workflow dashboard and prove the new pattern. | | `release` | Deep rewrite | 2 | Canonical high-level shipping path. | | `submit` | Deep rewrite | 2 | Core submission flow and validations. | | `validate` | Deep rewrite | 2 | Pairs directly with release/submit. | | `builds` | Deep rewrite | 3 | High churn and selector complexity. | | `testflight` | Deep rewrite | 3 | High workflow value and shared build concepts. | | `metadata` | Deep rewrite | 3 | Core workflow surface. | | `publish` | Deep rewrite | 3 | Keep aligned with release path. | | `apps` | Rewrite on new foundation | 3 | Large family; deep cleanup only where workflow-critical. | | `reviews` | Rewrite on new foundation | 3-4 | Port fully; deepen only as needed. | | `versions` | Rewrite on new foundation | 3-4 | Port with release/submit/builds work. | | `localizations` | Rewrite on new foundation | 3-4 | Port with metadata. | | `screenshots` and `videopreviews` | Rewrite on new foundation | 4 | Port fully; redesign can follow later. | | `signing`, `bundleids`, `certificates`, `profiles`, `users`, `devices` | Rewrite on new foundation | 4 | Operational domains; full port, selective cleanup. | | `xcode`, `xcodecloud`, `workflow`, `webhooks`, `notify`, `schema`, `snitch` | Rewrite on new foundation | 4 | Port to runtime-backed construction. | | `web`, `gamecenter`, `iap`, `subscriptions` | Runtime port plus import cleanup | 4 | Full port in Q2; deeper domain refactors may continue in Q3. | | all remaining root families | Runtime port plus import cleanup | 4 | No root family should still depend on old foundations after June 30. | ## `shared` Split Target Current `shared` responsibilities include all of the following: - root flag binding - usage rendering - output validation and rendering - profile selection - auth resolution - client construction - timeout helpers - CSV and date helpers - deprecation support Q2 target package shape: - `internal/cli/runtime` - `internal/cli/shared/auth` - `internal/cli/shared/output` - `internal/cli/shared/usage` - `internal/cli/shared/compat` - `internal/cli/shared/parse` Important rule: No new cross-cutting behavior should be added to [internal/cli/shared/shared.go](../../internal/cli/shared/shared.go) once Phase 1 starts unless it is part of reducing or delegating the file. Scratch-rebuild mindset: - if a responsibility belongs in `runtime`, move it there - if a responsibility belongs in a focused package, move it there - do not preserve current placement just because it already exists Retirement rule: - temporary bridge wrappers may exist in Phase 1 and Phase 2 - no new code may be written against those wrappers once replacement packages exist - all callers must be migrated by Phase 4 - [internal/cli/shared/shared.go](../../internal/cli/shared/shared.go) should be deleted, or reduced to a tiny non-authoritative shell under `150` lines, before Q2 closes ## `internal/asc` Cleanup Target Q2 target is not "generate the entire client." Q2 target is to stop defaulting to giant umbrella files. Rewrite stance: - prefer reorganizing the client as if we were naming files fresh today - keep compatibility only at call sites that still need old paths temporarily - do not let file splits become cosmetic if ownership is still muddled Rules: 1. New resource-specific options should not go into [internal/asc/client_options.go](../../internal/asc/client_options.go) unless they truly are cross-resource. 2. Prefer files like: - `client_options_builds.go` - `client_options_testflight.go` - `client_options_metadata.go` 3. Keep transport/core behavior in core files and request-shape catalogs in resource files. 4. Defer generator work unless manual splitting proves insufficient. ## Measurement Track these every Friday during Q2: | Metric | Baseline | Q2 target | | --- | --- | --- | | Root subcommands | `79` | no net growth without explicit approval | | Imports of monolithic `internal/cli/shared` | `384` | `0` outside temporary bridge code | | `shared.go` lines | `1482` | `0-150`, with deletion preferred | | `client_options.go` lines | `3590` | under `1000`, or deleted into domain files | | Runtime-backed root subcommands | `0` | `79` | | Workflow-critical families on runtime | `0` | `status`, `release`, `submit`, `validate`, `builds`, `testflight`, `metadata`, `publish` | | Compatibility aliases with owner/date | not centralized | `100%` inventoried and reviewed for removal | ## PR Rules for Q2 Every architecture-affecting PR should say: 1. which phase it belongs to 2. which workstream it advances 3. whether it increases or decreases shared/global state 4. whether it adds, removes, or extends a compatibility alias 5. what command family it migrates, if any 6. whether it primarily ports old code or meaningfully rewrites it onto the new shape Preferred PR size: - one command family migration - one runtime slice - one `internal/asc` split - one compatibility cleanup batch Avoid: - mixed feature + architecture + taxonomy mega-PRs ## Tracking Template Update this table as work lands. | Family / Area | Runtime-backed | Shared globals removed | Tests updated | Compat risk | Owner | Status | PRs | | --- | --- | --- | --- | --- | --- | --- | --- | | `auth` | no | no | no | medium | unassigned | planned | | | `shared` | no | no | no | high | unassigned | planned | | | `status` | no | no | no | low | unassigned | planned | | | `release` | no | no | no | low | unassigned | planned | | | `submit` | no | no | no | medium | unassigned | planned | | | `validate` | no | no | no | low | unassigned | planned | | | `builds` | no | no | no | high | unassigned | planned | | | `testflight` | no | no | no | high | unassigned | planned | | | `metadata` | no | no | no | medium | unassigned | planned | | | `publish` | no | no | no | medium | unassigned | planned | | | `internal/asc` options split | no | n/a | no | medium | unassigned | planned | | | alias inventory | n/a | n/a | no | high | unassigned | planned | | ## Risks ### Risk: package cycles during `shared` split Mitigation: - build the runtime package first - move dependencies outward, not sideways - keep compatibility wrappers thin ### Risk: root behavior changes accidentally Mitigation: - add root/runtime construction tests - keep help, output, profile, and auth behavior under explicit regression coverage ### Risk: migration stalls after the first slice Mitigation: - do Phase 2 immediately after runtime foundation - prove the pattern on `status` and `submit` before broad expansion - keep a weekly count of remaining imports of the monolithic `shared` package so drift is visible ### Risk: Q2 fills with new command churn instead of cleanup Mitigation: - require explicit approval for new root families - favor improving canonical workflows over new breadth ## Definition of Done for Q2 Q2 is successful if all of the following are true: 1. Runtime-backed construction is the default pattern for all root command work, not just new work. 2. Every root command family has been ported onto the new foundation. 3. `status`, `release`, `submit`, and `validate` use runtime-backed execution and are cleaner than the code they replaced. 4. `builds`, `testflight`, `metadata`, and `publish` are fully migrated. 5. The monolithic `shared` package is no longer a real dependency surface. 6. `client_options.go` is materially reduced or split enough to stop being a dumping ground. 7. Compatibility aliases are inventoried with owners and removal dates, and expired ones have been deleted. 8. Q3 follow-up work is feature-level or domain-level polish, not "finish the architecture rewrite." 9. A new contributor reading the root/runtime path would learn the intended architecture from the code itself, not from tribal knowledge. ## Immediate Next Step The first implementation PR after landing this file should do only this: 1. add `internal/cli/runtime` 2. wire root construction through it 3. move auth, output, client creation, and timeout resolution behind it Do not start with `gamecenter`, `web`, or broad client generation work. --- ### Architecture/Storekit Retention Messaging # StoreKit Retention Messaging Support Status: Implemented as stable Research date: June 18, 2026 Target API: Retention Messaging API 1.0 Target command root: `asc storekit` ## Design Retention Messaging is a StoreKit server API, not an App Store Connect API resource. It has different production and sandbox hosts, a different error envelope, and dedicated In-App Purchase API keys. It therefore lives in `internal/storekit` and under the stable Monetization command root: ```text asc storekit auth ... asc storekit retention-messaging ... ``` This keeps StoreKit credentials out of the existing `asc auth` profile pool and prevents an In-App Purchase key from being selected for an App Store Connect request. The App Store Connect OpenAPI snapshot under `docs/openapi` does not contain these endpoints. Alternatives considered: - Putting the commands under `asc subscriptions` would imply App Store Connect authentication and API ownership, both of which are incorrect. - A raw HTTP command would expose the endpoints but would not safely validate image constraints, message limits, environment selection, or destructive operations. ## Endpoint mapping | CLI command | Method and path | | --- | --- | | `images upload` | `PUT /inApps/v1/messaging/image/{imageIdentifier}` | | `images list` | `GET /inApps/v1/messaging/image/list` | | `images delete` | `DELETE /inApps/v1/messaging/image/{imageIdentifier}` | | `messages upload` | `PUT /inApps/v1/messaging/message/{messageIdentifier}` | | `messages list` | `GET /inApps/v1/messaging/message/list` | | `messages delete` | `DELETE /inApps/v1/messaging/message/{messageIdentifier}` | | `defaults set` | `PUT /inApps/v1/messaging/default/{productId}/{locale}` | | `defaults view` | `GET /inApps/v1/messaging/default/{productId}/{locale}` | | `defaults delete` | `DELETE /inApps/v1/messaging/default/{productId}/{locale}` | | `endpoint set` | `PUT /inApps/v1/messaging/realtime/url` | | `endpoint view` | `GET /inApps/v1/messaging/realtime/url` | | `endpoint delete` | `DELETE /inApps/v1/messaging/realtime/url` | | `performance start` | `POST /inApps/v1/messaging/performanceTest` | | `performance view` / `wait` | `GET /inApps/v1/messaging/performanceTest/result/{requestId}` | The base host is selected explicitly: - `production`: `https://api.storekit.apple.com` - `sandbox`: `https://api.storekit-sandbox.apple.com` Performance-test commands reject the production environment. `performance wait` polls no faster than every 10 seconds to remain within Apple's sandbox rate limit. ## Authentication Create an In-App Purchase API key in App Store Connect after Apple grants Retention Messaging access. A browser session logged into App Store Connect can be used to complete Apple's access-request form, but browser cookies are not API credentials and the CLI does not read them. Store a named profile: ```bash asc storekit auth login \ --name Production \ --key-id "$KEY_ID" \ --issuer-id "$ISSUER_ID" \ --private-key ./SubscriptionKey.p8 \ --bundle-id com.example.app ``` The keychain is preferred. Use `--bypass-keychain` for config-backed local development or CI. Environment authentication supports: ```text ASC_STOREKIT_KEY_ID ASC_STOREKIT_ISSUER_ID ASC_STOREKIT_PRIVATE_KEY_PATH ASC_STOREKIT_PRIVATE_KEY ASC_STOREKIT_PRIVATE_KEY_B64 ASC_STOREKIT_BUNDLE_ID ASC_STOREKIT_ENVIRONMENT ASC_STOREKIT_PROFILE ASC_STOREKIT_STRICT_AUTH ASC_STOREKIT_BYPASS_KEYCHAIN ``` Every request gets a newly signed ES256 JWT with `iss`, `iat`, `exp`, `aud=appstoreconnect-v1`, and `bid=`. The private key is parsed once per client but tokens are never cached. ## Input and output contract - The environment is always explicit through `--environment` or `ASC_STOREKIT_ENVIRONMENT`; there is no risky production default. - Image and message identifiers are caller-generated UUIDs. - Images must be PNG without transparency. `FULL_SIZE` requires width 3840 and height 160–2160; `BULLET_POINT` requires 1024×1024. - Message payloads use `--file` and reject unknown JSON fields. The CLI validates Apple's text limits and requires alternative text for every image. - Deletes and credential logout require `--confirm`. - TTY-aware `json`, `table`, and `markdown` output is available on API commands. - Invalid flags and local payloads return usage exit code `2`. API and transport failures return exit code `1` with Apple's error code and message. - Upload image and upload message are not idempotent. The client never retries an ambiguous upload automatically. Example message file: ```json { "header": "Keep everything you unlocked", "body": "Continue your subscription and keep access to every feature.", "image": { "imageIdentifier": "22222222-2222-4222-8222-222222222222", "altText": "The Example app on an iPhone" }, "headerPosition": "ABOVE_IMAGE" } ``` ## Sandbox verification Use a disposable app and resources. The full live verification sequence is: ```bash asc storekit auth doctor --environment sandbox --network asc storekit retention-messaging images upload \ --image-id 11111111-1111-4111-8111-111111111111 \ --image-size FULL_SIZE \ --file ./retention.png \ --environment sandbox asc storekit retention-messaging messages upload \ --message-id 33333333-3333-4333-8333-333333333333 \ --file ./message.json \ --environment sandbox asc storekit retention-messaging messages list --environment sandbox asc storekit retention-messaging defaults set \ --product-id com.example.monthly \ --locale en-US \ --message-id 33333333-3333-4333-8333-333333333333 \ --environment sandbox asc storekit retention-messaging endpoint set \ --url https://example.com/retention-messaging-api/ \ --environment sandbox asc storekit retention-messaging performance start \ --original-transaction-id 2000000000000000 \ --environment sandbox \ --wait ``` The original transaction ID must come from an actual StoreKit sandbox subscription purchase. The endpoint must be publicly reachable over HTTPS and implement Apple's Get Retention Message request/response contract. After the test reports `PASS`, configure the production URL with `--environment production`. Clean up disposable resources with the matching `delete --confirm` commands. Sandbox uploads are automatically approved, so list responses can be verified without waiting for Apple review. ## Compatibility and tests This adds a new root and a new optional `storekit` config object. Existing commands, App Store Connect profiles, and output formats are unchanged. There is no deprecation or migration. The RED-to-GREEN test plan covers: - all 14 documented HTTP operations, request methods, paths, query values, content types, and response decoding; - ES256 header and StoreKit JWT claims; - dedicated config credential lifecycle and strict environment resolution; - command discovery, local payload validation, confirmation gates, and output; - API error and millisecond `Retry-After` decoding; - built-binary usage exit code `2`. Live network verification is conditional on Apple granting access and valid `ASC_STOREKIT_*` credentials being present. It is not replaced by an App Store Connect browser login. --- ### Design/Agent Native Ad Hoc Distribution # Agent-native ad hoc distribution ## Product direction ASC should turn a local Xcode archive into a verifiable install result that an agent can hand to a person or another system. The public contract is based on the outcome, not on a particular automation framework or storage-provider vocabulary. Each stage emits structured output, writes deterministic artifacts under an operator-selected root, and can be retried without repeating completed account mutations. The complete workflow is planned as separate reviewable changes: 1. Generate modern Xcode `release-testing` export options and export an IPA. 2. Inspect the IPA and prepare a self-contained web-install bundle. 3. Publish that bundle through a caller-provided S3-compatible endpoint. 4. Reconcile registered devices and ad hoc provisioning profiles. 5. Use an explicit local PKCS#12 identity in an isolated signing environment; signing sync remains an optional lower-level preparation step. 6. Compose the stages into a resumable run with a durable receipt. The first change in this stack owns only item 1. The second change owns only local inspection and preparation from item 2; publishing remains a separate network-facing boundary. ## PR 2 public contract The experimental `distribute` family begins with two provider-neutral local commands: ```text asc distribute inspect --ipa ./App.ipa [--include-devices] [--output json|table|markdown] asc distribute prepare --ipa ./App.ipa [--output-dir DIR] [--title TITLE] \ [--channel CHANNEL] [--source-revision REVISION] [--source-url URL] \ [--output json|table|markdown] ``` `inspect` opens the IPA once without following a symlink, validates every ZIP member name, rejects encrypted members, duplicate paths, an ambiguous main app, and oversized selected metadata, then reports app, artifact, provisioning profile, certificate, and metadata-preparation facts. Raw device UDIDs are omitted unless the caller explicitly requests `--include-devices`; deterministic device-set and certificate fingerprints are safe to pass between agents. `prepare` applies the same inspection and requires an unexpired ad hoc profile whose bundle identifier matches the main app and contains at least one device. It writes a deterministic descriptor followed by the unchanged IPA in this layout: ```text bundle.json payload/app.ipa ``` The default path is `.asc/distribution//--`. The descriptor contains no timestamp, absolute input path, raw device UDID, URL manifest, or storage-provider setting. An existing byte-for-byte-equivalent bundle is reported as reused. Any other existing destination is a conflict and is never overwritten. A new bundle is assembled in an unpredictable sibling directory and published with no-replace semantics so `bundle.json` is never a receipt for a partial bundle. Both commands print data to stdout, diagnostics to stderr, and use exit code 2 for invalid flags or missing required flags. IPA or preparation validation failures use the ordinary non-zero command error. This is an additive experimental surface and requires no migration. ### PR 2 security and verification An IPA is treated as an untrusted ZIP, never extracted wholesale. Member names must be canonical relative slash paths without traversal, backslashes, NUL or control characters. The archive has fixed overall-size, entry, and declared-expansion limits; the main `Info.plist` is capped at 4 MiB and the embedded provisioning profile at 16 MiB, with both advertised-size and streamed-size enforcement. Preparation uses rooted, no-follow reads and writes, copies from the already-open IPA file, writes the descriptor last in staging, and refuses replacement at publication. RED-GREEN coverage includes the complete JSON schema, explicit device disclosure, ad hoc/development/enterprise/App Store classification, expired and mismatched profiles, missing metadata, malicious ZIP paths, duplicate and ambiguous app members, compressed-size limit bypasses, deterministic default paths, exact reuse, conflict/no-overwrite behavior, table/Markdown rendering, help/registration, built-binary stdout/stderr and exit behavior, command-doc generation, and the repository validation gate. Keeping install manifests out of preparation avoids pretending that URLs exist before a publisher assigns them. Extracting the IPA to a directory would make symlink and traversal handling much broader without adding needed metadata. Using a mutable channel directory as the bundle identity would be convenient for humans but would prevent safe retries and verifiable agent handoffs; channel is therefore descriptor metadata rather than an output-path key. The preparation result is `metadataEligible`; there is deliberately no generic `eligible` or `installable` boolean. Inspection and the persisted descriptor separately report profile CMS integrity, Apple profile trust, and the scoped `complete-main-app-code-resources-entitlements-and-profile-certificate-binding` verification. On macOS the complete main app is safely materialized into private bounded staging, `codesign --deep --strict` verifies its resource envelope and nested code, and each architecture's leaf signer certificate must occur in the embedded profile. Signed team, application identifier, debugging state, and other entitlements must be permitted by that profile. This does not claim project-wide verification when embedded targets exist; those IPAs remain blocked. Profile trust requires the expected Apple provisioning signer and a chain to an exact pinned Apple root. The verifier does not use the host root store and fails closed when a recognized root is not carried in the CMS; supporting a newly introduced Apple root requires a CLI update. CMS signature integrity alone is not Apple authenticity. `prepare` refuses to write unless profile integrity, Apple trust, and the exact complete-main-app signature scope are all `verified`. Profile certificate fingerprints are explicitly named `profileCertificateSha256Fingerprints`: they prove which certificates the embedded profile permits, not which identity signed the IPA. An IPA containing extensions, watch apps, or App Clips reports those target metadata paths but is not marked eligible in this change; a later signing slice must validate every target/profile pair before preparation claims project-wide readiness. IPA processing is copied once from the already-open input into a private snapshot so parsing, hashing, and publishing bind the same bytes. It is capped at 8 GiB before ZIP parsing; selected metadata and the complete materialized main app have expanded-size limits. Provenance text, IPA metadata, and ZIP member names are bounded and reject control, Unicode format, and bidirectional-control characters. `--source-url` must be an absolute HTTPS URL with no user information, query, or fragment so a deterministic descriptor cannot become a credential or signed-URL sink. This boundary matches current production patterns without inheriting their storage implementation: Blockstream separates local caller-URL bundle generation from upload; Mattermost uses immutable PR, merge, and commit object paths; Onym records structured build metadata and caps its index; ipa-server generates the complete web-install surface across providers. Retention, encryption, serialization, immutable object keys, channel indexes, comments, and final install URLs remain publishing concerns rather than local preparation concerns. ## PR 3: provider-neutral publication `asc distribute publish` consumes the immutable bundle produced by `asc distribute prepare` and makes it installable through a caller-owned, S3-compatible object store. The command intentionally does not create buckets, change ACLs or policies, or expose an AWS-shaped public API. The required storage coordinates are `--endpoint`, `--region`, `--bucket`, and `--prefix`; credentials come from the ordinary SDK chain, with optional `ASC_S3_*` aliases for agents that should not need AWS-named environment variables. `--receipt` and `--link-path` are also explicit required destinations outside the immutable prepared bundle, so publication state never contaminates a bundle that prepare may later reuse exactly. Private publication is the default. It stores a content-addressed IPA first, then an Apple installation manifest, and finally a small first-party HTML page. All three objects use bounded presigned GET URLs. The install page expires at the requested `--url-ttl`; the manifest and IPA URLs receive an additional `--download-grace` period so a tap near expiry can still finish. URLs are bearer credentials: normal JSON and receipts expose only a redacted install URL, while the exact URL is written only to a mode-0600 link artifact. Public publication requires both `--access public` and `--public-base-url`; it assumes the caller has already configured anonymous reads and never mutates storage policy. Private recovery validates each SigV4 signing time and lifetime against the receipt's page deadline, with the configured grace applied only to the manifest and IPA, before live-verifying any recovered URL. Public objects can outlive the app's signing profile; the receipt therefore records the profile expiry and verification facts, and publication requires a currently valid profile with a safety margin. Private publication additionally requires the profile to remain valid through the complete requested link and download-grace lifetime. The publisher validates a prepared `bundle.json` plus `payload/app.ipa`, rejects unsafe descriptor paths, and verifies the IPA digest and size before any network request. Existing objects are reused only when their SHA-256 metadata, length, and content type match exactly; mismatches are immutable-key conflicts. Every upload is followed by a no-redirect read verification; the IPA is downloaded within the declared size bound and hashed end to end. Retention remains the object-store operator's responsibility, preferably via a bucket lifecycle rule; this command never deletes older builds. Publication also fails closed until preparation records both a verified IPA code signature and verified provisioning-profile integrity and trust. Code-signature status alone is insufficient: the descriptor must carry the exact full-app scope `complete-main-app-code-resources-entitlements-and-profile-certificate-binding`, covering CodeResources, entitlements, the main executable, and profile-certificate binding. The publisher also requires the verified signer-certificate fingerprints to be canonical SHA-256 values present in the embedded profile certificate set, and carries that evidence into recovery receipts. Narrow, missing, `not-verified`, or unknown verification results are rejected even if another preparation implementation writes such a descriptor. The stable output contract is a camelCase JSON receipt containing schema, provider-neutral object coordinates, artifact identity, verification results, and a redacted install URL. Diagnostics and progress stay on stderr. Required or malformed flags are usage errors (exit 2); local validation, authentication, upload, and verification failures are ordinary command failures (exit 1). RED-GREEN coverage starts at the CLI boundary, then uses local HTTP servers for endpoint validation, signed PUT/HEAD/GET behavior, collision reuse/conflict, upload ordering, generated manifest/page content, presigning, verification, receipt/link permissions, and secret redaction. A built-binary invalid-invocation check and an S3-compatible integration smoke test complete verification. Alternatives considered were an AWS-specific command surface and uploading only an IPA. The former would leak one provider's deployment model into an agent workflow; the latter cannot produce Apple's `itms-services` installation flow. Bundling a web server in ASC was also rejected because distribution ownership, TLS, retention, and availability belong at the caller's chosen endpoint. This shape follows the strongest production properties already proven in other projects: Blockstream separates local manifest generation from provider upload and leaves short retention to backend lifecycle; Mattermost uses immutable PR/merge/commit object paths plus bucket-managed lifecycle and server-side encryption; Onym publishes serially and emits a structured build index and URLs. `ipa-server` demonstrates the value of arbitrary endpoints and public URL bases, but its credential-in-configuration-string pattern is explicitly not adopted. ## PR 6: agent-native orchestration PR 6 composes the lower-level commands into one typed state machine. It does not add a lane language, execute a caller-supplied shell script, or hide the effects behind one opaque `distribute` command. Planning, authorization, execution, recovery, local inspection, and live verification remain distinct: ```text asc distribute plan --archive-path PATH --config PATH --plan PATH \ [--state-dir DIR] [--output json|table|markdown] asc distribute apply --plan PATH --confirm PLAN_HASH [--output json|table|markdown] asc distribute resume --run RUN_ID [--state-dir DIR] [--output json|table|markdown] asc distribute status --run RUN_ID [--state-dir DIR] [--output json|table|markdown] asc distribute verify --run RUN_ID [--state-dir DIR] [--device DEVICE] \ [--timeout DURATION] [--output json|table|markdown] ``` Every command uses `shared.DefaultUsageFunc`. Long-form flags are canonical in documentation and examples. There are no interactive prompts. Machine-readable fields use camelCase; stage identifiers and enum values use lowercase snake case. ### V1 scope The first orchestration version deliberately supports one narrow, proven path: - an existing iOS `.xcarchive` containing one main application target; - a strict desired-devices file used by signing reconciliation; - one explicit local PKCS#12 distribution identity and an optional protected password file; - additive device, Bundle ID, and ad hoc provisioning-profile reconciliation; - manual Xcode `release-testing` export; - complete main-app, nested-code, resource, entitlement, profile-trust, and signer-certificate verification; - private publication to an existing caller-owned S3-compatible bucket; and - live fetch verification of the IPA, manifest, and install page. Extensions, watch apps, embedded apps, and App Clips make the plan `ready: false`. V1 also excludes archive creation, certificate creation or revocation, capability mutation, public object access, bucket creation, bucket policy or lifecycle changes, retention deletion, channel indexes, Git writes, and automatic install or launch control on a person's device. The storage API is S3-compatible but provider-neutral: AWS, Cloudflare R2, MinIO, and another compatible service use the same fields. `asc signing sync` is not part of the run state machine. An operator may use it before planning to place a normalized identity on local disk, then reference that local `.p12` and its password file in the distribution spec. The orchestrator never clones, pulls, commits, or pushes a signing repository and never guesses which identity from a repository should be used. ### Strict distribution config and spec The file passed to `--config` is the distribution spec. It is a bounded owner-private JSON file. Relative paths resolve against the config's directory. It does not interpolate environment variables or accept credential values. A representative V1 document is: ```json { "schemaVersion": 1, "devicesFile": "devices.json", "signing": { "identity": { "format": "pkcs12", "path": "../signing/distribution.p12", "passwordFile": "../secrets/distribution-p12-password", "certificateSha256": "OPTIONAL_LOWERCASE_SHA256" }, "minimumValidityDays": 7, "maxMutations": 32 }, "publication": { "endpoint": "https://objects.example.com", "downloadEndpoint": "https://downloads.example.com", "region": "auto", "bucket": "ios-builds", "prefix": "team/app", "addressingStyle": "path", "urlTtl": "24h", "downloadGrace": "1h", "verifyTimeout": "30s" }, "metadata": { "title": "App", "channel": "pull-request-42", "sourceRevision": "abc123", "sourceUrl": "https://example.com/team/app/commit/abc123" } } ``` `signing.identity.passwordFile`, `signing.identity.certificateSha256`, `publication.downloadEndpoint`, and every metadata field are optional. An omitted password file means the PKCS#12 must decode with an empty password. Credentials are intentionally absent from the spec. The publisher keeps its existing resolution contract: a complete `ASC_S3_ACCESS_KEY_ID` and `ASC_S3_SECRET_ACCESS_KEY` pair, with optional `ASC_S3_SESSION_TOKEN`, wins; otherwise it uses the standard AWS SDK credential chain. The spec schema rejects unknown fields, duplicate keys, trailing JSON values, invalid enums, non-positive limits, unsafe or unbounded text, credential-bearing URLs, public-access fields, and inconsistent optional-field combinations. The spec, devices file, identity, and password file are opened without following the final path component. Secret inputs must be regular, owner-private files; platforms with ownership and link-count support also require the current owner and one link. The devices file keeps its existing strict V1 shape. Raw names and UDIDs are consumed only from that protected file. ### `plan` `plan` validates the complete spec before authentication. It then performs bounded local inspection and read-only App Store Connect preflight. Storage coordinates are validated locally during planning; apply resolves the configured SDK credential chain before any Apple mutation. That proves credential resolution, not future `PutObject` authorization. The first conditional object write may still fail with a typed operational error from the provider. It proves that the local identity contains exactly one usable private key and certificate, selects that exact certificate for reconciliation, hashes the complete archive tree, config, and devices file, and records the PKCS#12's verified certificate and private-key relationship. Password bytes are neither hashed nor serialized. Planning performs no POST, PUT, keychain import, profile installation, Xcode export, or Git mutation. The plan destination is required and create-only. A new plan never replaces an old authorization artifact. An expected safety blocker is represented by exit 0 with `ready: false` and typed `blockers`; malformed input or a failed preflight is an ordinary command failure. The strict plan artifact has this shape: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` A blocker has exactly `code`, `stage`, and a bounded redacted `message`. For example: ```json { "code": "embedded_targets_unsupported", "stage": "preflight", "message": "V1 requires one main application target." } ``` `ready` is true only when `blockers` is empty. Each effect has a closed `stage` and `kind` enum, an optional non-secret bundle identifier or count, and the relationship that makes reuse safe. Account mutation kinds are `register_device`, `create_bundle_id`, and `create_profile`. Profile download is a local write, not an account mutation. Object-write kinds are `ensure_ipa`, `ensure_manifest`, and `ensure_install_page`; all use immutable keys and exact content evidence. `write_profile` authorizes the verified local provisioning-profile output even when no account mutation is required. Protected run snapshots and receipts are state-machine evidence implicit in applying the plan rather than separate external effects. `planHash` is SHA-256 over the canonical complete plan payload excluding `planHash` and `createdAt`. It therefore binds the random `planId`, every recorded input digest and path, selected certificate, nested reconcile plan, effective signing-validity policy, limits, destination, link policy, and effect inventory. Any material change requires a new plan. The effective signing-validity policy is the larger of the configured minimum and a whole-day duration strictly longer than `urlTtl + downloadGrace + 1m`. The plan binds both that policy and the absolute `minimumValidUntil`; local and App Store Connect certificates and profiles must outlive it before any account mutation. Known temporary object-store credentials are checked against the same publication window before reconciliation and again before a new publication intent, so the orchestrator never silently shortens an authorized link. ### Exact authorization and `apply` The only accepted authorization is the full hash printed by `plan`: ```text asc distribute apply \ --plan .asc/distribution/app-plan.json \ --confirm 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef ``` Missing, malformed, or unequal `--confirm` is a usage error and is rejected before authentication, state creation, or another side effect. `apply` strictly decodes the plan, recomputes its hash, requires `ready: true`, reloads the spec, and revalidates every input before the first account mutation. The hash authorizes only the enumerated additive account mutations, local writes, and immutable object writes. It does not authorize certificate creation, deletion, overwrite, retention, bucket administration, or a different destination. One plan creates at most one run. Reapplying an exact plan returns the existing run and its typed recovery status instead of creating a duplicate. The run identifier is deterministically derived from the hash-bound random `planId` and `planHash`, so the same authorization converges on one unpredictable run directory even across processes. One process owns the exact original run-lock inode. A retained state/run-directory lease detects pathname replacement before and after each stage and checkpoint and fail-stops without publication or a success result. This is drift detection, not a same-UID filesystem sandbox; a replacement directory may acquire a distinct lock but cannot be adopted by the original invocation. Before reconciliation, apply: 1. recovers any prior ephemeral-signing journal; 2. takes a bounded private snapshot of the complete archive and verifies the planned tree digest; 3. reopens the devices file and verifies its digest and device-set fingerprint; 4. reopens and decodes the protected PKCS#12 and verifies its leaf-certificate, private-key, team, and validity evidence; and 5. resolves the configured storage credentials and rechecks the read-only account observations that make the plan safe. The ordered execution stages are: ```text preflight identity_validate account_reconcile export prepare publish fetch_verify complete ``` Reconciliation remains additive and convergent. The downloaded or newly created profile must embed the planned identity certificate and match the planned team, bundle, ad hoc class, entitlements, and exact desired-device set. Export runs with that explicit profile mapping and the identity in the isolated temporary keychain. It invokes `xcodebuild` directly with a fixed argv and a sanitized environment; distribution passwords, S3 credentials, and App Store Connect credentials are not inherited by archive build scripts. A successful export is not checkpointed until the profile and keychain cleanup result is known. Cleanup failure leaves the run recoverable and blocks publication. Preparation verifies the exported IPA again and binds its app, profile, device-set, and signer evidence to the plan. Publication persists its intended random link identity, keys, digests, generated documents, and sensitive URLs in protected state before issuing the first PUT. It then follows the IPA, manifest, install-page order and exact reuse rules described in PR 3. The run does not become complete until all three published resources pass live fetch verification. ### Run state, status, and receipt The mutable run document is strict, versioned, typed state rather than an arbitrary map of command outputs: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` Run status is one of `planned`, `running`, `recoverable`, `blocked`, or `complete`. `stage` is one of the ordered execution-stage identifiers above. A state transition is valid only when every prerequisite has exact durable evidence. Unknown fields, inconsistent status/stage combinations, invalid digests, and evidence outside the selected run root are rejected. Every member of `artifacts` is optional until its stage has produced and checkpointed the corresponding evidence. `status` reads this state locally and performs no authentication, Git, Apple, keychain, Xcode, storage, or HTTP operation. A valid running, recoverable, or blocked run is still a successful status query; automation branches on `status`, `stage`, `lastFailureCode`, and `recoverable` rather than parsing prose. Completion publishes an immutable create-only `receipt.json`. It binds `runId`, `planHash`, app version/build, IPA digest and size, signing/profile verification, object keys and digests, live-fetch result, redacted install URL, profile expiry, and the paths and digests of its public and sensitive companion artifacts. Its completion scope is exactly `published_and_fetch_verified`. It never claims that a device installed or launched the app. The exact install, manifest, and IPA URLs exist only in the separate owner-private sensitive link artifact; normal output returns that artifact's path, not its bearer values. ### Resume and live verification `resume` continues only the exact confirmed plan. It never treats an earlier process exit as proof that a durable effect completed. Before skipping a successful stage it revalidates that stage's exact evidence: - reconcile receipts and downloaded profile content against current account resources; - local identity, profile, team, certificate, and expiry relationships; - archive snapshot, IPA, and prepared-bundle digests; - signing-session cleanup state; and - published object, generated-document, receipt, link, and expiry evidence. A failure definitely before a side effect is recorded as `recoverable` when the same confirmed run can safely retry it, or `blocked` when a new plan is required. A timeout, cancellation, crash, or lost response around a mutation uses a closed `lastFailureCode` rather than adding an unbounded status. Resume performs the exact read-only reconciliation for that mutation before deciding whether it succeeded, may be reused, or remains inconclusive. It never blindly repeats a device, Bundle ID, profile, or object write. Input drift, a conflicting immutable resource, an expired signing profile, and an expired private link stop as non-recoverable and require a new plan; they are not silently repaired under the old confirmation. Retention and deletion remain outside the run. `verify` is read-only but live. It returns a typed result with `publicationVerified`, the verified app/artifact identity, and an optional redacted `deviceObservation`. It reopens the immutable receipt, prepared bundle, and sensitive link artifact; reconstructs the manifest and install page; verifies their exact relationships and expiry; then performs bounded, no-redirect HTTPS reads of the page, manifest, and full IPA with content type, size, and SHA-256 checks. Success means the planned release-testing artifact is still fetchable through the recorded private publication. It does not claim an install or launch result. Optional `--device` uses an explicit connected-device selector to observe the exact bundle, version, and build; the selector is not persisted or echoed, and this observation does not prove byte identity with the published IPA. ### State and privacy boundary The plan, run state, receipts, lock, and sensitive-link artifact live beneath rooted handles anchored before execution. Path-based lower seams are bracketed by a retained state/run inode lease, and any path drift is a terminal stop. Run directories are mode `0700` and JSON state is mode `0600`. Files are bounded, owner-checked where supported, opened without symlink traversal, and rejected when non-regular, hard-linked, or group/world-accessible. Updates use unpredictable exclusive staging, file and parent-directory synchronization, and no-replace publication. The final receipt cannot be replaced. The permanent lock inode serializes cooperative apply/resume owners of the unchanged run directory. Raw UDIDs and device names, PKCS#12 or password bytes, secret-file contents, App Store Connect credentials, S3 access keys or session tokens, credentialed repository or endpoint URLs, exact presigned URLs, arbitrary child commands, and unbounded provider bodies never appear in a plan, run document, normal receipt, stdout, stderr, or error. Device names and UDIDs are reduced to a count and deterministic set fingerprints. Diagnostics use closed error codes and bounded redacted messages. Exact bearer URLs stay only in the sensitive link artifact. ### Output and exit semantics Data goes to stdout and progress or diagnostics to stderr. Explicit `--output` wins; otherwise the ordinary TTY-aware ASC default applies. JSON remains one parseable document even when an operational failure also returns a run snapshot. - Exit 0: the requested operation completed. `plan` may report `ready: false`, and `status` may report a running, recoverable, or blocked run, because both commands successfully answered their query. - Exit 1: an operational, safety, provider, export, publication, live verification, or cancellation failure. Apply/resume includes the saved typed run state when it exists and a checkpoint was durably available. - Exit 2: invalid syntax, an invalid flag or enum, a missing required flag, or a missing, malformed, or unequal `--confirm PLAN_HASH`. The root process currently translates `SIGINT` into context cancellation; it does not promise signal-number-preserving exit codes or `SIGTERM` handling. `apply` exits 0 only after `fetch_verify` and immutable receipt publication. `verify` exits 0 only when every requested live fetch check succeeds. Its `--timeout` bounds the complete requested live check; the fetch verifier uses the smaller of this value and the plan's configured `verifyTimeout`. ### Why this is agent-native Imperative task runners commonly expose task-oriented vocabulary, process-global environment, mutable keychain conventions, and a human log stream. Those are not the contract ASC wants agents to reason over. ASC instead exposes an immutable effect plan, exact hash authorization, closed schemas and enums, durable evidence per stage, parseable recovery state, and truthful verification scope. An agent can decide what will change, request authorization for those exact effects, recover after a crash, and prove what remains available without interpreting opaque steps or scraping logs. The existing generic `asc workflow` runner is also the wrong security boundary for this default. It executes caller-authored shell commands, inherits broad environment state, persists generic parameters and string outputs, and resumes from process success rather than domain evidence. A workflow may call one already-safe `asc distribute apply` or `resume` command as a higher-level repository convention, but it must not own the identity, account mutation, publication, secret, or recovery state. ### Acceptance evidence On 2026-08-13, the lower-level stack was composed manually with a real SmolLens archive, a local PKCS#12 identity, a reconciled ad hoc profile, and private S3-compatible publication. The private OTA link was opened on a registered physical iPhone, installation completed, and SmolLens launched successfully. This establishes the real install-and-launch promotion gate for the composed path. State-machine crash/recovery and private-object-store integration smoke remain separate handoff evidence when matching signing inputs and storage configuration are available. The sanitized Xcode environment and process-group cleanup are isolation and recovery controls, not a sandbox. Trusted project build scripts still execute with the caller's UID and can access the caller's filesystem, keychains, and network; a deliberately malicious script can create a new session. Private identity and password files should therefore stay outside an untrusted project tree. No install URL, object-store bucket or prefix, device identifier, profile UUID, certificate fingerprint, account identifier, or credential is retained in this design note. ### PR 6 RED-GREEN and verification RED begins at the public command boundary: registration and help; strict spec, plan, run, and receipt decoding; exact plan-hash confirmation before auth; read-only planning; hash coverage; single-main-app blocking; local-P12 selection and certificate binding; additive action inventory; protected state paths; cooperative run locking plus pathname-drift detection; atomic persistence failures; crash points before, during, and after each remote effect; unknown-outcome reconciliation; successful-stage revalidation; secret-canary scanning of every stream and artifact; sanitized Xcode environment; cleanup gating; immutable publication reuse and conflict; local-only status; live verify; and exact exit codes. Package tests use injected App Store Connect, Xcode, keychain, storage, HTTP, clock, randomness, signal, and filesystem dependencies. CLI tests assert JSON, table, and Markdown output plus stdout/stderr separation. The built binary is then exercised against a disposable archive and local S3-compatible service, followed by `make format`, `make check-docs`, `make lint`, and `ASC_BYPASS_KEYCHAIN=1 make test`. Any later live mutation uses only the disposable account and records cleanup separately from merge readiness. ## Placement and current behavior `asc xcode export-options generate` currently always writes `method=app-store-connect`. `asc xcode export` implicitly generates the same options when `--export-options` is omitted. A caller can provide a custom plist, but ASC cannot generate the non-App-Store export used for ad hoc delivery. Xcode 26.6 and Xcode 27 call this method `release-testing`. Both versions still accept `ad-hoc`, but mark it deprecated. ASC will use the current Xcode name and will not introduce the deprecated spelling as a new public value. ## PR 1 public contract The standalone generator adds: ```text asc xcode export-options generate \ --archive-path .asc/artifacts/App.xcarchive \ [--method app-store-connect|release-testing] \ [--destination export|upload] \ [--signing-style automatic|manual] ``` `--method` defaults to `app-store-connect`, preserving every existing invocation. `release-testing` requires `--destination export` because it creates a local IPA rather than an App Store Connect upload. Its default output is `.asc/export-options-release-testing.plist`; the existing App Store default remains `.asc/export-options-app-store.plist`. `asc xcode export` receives the same `--method` flag for its implicit generator: ```text asc xcode export \ --archive-path .asc/artifacts/App.xcarchive \ --method release-testing \ --signing-style manual \ --ipa-path .asc/artifacts/App.ipa ``` An explicit `--export-options` file remains authoritative and cannot be combined with `--method`, `--signing-style`, or `--team-id`. The default export method remains `app-store-connect`. No existing output field changes; `method` reports the actual generated value. Invalid values are usage errors with exit code 2. Data remains on stdout and diagnostics remain on stderr. ## Implementation and compatibility The repository-owned generator passes the selected method to the pinned Bitrise typed models. App Store Connect continues to use the App Store model. Release-testing uses the non-App-Store model. Manual signing resolution receives the selected method so profile selection matches the requested export. The pinned resolver still classifies installed ad hoc profiles with Xcode's legacy `ad-hoc` enum, so ASC translates only at that internal resolver boundary and continues to emit `release-testing` in the generated plist. The change is additive. No deprecation or migration is required. The legacy `ad-hoc` Xcode spelling is intentionally rejected with guidance to use `release-testing`. ## RED-GREEN and verification Coverage must establish: - valid generator and implicit-export parsing for both methods; - invalid and explicitly empty method values as usage errors; - rejection of release-testing with `destination=upload` or `xcode export --wait`; - conflict errors when `--method` accompanies an explicit plist; - exact `method=release-testing` plist and JSON output; - manual generator receipt of the selected method; - portable and Darwin typed-model parity; - unchanged app-store-connect defaults; - generated command documentation, focused tests, built-binary stdout/stderr and exit codes, followed by the repository validation gate; - real archive export with Xcode 26.6 and Xcode 27 before the distribution stack is declared complete. ## Handoff and promotion gates Each slice must be committed on its own feature branch and pushed at the exact revision that passed its focused tests and repository validation gates. A downstream slice must not be folded into the same commit merely because it uses the preceding command. Review handoff must record the tested Xcode versions, the exact commit, live verification performed, and any gate that remains. When matching signing inputs or private storage are unavailable, the handoff must explicitly record missing state-machine crash/recovery or object-store integration smoke as an unresolved verification risk. `--method` remains experimental until the complete workflow has exported a real archive, published a fetch-verified HTTPS manifest and IPA, and installed the expected bundle and build on a registered device. Manual exports still depend on a locally available distribution private key and provisioning profiles that cover every embedded target and capability. Later slices must also settle the security and retention contract for caller-provided storage, bearer install URLs, device identifiers, and resumable state before `asc distribute` can be promoted to stable. ## Alternatives Accepting `ad-hoc` would mirror older automation tools but create a deprecated surface on day one. Hiding the method only inside the future distribution orchestrator would leave `asc xcode export` incomplete and make that orchestrator depend on a private code path. Supporting every Xcode export method in this change would widen the review without helping the first install-link workflow. --- ### Design/App Search Keywords Set Rework # Rework the app search-keywords setter ## Placement and command shape `asc apps search-keywords list` remains the low-level app-keyword read surface. The released sibling spelling `asc apps search-keywords set` remains available, but its write is routed through the supported App Store version-localization resource instead of the app's read-only `searchKeywords` relationship. The supported invocation is: ```bash asc apps search-keywords set \ --app "APP_ID" \ --version "1.2.3" \ --locale "en-US" \ --platform IOS \ --keywords "kw1,kw2" \ --confirm ``` `--app`, `--version`, `--locale`, `--keywords`, and `--confirm` are required. `--platform` is optional when the app has only one App Store version with that version string. If the same version string exists on multiple platforms, the command fails before mutation and asks for `--platform`. On success the command writes the updated App Store version-localization response to stdout. Usage failures, resolution failures, and API diagnostics go to stderr through the normal CLI runner. Invalid or missing flags exit 2; not-found, ambiguity, pagination, and API failures exit 1. ## API contract The current OpenAPI defines this three-request flow: 1. `GET /v1/apps/{id}/appStoreVersions` with `filter[versionString]`, optional `filter[platform]`, and `limit=200` resolves the App Store version. Every response page is considered. 2. `GET /v1/appStoreVersions/{id}/appStoreVersionLocalizations` with `filter[locale]` and `limit=200` resolves exactly one existing localization. Every response page is considered. 3. `PATCH /v1/appStoreVersionLocalizations/{id}` sends an `AppStoreVersionLocalizationUpdateRequest` containing only `data.attributes.keywords`. The update returns an `AppStoreVersionLocalizationResponse`. The keyword field is a comma-separated string and is subject to App Store Connect's 100-character limit. Apple exposes only `GET` at `/v1/apps/{id}/relationships/searchKeywords` and `GET /v1/apps/{id}/searchKeywords`. The original `PATCH /v1/apps/{id}/relationships/searchKeywords` transport is not in the OpenAPI and must not be restored. Localization-level `searchKeywords` relationship POST and DELETE operations do exist, but they accept opaque `appKeywords` linkage IDs rather than raw keyword text and are unrelated to this setter. ## Maintained implementation evidence Codemagic CLI Tools exposes `keywords` on App Store version localization create and modify actions. Its maintained client sends keyword text as a localization attribute, not as app-keyword relationship linkage. Relevant current sources: - [`app_store_version_localizations.py`](https://github.com/codemagic-ci-cd/cli-tools/blob/feb80b2d944923402e56b825f40c038a39f35b64/src/codemagic/apple/app_store_connect/versioning/app_store_version_localizations.py) - [`app_store_version_localizations_action_group.py`](https://github.com/codemagic-ci-cd/cli-tools/blob/feb80b2d944923402e56b825f40c038a39f35b64/src/codemagic/tools/app_store_connect/action_groups/app_store_version_localizations_action_group.py) These implementations agree with the local OpenAPI snapshot and the existing `asc localizations update --version ... --locale ... --keywords ...` and `asc metadata keywords ...` implementations. ## History and compatibility Commit `56e162bb7ea0e2a9e7c3471ea2ffedf163411c1f` introduced `set` in PR #346 for issue #317. The OpenAPI snapshot at that commit already exposed only GET for the app relationship. The PR's live smoke exercised `list`, not `set`; its only set evidence was a mock that accepted the invented PATCH and returned 204. The public command spelling and the existing `--app`, `--keywords`, `--confirm`, and output flags are preserved. An old invocation without `--version` and `--locale` cannot be preserved safely: app ID plus keyword text does not identify the version-localized record Apple requires. Such an invocation now fails with migration guidance before authentication or network access. No automatic "editable version", latest version, primary locale, or first-match selection is performed. The direct alternatives remain valid: - `asc localizations update --version "VERSION_ID" --locale "en-US" --keywords "kw1,kw2"` when the version ID is already known; - `asc metadata keywords push --version-id "VERSION_ID" --input "./keywords.json"` for locale-keyed direct input; and - `asc metadata keywords apply --app "APP_ID" --version "1.2.3" --dir "./metadata" --confirm` for repository-backed metadata. ## Verification design RED coverage first exercises the retained spelling and proves it cannot perform the required supported flow. GREEN coverage asserts the exact methods, paths, filters, pagination URLs, PATCH body, JSON output, and exit behavior. It also covers missing required flags, invalid platform and locale values, missing versions and localizations, version ambiguity across platforms, duplicate locale ambiguity, pagination failures, and API failures. The built binary is checked for help, stdout, stderr, and exit status. Live verification is read-only: resolve versions on the disposable app and list the selected version's localizations. Live mutation uses only disposable app `6759231657` when the current value can be restored and verified safely. The final built binary resolved iOS version `1.0` and its single `en-US` localization on that disposable app. It changed `keywords` from `baseline,copy` to `pr1878-live-temporary`, a separate read observed the new value, then the same command restored `baseline,copy`; a final read confirmed the restoration. No disposable state was left behind. ## Alternatives considered Accepting only a version-localization ID would reduce requests, but it would discard the released app-oriented command shape and make `--app` meaningless. Automatically selecting a latest or editable version and primary locale would keep the old flag count, but could silently mutate the wrong platform, version, or language. Explicit version and locale selection with optional platform is the smallest supported, deterministic rework. --- ### Design/App Store Connect Api 4.4.1 Coverage # App Store Connect API 4.4.1 coverage ledger ## Objective Deliver production-ready CLI support for every behavior added or changed by App Store Connect API 4.4.1 without silently changing the ID or lifecycle semantics of existing stable commands. Schema-only support is acceptable only when the ledger below records an evidence-backed reason that a first-class command is not useful. The work was split into stacked, independently reviewable pull requests based on the 4.4.1 schema update. Each behavior PR used RED-GREEN tests, verified a freshly built binary, and passed the complete repository gate before review. ## Source contract The coverage ledger is derived from three independent views of the contract: 1. Apple's App Store Connect API 4.4.1 release notes. 2. Apple's official OpenAPI zip, independently downloaded and compared byte-for-byte with `docs/openapi/latest.json`. 3. A semantic diff from the original repository 4.4 snapshot commit to 4.4.1, plus a second diff from the immediate pre-PR repository snapshot so work already reconciled after the 4.4 import is not counted twice. Reproducible inputs: | Input | Value | | --- | --- | | Official source artifact | `https://developer.apple.com/sample-code/app-store-connect/app-store-connect-openapi-specification.zip` | | Downloaded zip SHA-256 | `9386762084aa7156a9d5aab20526daf8d4ca423ddaebb0b3fffd2ef6fd836370` | | Extracted filename | `openapi.oas (2).json` | | Extracted JSON SHA-256 | `ed0202ef37155b9334772482d2ea0be688c3046b284c895bcbea5455fbe54fd8` | | Repository 4.4.1 JSON SHA-256 | `ed0202ef37155b9334772482d2ea0be688c3046b284c895bcbea5455fbe54fd8` | | Original 4.4 baseline commit | `d465bea0c9563e415da8989b284f1810173b073e` | | Original 4.4 JSON SHA-256 | `eb33a4909309c75c5f4a24e2a41db9bb18df02c4b2113c5b1d6e1eed4ce4c891` | | Immediate pre-4.4.1 base | `839c4da6db3678ecbab5cf1db6d78b4b8c486957` | Verified snapshot facts: | Contract item | 4.4 | 4.4.1 | Delta | | --- | ---: | ---: | ---: | | Paths | 929 | 966 | +37 | | Operations | 1,216 | 1,263 | +47 | | Component schemas | 1,346 | 1,393 | +47 | | Removed operations | - | - | 0 | | Removed schemas | - | - | 0 | | Modified existing operations from original 4.4 | - | - | 102 | | Modified existing schemas from original 4.4 | - | - | 61 | | Unchanged operations transitively affected by modified schemas | - | - | 71 | | Schema-mediated operations with behavior work remaining at the immediate pre-PR base | - | - | 40 | | Schema-mediated operations already reconciled after the 4.4 import | - | - | 31 | | Still-different operations at the immediate pre-PR base | - | - | 50 | | Still-different schemas at the immediate pre-PR base | - | - | 17 | | Operation changes already reconciled after the 4.4 import | - | - | 52 | | Schema changes already reconciled after the 4.4 import | - | - | 44 | The semantic diff changes only `info.version`, `paths`, and `components.schemas`; no other component category, security scheme, or top-level contract changes. ## Current effort and status The CLI pull requests landed sequentially after exact-head audit. Each behavior PR passed its exact-head gates before merge. The final behavior integration at `48e0d003` also passed its post-merge Main Branch, Govulncheck, and CodeQL workflows. No App Store Connect API 4.4.1 behavior PR remains open: | Scope | Pull request | Reference head | Landed `main` commit | Status | | --- | --- | --- | --- | --- | | Relationship-aware schema discovery and stale-index enforcement | #1776 | `aaa9b62d` | `893c840e` | Merged and `main`-gated | | IAP versions, v2 localizations/images, compatibility, and docs | #1777 | `ee40c7b3` | `bd453af4` | Merged and `main`-gated | | Age-rating social-media fields and adjusted equalizations | #1778 | `39284b0a` | `f6be93f6` | Merged and `main`-gated | | Subscription versions, v2 localizations/images, compatibility, and docs | #1779 | `c8f3ab52` | `0349e397` | Merged and `main`-gated | | Subscription-group versions, v2 localizations, compatibility, and docs | #1780 | `48d04e0b` | `8cbcdadc` | Merged and `main`-gated | | Cross-cutting review-submission version items and migration notes | #1781 | `aba35e0a` | `eea98d3a` | Merged and `main`-gated | | Age-rating dependency validation | #1782 | `3dfd15a0` | `b92e5605` | Merged and `main`-gated | | Adjusted-equalization required filters | #1783 | `a06c935c` | `6f11d6aa` | Merged and `main`-gated | | Positional-argument and usage validation | #1784 | `fc5f0504` | `1cc9face` | Merged and `main`-gated | | Plural relationship-limit compatibility | #1785 | `da030480` | `b01d4d70` | Merged and `main`-gated | | Legacy 4.4.1 resource deprecation transition | #1786 | `1ef32153` | `a00985b2` | Merged and `main`-gated | | Deprecated IAP submit discoverability | #1787 | `04b52a62` | `38fa9b5f` | Merged and `main`-gated | | Transitive age-rating dependency closure | #1788 | `08003c38` | `9282e82d` | Merged and `main`-gated | | Final 4.4.1 coverage ledger | #1789 | `51b3a962` | `d6d8d94b` | Merged and `main`-gated; documentation only | | Subscription localization delete-confirmation coverage | #1790 | `49eda126` | `73466720` | Merged; test only | | Hardened public 4.4.1 command workflows | #1791 | `e9c2a0dc` | `e902d375` | Merged; documentation only | | Seven-property nullable request fidelity | #1792 | `3f7d1449` | `804624cd` | Merged; exact final head landed on `main` | | IAP and promoted-purchase related sparse fields | #1793 | `917d719d` | `f6b34d9e` | Merged; exact final head, six resolved threads, and green exact-head gates | | Subscription and pricing related sparse fields | #1795 | `229dc07c` | `5bf2d154` | Merged; exact final head landed on `main` | | App-info, age-rating, and Xcode Cloud related sparse fields | #1796 | `8b4821a4` | `48e0d003` | Merged and `main`-gated; exact final head, four resolved threads, and green exact-head gates | | External ASC workflow skills | rorkai/app-store-connect-cli-skills#51 and #52 | `1aeb0dc607d8fa327501bc4b1d1cf981448512f9` | `f8f43c29d96a85792b99a8a1f23a7f048f8b312d` | Merged; final cross-repository audit passed 23/23 skills and 695/695 runnable command occurrences; zero review threads | The hard audit fixed contract gaps beyond the initial six implementation PRs: endpoint-exact fields, includes, sparse fields, and relationship limits; opaque continuation URLs; next-aware argument exclusivity; review-response links and metadata; positional-argument rejection; age-rating prerequisite declarations; adjusted-equalization required filters; exact deprecation warnings and migration guidance; and rendered parent-help discoverability for all 29 deprecated leaves. A post-merge thread audit found the final transitive age-rating contradiction omitted by #1782; #1788 added both sparse-update regression cases, landed green, and closed that thread. The later cross-verification found one remaining typed-request gap: seven properties marked optional and nullable by OpenAPI could represent omission and a value, but not explicit JSON `null`. #1792 is the first implementation to preserve all three states for `socialMedia`, `socialMediaAgeRestricted`, the two v2 localization `description` properties, v2 group-localization `customAppName`, and the two v2 image `uploaded` properties. Its exact audited final PR head is `3f7d14495fcb3b696692bee4955e36fd2f36c63f`. That head also preserves the legacy omission behavior for a whitespace-only `customAppName` while retaining explicit JSON `null`; it landed on `main` as `804624cd158d1eb8843d8e0be7cf55bc639da0a1`. The fully merged behavior integration is #1796 at `main` commit `48e0d003ebde8d2046b0a19463526c95c3bc25e4`; #1789 previously landed this coverage ledger at `d6d8d94b`, test-only #1790 landed at `73466720`, docs-only #1791 landed at `e902d375`, and nullable-fidelity #1792 landed at `804624cd`; IAP sparse-field #1793 landed at `f6b34d9e`, and subscription/pricing sparse-field #1795 landed at `5bf2d154`. The 37-path, 47-operation, 47-schema, 102-direct, 71-transitive, 173-contract, 61-modified-schema, and 9-addition/7-deprecation counts are unchanged. The recursive built-help comparison from `839c4da6` to `9282e82d` found 48 added and 52 changed leaf paths, 100 affected paths total, with zero removals; it is historical help-surface evidence rather than the nullable request-encoding proof, which is carried by #1792's typed tests. PR `#1796` was audited at exact final head `8b4821a4529ae3a257dc56945a67dbef0ab7ac6b` and landed as `48e0d003ebde8d2046b0a19463526c95c3bc25e4`, completing all eight app-info, age-rating, and Xcode Cloud sparse-query transports. No behavior follow-up remains open. ## Live App Store Connect verification All manual commands used `ASC_BYPASS_KEYCHAIN=1` and the disposable app `6759231657`. The exact 4.4.1 path delta was exercised against the live service: | Domain | Operations | Live result | | --- | ---: | --- | | IAP versions, localizations, and images | 18 | All added operations received a successful live response | | Subscription versions, localizations, and images | 18 | All added operations received a successful live response | | Subscription-group versions and localizations | 10 | All added operations received a successful live response | | Adjusted equalizations | 1 | Successful with the required upfront price point and plan type | | **Total** | **47** | **29 GET, 8 POST, 5 PATCH, and 5 DELETE operations on 37 paths** | The live run covered version creation and readback, localization CRUD, complete image reserve/upload/commit/read/delete lifecycles, relationship endpoints, and pagination. A subscription-group version review item was added, updated, and removed. IAP and subscription version-item attempts reached Apple and returned the expected readiness rejection because the throwaway products lacked review prerequisites; no review submission was submitted. All four current `READY_FOR_REVIEW` submissions contained zero items at closeout. Live behavior refined five schema-level assumptions: - Adjusted equalizations succeed only when both an upfront price point and `--plan-type MONTHLY` are supplied. #1783 now enforces both before HTTP. - Setting `socialMediaAgeRestricted=true` required `userGeneratedContent=true`, `ageAssurance=true`, and `socialMedia=true`. #1782 and #1788 reject all 25 sparse flag combinations that are provably contradictory without reading stored state. All four attributes were restored to `false` afterward. - Apple rejects empty and explicit JSON-null descriptions for IAP-version and subscription-version localizations even though OpenAPI marks the attribute nullable. Non-empty PATCH requests succeed; the CLI retains schema-correct encoding while the docs and skills record the live restriction. - Five retained validator v1 reads returned the same IDs for v2-created group localizations, subscription localizations, and subscription images. A runtime validator migration was therefore a live-verified no-op, not an assumption. - Apple production returned `400` for the official `fields[appInfos]=kidsAgeBand` selector on app-info detail and collection reads. The CLI retains the published 4.4.1 contract and records this as an upstream rollout lag. The other new app-carried IAP/subscription-group and age-rating sparse fields returned `200`; the disposable app has no Xcode Cloud product, so that transport remains deterministically HTTP-tested. PR #1792 made no new live explicit-null claim. It added typed-client fidelity and table-driven omit/value/null encoding tests only. The existing CLI commands continue to send the same concrete values or omissions as before; they do not gain a new clear/null flag. No explicit-null request for the age-rating, group localization, or image-upload fields was sent to App Store Connect for #1792, and the earlier live rejection of null localization descriptions remains the only live null evidence. Cleanup was verified by ID. Image resources, secondary localizations, review items, and temporary group-version resources were deleted where the API permits it. All four review submissions were empty at closeout. | Resource | Cleanup result | | --- | --- | | IAP parent `6791819283` | Deleted | | IAP version `07d9113f-2a96-43d0-8399-914deeaa49d4` | Remains orphaned; Apple exposes no version DELETE and parent deletion did not cascade | | IAP localization `0073ad1d-879d-41f4-bb7d-49c73f0479b5` | Remains because Apple rejects deletion of the final required localization | | Subscription group `22243347` and subscription `6791819604` | Deleted | | Subscription version `6f861af5-23c9-4e3c-807a-9879220c051f` | Remains orphaned | | Subscription localization `9fc52d69-2eed-440c-8053-0646de35daa8` | Remains because it is the final required localization | | Group `22243440` and all version/localization descendants | Deleted or cascaded; verified gone | | Validator-test group `22243740` and its group-version/group-localization descendants | Deleted; verified gone | | Validator-test subscription `6791895240` | Deleted | | Subscription version `1dcba57e-f03f-49e0-a24e-d048fb6dd479` | Remains orphaned | | Subscription localization `67326a94-973a-4061-a991-dc7061018232` | Remains because it is the final required localization | These six retained version/localization resources are confined to the disposable app. No presigned upload URL, credential, or non-disposable app data is recorded. After every CLI behavior change was merged, a final read-only smoke with the locally built exact-`main` `48e0d003` CLI re-read app `6759231657`, its full age-rating declaration, all four review submissions, every submission's items, and all retained resource IDs. The app read succeeded; `userGeneratedContent`, `ageAssurance`, `socialMedia`, and `socialMediaAgeRestricted` were all `false`; every submission returned no items; all six retained resources remained readable; and every parent or container recorded as deleted still returned not found. The subsequent skills-only #52 help-path correction did not change CLI or live behavior. ## Definition of done - Every added operation below is implemented and tested, or marked schema-only with a concrete rationale. - Every one of the 102 modified existing operations is classified as a new query/response behavior, a deprecation reversal, or a change already covered before the schema PR. - All 71 schema-mediated operation-contract changes that are not direct path-item diffs are classified separately. Two change request contracts; all 71 change response contracts through referenced schemas. - All 47 added and 61 modified schemas decode and encode through typed models where user-facing behavior depends on them, or have an explicit schema-only disposition. - The three new version resource types can be created, listed, viewed, and submitted through discoverable CLI commands. - Version-scoped localization and image workflows support create, read, update, delete, pagination, and uploads where the API permits them. - Existing product-ID and group-ID commands retain their current behavior until they have an explicit deprecation warning, migration command, and transition tests. A version ID is never silently substituted for a product or group ID. - `asc schema` exposes relationship fields needed to construct relationship-only requests, and CI fails when either generated schema index is stale. - Command documentation, API notes, migration guidance, and external workflow skills reflect the final command surface. - Focused tests, adjacent tests, built-binary checks, the full repository gate, GitHub checks, and appropriate live verification are green on each latest PR head. ## Added operation ledger ### In-app purchase versions and version-scoped metadata: 18 | Method | Path | Required behavior | Disposition | Owner | Evidence | | --- | --- | --- | --- | --- | --- | | `POST` | `/v1/inAppPurchaseVersions` | Create a version for an IAP relationship | Implemented typed command | #1777 | `ee40c7b3`; HTTP body and built-command tests | | `GET` | `/v1/inAppPurchaseVersions/{id}` | View a version | Implemented typed command | #1777 | `ee40c7b3`; HTTP query and built-command tests | | `GET` | `/v2/inAppPurchases/{id}/versions` | List related versions with pagination | Implemented typed command | #1777 | `ee40c7b3`; exact query and pagination tests | | `GET` | `/v2/inAppPurchases/{id}/relationships/versions` | List version linkages | Implemented typed client/command | #1777 | `ee40c7b3`; linkage response tests | | `GET` | `/v1/inAppPurchaseVersions/{id}/localizations` | List version localizations | Implemented typed command | #1777 | `ee40c7b3`; exact path/query tests | | `GET` | `/v1/inAppPurchaseVersions/{id}/relationships/localizations` | List localization linkages | Implemented typed client/command | #1777 | `ee40c7b3`; linkage response tests | | `GET` | `/v1/inAppPurchaseVersions/{id}/image` | Get the singular review image | Implemented typed command | #1777 | `ee40c7b3`; singular response tests | | `GET` | `/v1/inAppPurchaseVersions/{id}/relationships/image` | Get singular image linkage | Implemented typed client/command | #1777 | `ee40c7b3`; linkage response tests | | `GET` | `/v1/inAppPurchaseVersions/{id}/images` | List review images | Implemented typed command | #1777 | `ee40c7b3`; list and pagination tests | | `GET` | `/v1/inAppPurchaseVersions/{id}/relationships/images` | List image linkages | Implemented typed client/command | #1777 | `ee40c7b3`; linkage response tests | | `POST` | `/v2/inAppPurchaseLocalizations` | Create a version-scoped localization | Implemented typed command | #1777 | `ee40c7b3`; exact create payload tests | | `GET` | `/v2/inAppPurchaseLocalizations/{id}` | View a localization | Implemented typed command | #1777 | `ee40c7b3`; detail response tests | | `PATCH` | `/v2/inAppPurchaseLocalizations/{id}` | Update a localization | Implemented typed command | #1777 | `ee40c7b3`; omitted/value/null payload tests | | `DELETE` | `/v2/inAppPurchaseLocalizations/{id}` | Delete a localization with confirmation | Implemented typed command | #1777 | `ee40c7b3`; confirmation and HTTP tests | | `POST` | `/v2/inAppPurchaseImages` | Reserve and upload a version-scoped image | Implemented upload command | #1777 | `ee40c7b3`; reserve/upload lifecycle tests | | `GET` | `/v2/inAppPurchaseImages/{id}` | View an image and upload state | Implemented typed command | #1777 | `ee40c7b3`; detail response tests | | `PATCH` | `/v2/inAppPurchaseImages/{id}` | Commit uploaded parts | Implemented upload command | #1777 | `ee40c7b3`; checksum and commit tests | | `DELETE` | `/v2/inAppPurchaseImages/{id}` | Delete an image with confirmation | Implemented typed command | #1777 | `ee40c7b3`; confirmation and HTTP tests | The review-submission relationship for `inAppPurchaseVersion` modifies the existing `/v1/reviewSubmissionItems` operation rather than adding another path. ### Subscription versions and version-scoped metadata: 18 | Method | Path | Required behavior | Disposition | Owner | Evidence | | --- | --- | --- | --- | --- | --- | | `POST` | `/v1/subscriptionVersions` | Create a version for a subscription relationship | Implemented typed command | #1779 | `c8f3ab52`; HTTP body and built-command tests | | `GET` | `/v1/subscriptionVersions/{id}` | View a version | Implemented typed command | #1779 | `c8f3ab52`; HTTP query and built-command tests | | `GET` | `/v1/subscriptions/{id}/versions` | List related versions with pagination | Implemented typed command | #1779 | `c8f3ab52`; exact query and pagination tests | | `GET` | `/v1/subscriptions/{id}/relationships/versions` | List version linkages | Implemented typed client/command | #1779 | `c8f3ab52`; linkage response tests | | `GET` | `/v1/subscriptionVersions/{id}/localizations` | List version localizations | Implemented typed command | #1779 | `c8f3ab52`; exact path/query tests | | `GET` | `/v1/subscriptionVersions/{id}/relationships/localizations` | List localization linkages | Implemented typed client/command | #1779 | `c8f3ab52`; linkage response tests | | `GET` | `/v1/subscriptionVersions/{id}/image` | Get the singular promotional image | Implemented typed command | #1779 | `c8f3ab52`; singular response tests | | `GET` | `/v1/subscriptionVersions/{id}/relationships/image` | Get singular image linkage | Implemented typed client/command | #1779 | `c8f3ab52`; linkage response tests | | `GET` | `/v1/subscriptionVersions/{id}/images` | List promotional images | Implemented typed command | #1779 | `c8f3ab52`; list and pagination tests | | `GET` | `/v1/subscriptionVersions/{id}/relationships/images` | List image linkages | Implemented typed client/command | #1779 | `c8f3ab52`; linkage response tests | | `POST` | `/v2/subscriptionLocalizations` | Create a version-scoped localization | Implemented typed command | #1779 | `c8f3ab52`; exact create payload tests | | `GET` | `/v2/subscriptionLocalizations/{id}` | View a localization | Implemented typed command | #1779 | `c8f3ab52`; detail response tests | | `PATCH` | `/v2/subscriptionLocalizations/{id}` | Update a localization | Implemented typed command | #1779 | `c8f3ab52`; omitted/value/null payload tests | | `DELETE` | `/v2/subscriptionLocalizations/{id}` | Delete a localization with confirmation | Implemented typed command | #1779 | `c8f3ab52`; confirmation and HTTP tests | | `POST` | `/v2/subscriptionImages` | Reserve and upload a version-scoped image | Implemented upload command | #1779 | `c8f3ab52`; reserve/upload lifecycle tests | | `GET` | `/v2/subscriptionImages/{id}` | View an image and upload state | Implemented typed command | #1779 | `c8f3ab52`; detail response tests | | `PATCH` | `/v2/subscriptionImages/{id}` | Commit uploaded parts | Implemented upload command | #1779 | `c8f3ab52`; checksum and commit tests | | `DELETE` | `/v2/subscriptionImages/{id}` | Delete an image with confirmation | Implemented typed command | #1779 | `c8f3ab52`; confirmation and HTTP tests | The review-submission relationship for `subscriptionVersion` modifies the existing `/v1/reviewSubmissionItems` operation. ### Subscription-group versions and localizations: 10 | Method | Path | Required behavior | Disposition | Owner | Evidence | | --- | --- | --- | --- | --- | --- | | `POST` | `/v1/subscriptionGroupVersions` | Create a version for a group relationship | Implemented typed command | #1780 | `48d04e0b`; HTTP body and built-command tests | | `GET` | `/v1/subscriptionGroupVersions/{id}` | View a version | Implemented typed command | #1780 | `48d04e0b`; HTTP query and built-command tests | | `GET` | `/v1/subscriptionGroups/{id}/versions` | List related versions with pagination | Implemented typed command | #1780 | `48d04e0b`; exact query, owner/next validation, and pagination tests | | `GET` | `/v1/subscriptionGroups/{id}/relationships/versions` | List version linkages | Implemented typed client/command | #1780 | `48d04e0b`; owner/next validation and linkage response tests | | `GET` | `/v1/subscriptionGroupVersions/{id}/localizations` | List version localizations | Implemented typed command | #1780 | `48d04e0b`; exact path/query and owner/next validation tests | | `GET` | `/v1/subscriptionGroupVersions/{id}/relationships/localizations` | List localization linkages | Implemented typed client/command | #1780 | `48d04e0b`; owner/next validation and linkage response tests | | `POST` | `/v2/subscriptionGroupLocalizations` | Create a version-scoped localization | Implemented typed command | #1780 | `48d04e0b`; exact create payload tests | | `GET` | `/v2/subscriptionGroupLocalizations/{id}` | View a localization | Implemented typed command | #1780 | `48d04e0b`; detail response tests | | `PATCH` | `/v2/subscriptionGroupLocalizations/{id}` | Update a localization | Implemented typed command | #1780 | `48d04e0b`; omitted/value/null payload tests | | `DELETE` | `/v2/subscriptionGroupLocalizations/{id}` | Delete a localization with confirmation | Implemented typed command | #1780 | `48d04e0b`; confirmation and HTTP tests | The review-submission relationship for `subscriptionGroupVersion` modifies the existing `/v1/reviewSubmissionItems` operation. ### Version review-submission coverage All three types use `POST /v1/reviewSubmissionItems` with required `reviewSubmission.data` plus exactly one version relationship. The discoverable generic command is `asc review items add --submission "SUBMISSION_ID" --item-type "TYPE" --item-id "VERSION_ID"`; domain-specific submit shortcuts may delegate to the same typed client after their ID semantics are explicit. | Version type | Relationship payload | `--item-type` | Required test evidence | Status | | --- | --- | --- | --- | --- | | IAP | `inAppPurchaseVersion.data.type=inAppPurchaseVersions` | `inAppPurchaseVersions` | HTTP body test plus built command test | Implemented in #1777 at `ee40c7b3` and cross-verified in #1781 at `aba35e0a` | | Subscription | `subscriptionVersion.data.type=subscriptionVersions` | `subscriptionVersions` | HTTP body test plus built command test | Implemented in #1781 at `aba35e0a` | | Subscription group | `subscriptionGroupVersion.data.type=subscriptionGroupVersions` | `subscriptionGroupVersions` | HTTP body test plus built command test | Implemented in #1781 at `aba35e0a` | The exact directly modified-operation checklist includes the four review-submission read operations whose sparse fields and includes gain the three version relationships. The `POST` request change is schema-mediated and is tracked separately below because the operation object itself is unchanged. ### Subscription adjusted equalizations: 1 | Method | Path | Required behavior | Disposition | Owner | Evidence | | --- | --- | --- | --- | --- | --- | | `GET` | `/v1/subscriptionPricePoints/{id}/adjustedEqualizations` | List adjusted equalized price points using the exact territory, subscription, upfront-price-point, and plan-type filters supported by this operation | Implemented typed command | #1778 | `39284b0a`; exact query/response, strict CSV/enums, territory inclusion, opaque-next, ID validation, aggregation, and conflict tests | ## Modified existing contract ledger The original 4.4-to-4.4.1 diff modifies 102 existing operations. Fifty remain different from the immediate pre-PR repository snapshot and expand query or response contracts. The other 52 were already reconciled in the repository after the original 4.4 import: 44 reverse OpenAPI `deprecated: true` flags and eight add media-localization sparse-field parameters. Each item remains in the checklist so "already covered" is an audited disposition rather than an omitted change. | Contract area | Semantic change | Verification owner | Exact evidence | | --- | --- | --- | --- | | IAP reads | `fields[inAppPurchases]` gains `versions`; IAP detail and app-IAP collection reads gain `include=versions`, `fields[inAppPurchaseVersions]`, and `limit[versions]` | #1777, #1793, and #1796 | `ee40c7b3`; #1793 final head `917d719d`, landed as `f6b34d9e`, adds all 11 propagated IAP/promoted-purchase GETs; #1796 final head `8b4821a4`, landed as `48e0d003`, adds the three app and CI-product transports | | Subscription reads | Subscription detail and group-subscription reads gain version includes, sparse fields, and relationship limits; subscription sparse fields gain `versions` across related endpoints | #1779 and #1795 | `c8f3ab52`; #1795 final head `229dc07c`, landed as `5bf2d154`, adds exact query, stable owner selection, opaque-pagination, and compatibility tests across all 17 propagated subscription/pricing GETs | | Subscription-group reads | Group detail and app-group collection reads gain version includes, sparse fields, and relationship limits; group sparse fields gain `versions` | #1780, #1795, and #1796 | `48d04e0b`; #1795 at `229dc07c`, landed as `5bf2d154`, adds two propagated group-field GETs; #1796 at `8b4821a4`, landed as `48e0d003`, adds the three app and CI-product transports | | Review submission reads | Review-item sparse fields and includes gain `inAppPurchaseVersion`, `subscriptionVersion`, and `subscriptionGroupVersion` | #1781 | `aba35e0a`; all four changed GET surfaces, automatic item inclusion, and response round-trip tests | | Pricing reads | Price-point sparse fields gain `adjustedEqualizations`; existing equalization and price-point relationship operations gain `filter[upfrontPricePointId]` and `filter[planType]` where allowed | #1778 and #1795 | `39284b0a` adds endpoint-specific filters, strict CSV/enums, territory inclusion, opaque-next, ID validation, and aggregation; #1795 final head `229dc07c`, landed as `5bf2d154`, adds the propagated sparse-field transports | | Age rating reads and update | Age-rating sparse fields and update schema gain `socialMedia` and `socialMediaAgeRestricted` | #1778, #1792, and #1796 | `39284b0a` added the fields and CLI behavior; #1792 at `3f7d1449`, landed as `804624cd`, adds omit/value/null encoding; #1796 at `8b4821a4`, landed as `48e0d003`, completes direct and included sparse-query transport | | App info reads | `AppInfo.attributes.kidsAgeBand` and `fields[appInfos]=kidsAgeBand` appear as deprecated additions | #1778 and #1796 | #1778 at `39284b0a` adds response decoding and output characterization; #1796 final head `8b4821a4`, landed as `48e0d003`, adds operation-specific query transport, CLI flags and automatic includes, strict validation, and query-cardinality tests across all seven `fields[appInfos]` reads | | Included-resource unions | IAP, subscription, group, and review-submission responses gain their corresponding version resource discriminators | #1777, #1779, #1780, #1781 | `ee40c7b3`, `c8f3ab52`, `48d04e0b`, `aba35e0a`; typed response and included-resource tests | No existing operation changes from nondeprecated to `deprecated: true` in the OpenAPI JSON. Forty-four operations instead reverse `deprecated: true` from the original 4.4 snapshot, primarily screenshot and preview resources. Separately, Apple's prose release notes deprecate the seven version-replaced resource families below without setting new operation-level flags. Deprecation behavior therefore cannot be inferred solely from OpenAPI flags. ## Added and modified schema ledger Schema discovery and drift enforcement are implemented in #1776 at `aaa9b62d`: create/update request relationships are exposed through `asc schema`, referenced relationship schemas are resolved recursively, and both generated indexes fail their tests when stale. The 47 added schemas break down into: - 18 IAP schemas: version resources/linkages/responses, localization v2 CRUD requests/responses, and image v2 CRUD/upload requests/responses. - 18 subscription schemas: version resources/linkages/responses, localization v2 CRUD requests/responses, and image v2 CRUD/upload requests/responses. - 11 subscription-group schemas: version resources/linkages/responses and localization v2 CRUD requests/responses. The exact 61-schema modified-contract checklist is split by whether code work remained at the immediate pre-PR base. Still different before the 4.4.1 schema PR: - [x] `AgeRatingDeclaration` - two social-media Boolean attributes (#1778, `39284b0a`) - [x] `AgeRatingDeclarationUpdateRequest` - two social-media update attributes added in #1778 (`39284b0a`); exact omit/value/null fidelity landed from #1792 at `804624cd` - [x] `AppInfo` - deprecated `kidsAgeBand` read attribute (#1778, `39284b0a`) - [x] `InAppPurchaseV2` - versions relationship (#1777, `ee40c7b3`) - [x] `InAppPurchaseV2Response` - included IAP-version discriminator (#1777, `ee40c7b3`) - [x] `InAppPurchasesV2Response` - included IAP-version discriminator (#1777, `ee40c7b3`) - [x] `ReviewSubmissionItem` - three version relationships (#1781, `aba35e0a`) - [x] `ReviewSubmissionItemCreateRequest` - three version create relationships (#1781, `aba35e0a`) - [x] `ReviewSubmissionItemResponse` - three included version discriminators (#1781, `aba35e0a`) - [x] `ReviewSubmissionItemsResponse` - three included version discriminators (#1781, `aba35e0a`) - [x] `Subscription` - versions relationship (#1779, `c8f3ab52`) - [x] `SubscriptionGroup` - versions relationship (#1780, `48d04e0b`) - [x] `SubscriptionGroupResponse` - included group-version discriminator (#1780, `48d04e0b`) - [x] `SubscriptionGroupsResponse` - included group-version discriminator (#1780, `48d04e0b`) - [x] `SubscriptionPricePoint` - adjusted-equalizations relationship (#1778, `39284b0a`) - [x] `SubscriptionResponse` - included subscription-version discriminator (#1779, `c8f3ab52`) - [x] `SubscriptionsResponse` - included subscription-version discriminator (#1779, `c8f3ab52`) Already reconciled after the original 4.4 import and retained as audited schema-only dispositions: - [x] `AppCustomProductPageLocalization` - media sparse-field propagation already present - [x] `AppCustomProductPageLocalizationAppPreviewSetsLinkagesResponse` - deprecation reversal already present - [x] `AppCustomProductPageLocalizationAppScreenshotSetsLinkagesResponse` - deprecation reversal already present - [x] `AppEventLocalization` - media sparse-field propagation already present - [x] `AppEventLocalizationAppEventScreenshotsLinkagesResponse` - deprecation reversal already present - [x] `AppEventLocalizationAppEventVideoClipsLinkagesResponse` - deprecation reversal already present - [x] `AppEventScreenshot` - deprecation reversal already present - [x] `AppEventScreenshotCreateRequest` - deprecation reversal already present - [x] `AppEventScreenshotResponse` - deprecation reversal already present - [x] `AppEventScreenshotUpdateRequest` - deprecation reversal already present - [x] `AppEventScreenshotsResponse` - deprecation reversal already present - [x] `AppEventVideoClip` - deprecation reversal already present - [x] `AppEventVideoClipCreateRequest` - deprecation reversal already present - [x] `AppEventVideoClipResponse` - deprecation reversal already present - [x] `AppEventVideoClipUpdateRequest` - deprecation reversal already present - [x] `AppEventVideoClipsResponse` - deprecation reversal already present - [x] `AppPreview` - deprecation reversal already present - [x] `AppPreviewCreateRequest` - deprecation reversal already present - [x] `AppPreviewResponse` - deprecation reversal already present - [x] `AppPreviewSet` - deprecation reversal already present - [x] `AppPreviewSetAppPreviewsLinkagesRequest` - deprecation reversal already present - [x] `AppPreviewSetAppPreviewsLinkagesResponse` - deprecation reversal already present - [x] `AppPreviewSetCreateRequest` - deprecation reversal already present - [x] `AppPreviewSetResponse` - deprecation reversal already present - [x] `AppPreviewSetsResponse` - deprecation reversal already present - [x] `AppPreviewUpdateRequest` - deprecation reversal already present - [x] `AppPreviewsResponse` - deprecation reversal already present - [x] `AppScreenshot` - deprecation reversal already present - [x] `AppScreenshotCreateRequest` - deprecation reversal already present - [x] `AppScreenshotResponse` - deprecation reversal already present - [x] `AppScreenshotSet` - deprecation reversal already present - [x] `AppScreenshotSetAppScreenshotsLinkagesRequest` - deprecation reversal already present - [x] `AppScreenshotSetAppScreenshotsLinkagesResponse` - deprecation reversal already present - [x] `AppScreenshotSetCreateRequest` - deprecation reversal already present - [x] `AppScreenshotSetResponse` - deprecation reversal already present - [x] `AppScreenshotSetsResponse` - deprecation reversal already present - [x] `AppScreenshotUpdateRequest` - deprecation reversal already present - [x] `AppScreenshotsResponse` - deprecation reversal already present - [x] `AppStoreVersionExperimentTreatmentLocalization` - media sparse-field propagation already present - [x] `AppStoreVersionExperimentTreatmentLocalizationAppPreviewSetsLinkagesResponse` - deprecation reversal already present - [x] `AppStoreVersionExperimentTreatmentLocalizationAppScreenshotSetsLinkagesResponse` - deprecation reversal already present - [x] `AppStoreVersionLocalization` - media sparse-field propagation already present - [x] `AppStoreVersionLocalizationAppPreviewSetsLinkagesResponse` - deprecation reversal already present - [x] `AppStoreVersionLocalizationAppScreenshotSetsLinkagesResponse` - deprecation reversal already present ### Exact added schema checklist IAP ownership (#1777 at `ee40c7b3`; typed models plus request/response and round-trip tests): - [x] `InAppPurchaseVersion` - [x] `InAppPurchaseVersionCreateRequest` - [x] `InAppPurchaseVersionResponse` - [x] `InAppPurchaseVersionsResponse` - [x] `InAppPurchaseV2VersionsLinkagesResponse` - [x] `InAppPurchaseVersionImageLinkageResponse` - [x] `InAppPurchaseVersionImagesLinkagesResponse` - [x] `InAppPurchaseVersionLocalizationsLinkagesResponse` - [x] `InAppPurchaseLocalizationV2` - [x] `InAppPurchaseLocalizationV2CreateRequest` - nullable create description completed by #1792, landed at `804624cd` - [x] `InAppPurchaseLocalizationV2UpdateRequest` - [x] `InAppPurchaseLocalizationV2Response` - [x] `InAppPurchaseLocalizationsV2Response` - [x] `InAppPurchaseImageV2` - [x] `InAppPurchaseImageV2CreateRequest` - [x] `InAppPurchaseImageV2UpdateRequest` - nullable uploaded state completed by #1792, landed at `804624cd` - [x] `InAppPurchaseImageV2Response` - [x] `InAppPurchaseImagesV2Response` Subscription ownership (#1779 at `c8f3ab52`; typed models plus request/response and round-trip tests): - [x] `SubscriptionVersion` - [x] `SubscriptionVersionCreateRequest` - [x] `SubscriptionVersionResponse` - [x] `SubscriptionVersionsResponse` - [x] `SubscriptionVersionsLinkagesResponse` - [x] `SubscriptionVersionImageLinkageResponse` - [x] `SubscriptionVersionImagesLinkagesResponse` - [x] `SubscriptionVersionLocalizationsLinkagesResponse` - [x] `SubscriptionLocalizationV2` - [x] `SubscriptionLocalizationV2CreateRequest` - nullable create description completed by #1792, landed at `804624cd` - [x] `SubscriptionLocalizationV2UpdateRequest` - [x] `SubscriptionLocalizationV2Response` - [x] `SubscriptionLocalizationsV2Response` - [x] `SubscriptionImageV2` - [x] `SubscriptionImageV2CreateRequest` - [x] `SubscriptionImageV2UpdateRequest` - nullable uploaded state completed by #1792, landed at `804624cd` - [x] `SubscriptionImageV2Response` - [x] `SubscriptionImagesV2Response` Subscription-group ownership (#1780 at `48d04e0b`; typed models plus request/response and round-trip tests): - [x] `SubscriptionGroupVersion` - [x] `SubscriptionGroupVersionCreateRequest` - [x] `SubscriptionGroupVersionResponse` - [x] `SubscriptionGroupVersionsResponse` - [x] `SubscriptionGroupVersionsLinkagesResponse` - [x] `SubscriptionGroupVersionLocalizationsLinkagesResponse` - [x] `SubscriptionGroupLocalizationV2` - [x] `SubscriptionGroupLocalizationV2CreateRequest` - nullable custom app name completed by #1792, landed at `804624cd` - [x] `SubscriptionGroupLocalizationV2UpdateRequest` - [x] `SubscriptionGroupLocalizationV2Response` - [x] `SubscriptionGroupLocalizationsV2Response` ### Exact modified-operation checklist This checklist contains exactly the 102 operations whose path-item operation objects differ between 4.4 and 4.4.1: 50 behavior changes that remained at the immediate pre-PR base plus 52 changes already reconciled after the 4.4 import. Schema-mediated request-contract changes are listed separately after it and do not alter this count. Age rating and app info (#1778 at `39284b0a` covers response decoding and update behavior; #1796 final head `8b4821a4`, landed as `48e0d003`, covers exact sparse-query transport, CLI flags, and query-cardinality tests): - [x] `GET /v1/appInfoLocalizations/{id}` - [x] `GET /v1/appInfos/{id}` - [x] `GET /v1/appInfos/{id}/ageRatingDeclaration` - [x] `GET /v1/appInfos/{id}/appInfoLocalizations` - [x] `GET /v1/apps` - [x] `GET /v1/apps/{id}` - [x] `GET /v1/apps/{id}/appInfos` - [x] `GET /v1/ciProducts/{id}/app` IAP and promoted-purchase propagation (#1777 at `ee40c7b3` plus #1793 final head `917d719d`, landed as `f6b34d9e`; endpoint-exact query and response compatibility tests). #1793 closes 11 of the 13 GETs below while preserving the two top-level IAP list/detail behaviors from #1777: - [x] `GET /v1/apps/{id}/inAppPurchasesV2` - [x] `GET /v1/inAppPurchaseAppStoreReviewScreenshots/{id}` - [x] `GET /v1/inAppPurchaseContents/{id}` - [x] `GET /v1/inAppPurchaseImages/{id}` - [x] `GET /v1/inAppPurchaseLocalizations/{id}` - [x] `GET /v1/promotedPurchases/{id}` - [x] `GET /v2/inAppPurchases/{id}` - [x] `GET /v2/inAppPurchases/{id}/appStoreReviewScreenshot` - [x] `GET /v2/inAppPurchases/{id}/content` - [x] `GET /v2/inAppPurchases/{id}/images` - [x] `GET /v2/inAppPurchases/{id}/inAppPurchaseLocalizations` - [x] `GET /v2/inAppPurchases/{id}/promotedPurchase` - [x] `GET /v1/apps/{id}/promotedPurchases` Review submissions (#1781 at `aba35e0a`; exact sparse-field/include tests plus `links`, `included`, and `meta` response round trips): - [x] `GET /v1/reviewSubmissions` - [x] `GET /v1/reviewSubmissions/{id}` - [x] `GET /v1/reviewSubmissions/{id}/items` - [x] `GET /v1/apps/{id}/reviewSubmissions` Review-item includes are exhaustive for the endpoint. Sparse fields for related resources are exact for the 4.4.1 delta, but ten older related-resource sparse groups remain outside this slice: `appStoreVersions`, `appCustomProductPageVersions`, `appStoreVersionExperiments`, `appEvents`, `backgroundAssetVersions`, and five Game Center resource groups. The API has no review-item detail GET; deprecated detail stubs therefore remain explicit errors. Item PATCH preserves nullable `resolved` and `removed` without adding a confirmation prompt, while submission PATCH preserves nullable `platform`, `submitted`, and `canceled` and still requires confirmation. Create targets are restricted to the exact version resource types. The pre-existing submission- create command still requires a concrete platform even though the schema permits omission or `null`; changing that stable behavior is outside this compatibility slice. Subscriptions, groups, and pricing (#1778 at `39284b0a`, #1779 at `c8f3ab52`, PR `#1780` at `48d04e0b`, and #1795 final head `229dc07c`, landed as `5bf2d154`; endpoint-exact query, response, compatibility, and opaque-pagination tests): - [x] `GET /v1/apps/{id}/subscriptionGroups` - [x] `GET /v1/subscriptionAppStoreReviewScreenshots/{id}` - [x] `GET /v1/subscriptionGroupLocalizations/{id}` - [x] `GET /v1/subscriptionGroups/{id}` - [x] `GET /v1/subscriptionGroups/{id}/subscriptionGroupLocalizations` - [x] `GET /v1/subscriptionGroups/{id}/subscriptions` - [x] `GET /v1/subscriptionImages/{id}` - [x] `GET /v1/subscriptionLocalizations/{id}` - [x] `GET /v1/subscriptionOfferCodes/{id}` - [x] `GET /v1/subscriptionOfferCodes/{id}/prices` - [x] `GET /v1/subscriptionPricePoints/{id}` - [x] `GET /v1/subscriptionPricePoints/{id}/equalizations` - [x] `GET /v1/subscriptionPromotionalOffers/{id}` - [x] `GET /v1/subscriptionPromotionalOffers/{id}/prices` - [x] `GET /v1/subscriptions/{id}` - [x] `GET /v1/subscriptions/{id}/appStoreReviewScreenshot` - [x] `GET /v1/subscriptions/{id}/images` - [x] `GET /v1/subscriptions/{id}/introductoryOffers` - [x] `GET /v1/subscriptions/{id}/offerCodes` - [x] `GET /v1/subscriptions/{id}/pricePoints` - [x] `GET /v1/subscriptions/{id}/prices` - [x] `GET /v1/subscriptions/{id}/promotedPurchase` - [x] `GET /v1/subscriptions/{id}/promotionalOffers` - [x] `GET /v1/subscriptions/{id}/subscriptionLocalizations` - [x] `GET /v1/winBackOffers/{id}/prices` Already reconciled between the original 4.4 import and the immediate pre-PR base; checked items require no new behavior PR but remain part of the 102-item contract audit. Media sparse-field parameter changes: - [x] `GET /v1/appCustomProductPageLocalizations/{id}` - [x] `GET /v1/appCustomProductPageVersions/{id}/appCustomProductPageLocalizations` - [x] `GET /v1/appEventLocalizations/{id}` - [x] `GET /v1/appEvents/{id}/localizations` - [x] `GET /v1/appStoreVersionExperimentTreatmentLocalizations/{id}` - [x] `GET /v1/appStoreVersionExperimentTreatments/{id}/appStoreVersionExperimentTreatmentLocalizations` - [x] `GET /v1/appStoreVersionLocalizations/{id}` - [x] `GET /v1/appStoreVersions/{id}/appStoreVersionLocalizations` Operation deprecation reversals: - [x] `DELETE /v1/appEventScreenshots/{id}` - [x] `DELETE /v1/appEventVideoClips/{id}` - [x] `DELETE /v1/appPreviewSets/{id}` - [x] `DELETE /v1/appPreviews/{id}` - [x] `DELETE /v1/appScreenshotSets/{id}` - [x] `DELETE /v1/appScreenshots/{id}` - [x] `GET /v1/appCustomProductPageLocalizations/{id}/appPreviewSets` - [x] `GET /v1/appCustomProductPageLocalizations/{id}/appScreenshotSets` - [x] `GET /v1/appCustomProductPageLocalizations/{id}/relationships/appPreviewSets` - [x] `GET /v1/appCustomProductPageLocalizations/{id}/relationships/appScreenshotSets` - [x] `GET /v1/appEventLocalizations/{id}/appEventScreenshots` - [x] `GET /v1/appEventLocalizations/{id}/appEventVideoClips` - [x] `GET /v1/appEventLocalizations/{id}/relationships/appEventScreenshots` - [x] `GET /v1/appEventLocalizations/{id}/relationships/appEventVideoClips` - [x] `GET /v1/appEventScreenshots/{id}` - [x] `GET /v1/appEventVideoClips/{id}` - [x] `GET /v1/appPreviewSets/{id}` - [x] `GET /v1/appPreviewSets/{id}/appPreviews` - [x] `GET /v1/appPreviewSets/{id}/relationships/appPreviews` - [x] `GET /v1/appPreviews/{id}` - [x] `GET /v1/appScreenshotSets/{id}` - [x] `GET /v1/appScreenshotSets/{id}/appScreenshots` - [x] `GET /v1/appScreenshotSets/{id}/relationships/appScreenshots` - [x] `GET /v1/appScreenshots/{id}` - [x] `GET /v1/appStoreVersionExperimentTreatmentLocalizations/{id}/appPreviewSets` - [x] `GET /v1/appStoreVersionExperimentTreatmentLocalizations/{id}/appScreenshotSets` - [x] `GET /v1/appStoreVersionExperimentTreatmentLocalizations/{id}/relationships/appPreviewSets` - [x] `GET /v1/appStoreVersionExperimentTreatmentLocalizations/{id}/relationships/appScreenshotSets` - [x] `GET /v1/appStoreVersionLocalizations/{id}/appPreviewSets` - [x] `GET /v1/appStoreVersionLocalizations/{id}/appScreenshotSets` - [x] `GET /v1/appStoreVersionLocalizations/{id}/relationships/appPreviewSets` - [x] `GET /v1/appStoreVersionLocalizations/{id}/relationships/appScreenshotSets` - [x] `PATCH /v1/appEventScreenshots/{id}` - [x] `PATCH /v1/appEventVideoClips/{id}` - [x] `PATCH /v1/appPreviewSets/{id}/relationships/appPreviews` - [x] `PATCH /v1/appPreviews/{id}` - [x] `PATCH /v1/appScreenshotSets/{id}/relationships/appScreenshots` - [x] `PATCH /v1/appScreenshots/{id}` - [x] `POST /v1/appEventScreenshots` - [x] `POST /v1/appEventVideoClips` - [x] `POST /v1/appPreviewSets` - [x] `POST /v1/appPreviews` - [x] `POST /v1/appScreenshotSets` - [x] `POST /v1/appScreenshots` ### Schema-mediated operation-contract checklist These 71 operations do not appear in the 102-operation path-item diff because their operation objects are byte-for-byte unchanged. They reference one or more of the 61 modified schemas, so their effective request or response contracts still change. Two have modified request contracts and all 71 have modified response contracts. Together with the 102 directly modified operations, they produce 173 unique operation-contract audit items. Behavior work remaining at the immediate pre-PR base (40), now reconciled. `PATCH /v1/ageRatingDeclarations/{id}` is covered by #1778 at `39284b0a`; review-submission item request/response changes are covered by #1781 at `aba35e0a`; IAP, subscription, and group response propagation is covered by PR `#1777` at `ee40c7b3`, #1779 at `c8f3ab52`, and #1780 at `48d04e0b`. Checked response-only operations retain their existing command semantics and decode the expanded typed relationships without introducing a new flag or ID contract. - [x] `PATCH /v1/ageRatingDeclarations/{id}` - request and response - [x] `PATCH /v1/appInfoLocalizations/{id}` - [x] `PATCH /v1/appInfos/{id}` - [x] `PATCH /v1/apps/{id}` - [x] `PATCH /v1/inAppPurchaseAppStoreReviewScreenshots/{id}` - [x] `PATCH /v1/inAppPurchaseImages/{id}` - [x] `PATCH /v1/inAppPurchaseLocalizations/{id}` - [x] `PATCH /v1/promotedPurchases/{id}` - [x] `PATCH /v1/reviewSubmissionItems/{id}` - [x] `PATCH /v1/reviewSubmissions/{id}` - [x] `PATCH /v1/subscriptionAppStoreReviewScreenshots/{id}` - [x] `PATCH /v1/subscriptionGroupLocalizations/{id}` - [x] `PATCH /v1/subscriptionGroups/{id}` - [x] `PATCH /v1/subscriptionImages/{id}` - [x] `PATCH /v1/subscriptionIntroductoryOffers/{id}` - [x] `PATCH /v1/subscriptionLocalizations/{id}` - [x] `PATCH /v1/subscriptionOfferCodes/{id}` - [x] `PATCH /v1/subscriptionPromotionalOffers/{id}` - [x] `PATCH /v1/subscriptions/{id}` - [x] `PATCH /v2/inAppPurchases/{id}` - [x] `POST /v1/appInfoLocalizations` - [x] `POST /v1/inAppPurchaseAppStoreReviewScreenshots` - [x] `POST /v1/inAppPurchaseImages` - [x] `POST /v1/inAppPurchaseLocalizations` - [x] `POST /v1/inAppPurchaseSubmissions` - [x] `POST /v1/promotedPurchases` - [x] `POST /v1/reviewSubmissionItems` - request and response - [x] `POST /v1/reviewSubmissions` - [x] `POST /v1/subscriptionAppStoreReviewScreenshots` - [x] `POST /v1/subscriptionGroupLocalizations` - [x] `POST /v1/subscriptionGroups` - [x] `POST /v1/subscriptionImages` - [x] `POST /v1/subscriptionIntroductoryOffers` - [x] `POST /v1/subscriptionLocalizations` - [x] `POST /v1/subscriptionOfferCodes` - [x] `POST /v1/subscriptionPrices` - [x] `POST /v1/subscriptionPromotionalOffers` - [x] `POST /v1/subscriptionSubmissions` - [x] `POST /v1/subscriptions` - [x] `POST /v2/inAppPurchases` Already reconciled through schema deprecation reversals or media model changes after the original 4.4 import (31): - [x] `GET /v1/appClipDefaultExperiences/{id}/releaseWithAppStoreVersion` - [x] `GET /v1/appCustomProductPageVersions/{id}` - [x] `GET /v1/appCustomProductPages/{id}` - [x] `GET /v1/appCustomProductPages/{id}/appCustomProductPageVersions` - [x] `GET /v1/appEvents/{id}` - [x] `GET /v1/appStoreVersionExperimentTreatments/{id}` - [x] `GET /v1/appStoreVersionExperiments/{id}/appStoreVersionExperimentTreatments` - [x] `GET /v1/appStoreVersions/{id}` - [x] `GET /v1/apps/{id}/appCustomProductPages` - [x] `GET /v1/apps/{id}/appEvents` - [x] `GET /v1/apps/{id}/appStoreVersions` - [x] `GET /v1/builds/{id}/appStoreVersion` - [x] `GET /v1/gameCenterAppVersions/{id}/appStoreVersion` - [x] `GET /v2/appStoreVersionExperiments/{id}/appStoreVersionExperimentTreatments` - [x] `PATCH /v1/appCustomProductPageLocalizations/{id}` - [x] `PATCH /v1/appCustomProductPageVersions/{id}` - [x] `PATCH /v1/appCustomProductPages/{id}` - [x] `PATCH /v1/appEventLocalizations/{id}` - [x] `PATCH /v1/appEvents/{id}` - [x] `PATCH /v1/appStoreVersionExperimentTreatments/{id}` - [x] `PATCH /v1/appStoreVersionLocalizations/{id}` - [x] `PATCH /v1/appStoreVersions/{id}` - [x] `POST /v1/appCustomProductPageLocalizations` - [x] `POST /v1/appCustomProductPageVersions` - [x] `POST /v1/appCustomProductPages` - [x] `POST /v1/appEventLocalizations` - [x] `POST /v1/appEvents` - [x] `POST /v1/appStoreVersionExperimentTreatmentLocalizations` - [x] `POST /v1/appStoreVersionExperimentTreatments` - [x] `POST /v1/appStoreVersionLocalizations` - [x] `POST /v1/appStoreVersions` ## Release-note capability ledger | # | Apple addition | Owner | Verification | Status | | ---: | --- | --- | --- | --- | | 1 | Discrete IAP versions and their localizations/review images | #1777 | 18-operation ledger, CLI/HTTP/upload tests | Implemented at `ee40c7b3` | | 2 | Discrete subscription versions and their localizations/promotional images | #1779 | 18-operation ledger, CLI/HTTP/upload tests | Implemented at `c8f3ab52` | | 3 | Discrete subscription-group versions and their localizations | #1780 | 10-operation ledger and CLI/HTTP tests | Implemented at `48d04e0b` | | 4 | Submit all three version types through review-submission items | #1777 and #1781 | Three exact relationship payload tests plus built-command tests | Implemented at `ee40c7b3` and `aba35e0a` | | 5 | Version-scoped v2 IAP localizations and images | #1777 and #1792 | CRUD/upload coverage from #1777; #1792 table-tests create-description and image-upload omission/value/null encoding | Feature implementation at `ee40c7b3`; complete nullable request fidelity landed from #1792 at `804624cd` | | 6 | Version-scoped v2 subscription localizations and images | #1779 and #1792 | CRUD/upload coverage from #1779; #1792 table-tests create-description and image-upload omission/value/null encoding | Feature implementation at `c8f3ab52`; complete nullable request fidelity landed from #1792 at `804624cd` | | 7 | Version-scoped v2 subscription-group localizations | #1780 and #1792 | CRUD coverage from #1780; #1792 table-tests `customAppName` omission/value/null encoding, including whitespace omission | Feature implementation at `48d04e0b`; complete nullable request fidelity landed from #1792 at `804624cd` | | 8 | Adjusted subscription equalizations and new filters | #1778 | Exact query/response, option-scope, strict-CSV/enum, territory-inclusion, opaque-next, ID-validation, and aggregation tests | Implemented at `39284b0a` | | 9 | `socialMedia` and `socialMediaAgeRestricted` age-rating attributes | #1778 and #1792 | Payload/output/help coverage from #1778; #1792 table-tests omission/value/null encoding for both update fields | Feature implementation at `39284b0a`; complete nullable request fidelity landed from #1792 at `804624cd` | ## Deprecation and migration ledger Apple deprecates seven resource families in the prose release notes: | Deprecated family | Replacement API and implemented command | Owner | Compatibility and warning status | Transition evidence | | --- | --- | --- | --- | --- | | IAP localizations v1 | `/v2/inAppPurchaseLocalizations`; `asc iap versions localizations ... --version-id` | #1777, #1786 | Four stable leaves preserved with one exact warning and DEPRECATED direct help | CRUD/ID tests, warning/payload/exit compatibility tests, migration docs | | IAP images v1 | `/v2/inAppPurchaseImages`; `asc iap versions images ... --version-id` | #1777, #1786 | Five stable leaves preserved with one exact warning and DEPRECATED direct help | Upload characterization, reserve/upload/commit tests, migration docs | | IAP submissions | `/v1/reviewSubmissionItems`; `asc review items add --item-type inAppPurchaseVersions` | #1777, #1781, #1786, #1787 | Stable submit leaf warns, remains directly callable, and is visible in parent help | Exact relationship payload, warning/exit tests, rendered-help regression | | Subscription localizations v1 | `/v2/subscriptionLocalizations`; `asc subscriptions versions localizations ... --version-id` | #1779, #1786 | Five stable leaves and one experimental `sync` leaf preserved with exact warnings | CRUD/ID, warning/payload/exit, and migration-doc tests | | Subscription images v1 | `/v2/subscriptionImages`; `asc subscriptions versions images ... --version-id` | #1779, #1786 | Five stable leaves preserved with exact warnings | Upload characterization, reserve/upload/commit, and migration-doc tests | | Subscription-group localizations v1 | `/v2/subscriptionGroupLocalizations`; `asc subscriptions groups versions localizations ... --version-id` | #1780, #1786 | Five stable leaves and one experimental `sync` leaf preserved with exact warnings | CRUD/ID, warning/payload/exit, and migration-doc tests | | Subscription and group submissions | `/v1/reviewSubmissionItems`; item types `subscriptionVersions` and `subscriptionGroupVersions` | #1781, #1786 | Two stable submit leaves preserved with exact warnings and DEPRECATED direct help | Relationship payload, warning/exit, and migration-doc tests | PR `#1786` begins the repository's required deprecation window for 29 public leaves: 27 stable and two experimental `sync` commands. `asc iap setup` and `asc subscriptions setup` remain stable but emit one combined warning when legacy localization flags are requested. All 33 exported legacy client methods carry precise Go `Deprecated:` replacement documentation. The wrapper preserves flags, endpoint selection, stdout, confirmation requirements, and exit behavior while writing one migration warning to stderr. No stable behavior was deleted. Removal requires a later release after the documented deprecation window; this goal intentionally stops before release. ## Pull-request sequence and status 1. Schema tooling was audited at #1776 head `aaa9b62d` and landed as `893c840e`. 2. IAP versions were audited at #1777 head `ee40c7b3` and landed as `bd453af4`. 3. Age rating and pricing were audited at #1778 head `39284b0a` and landed as `f6be93f6`. 4. Subscription versions were audited at #1779 head `c8f3ab52` and landed as `0349e397`. 5. Subscription-group versions were audited at #1780 head `48d04e0b` and landed as `8cbcdadc`. 6. Cross-cutting review integration was audited at #1781 head `aba35e0a` and landed as `eea98d3a`. 7. Age-rating prerequisite validation was audited at #1782 head `3dfd15a0` and landed as `b92e5605`. 8. Adjusted-equalization filter validation was audited at #1783 head `a06c935c` and landed as `6f11d6aa`. 9. Positional-argument validation was audited at #1784 head `fc5f0504` and landed as `1cc9face`. 10. Relationship-limit compatibility was audited at #1785 head `da030480` and landed as `b01d4d70`. 11. The 29-leaf deprecation transition was audited at #1786 head `1ef32153` and landed as `a00985b2`. 12. Deprecated IAP submit discoverability was audited at #1787 head `04b52a62` and landed as `38fa9b5f`. 13. The final transitive age-rating dependency was audited at #1788 head `08003c38` and landed as `9282e82d`; the historical #1782 thread was then resolved with exact-main evidence. 14. The final coverage ledger was audited at #1789 head `51b3a962` and landed as `d6d8d94b` without changing CLI behavior. 15. Subscription localization delete-confirmation coverage was audited at #1790 head `49eda126` and landed as `73466720`; it is test-only. 16. Hardened public 4.4.1 command workflows were audited at #1791 head `e9c2a0dc` and landed as `e902d375`; that docs-only commit became the base for #1792. 17. #1792 completed all seven nullable request properties at exact final head `3f7d14495fcb3b696692bee4955e36fd2f36c63f` and landed on `main` as `804624cd158d1eb8843d8e0be7cf55bc639da0a1`. 18. #1793 completed the IAP and promoted-purchase related sparse-field follow-up at exact final head `917d719df73a8dce9eefd5f378bad5a0562a67c0` and landed on `main` as `f6b34d9e042964673ee39c32fbae4f7aa99fc874`. 19. External workflow skills were first audited through rorkai/app-store-connect-cli-skills#51 at exact head `d7888b2b4a1a152f8524fc18c99d2d73d1c431fc`. The final cross-repository audit found one invalid combined Xcode help path; skills #52 fixed it at exact head `1aeb0dc607d8fa327501bc4b1d1cf981448512f9` and landed as skills `main` `f8f43c29d96a85792b99a8a1f23a7f048f8b312d`. CodeRabbit and Cursor Bugbot passed, the PR had zero review threads, all 23 skill validators passed, and all 695 runnable command occurrences validated against CLI `48e0d003`. 20. #1795 completed the subscription and pricing related sparse-field follow-up at exact final head `229dc07c3b999777e9b03dd543f66c3c6898e705` and landed on `main` as `5bf2d1545785a863242a8509bf55cc25d8a4ab49`. 21. #1796 completed the app-info, age-rating, and Xcode Cloud sparse-field follow-up at exact final head `8b4821a4529ae3a257dc56945a67dbef0ab7ac6b` and landed on `main` as `48e0d003ebde8d2046b0a19463526c95c3bc25e4`; all four review threads are resolved. 22. Exact CLI `main` `48e0d003` is the fully merged behavior integration through #1796. Its PR gates and post-merge Main Branch, Govulncheck, and CodeQL workflows passed. The historical built-help audit through #1788 found 48 added and 52 changed leaf paths relative to `839c4da6`, with zero removals; later flag-level help changes are verified by their focused command tests. Those historical counts do not replace #1792's typed nullable-encoding tests. CLI behavior and lifecycle PRs through #1788, the ledger PR, test-only #1790, docs-only #1791, nullable-fidelity #1792, IAP sparse-field #1793, subscription/pricing sparse-field #1795, app-info/age-rating/Xcode Cloud sparse-field #1796, and the companion skills PR are merged. No behavior PR remains open. No release, tag, or package publication is part of this goal. ### Built help-surface delta The recursive comparison executes every reachable leaf `--help` path in fresh binaries built at `839c4da6` and `9282e82d`. It found 48 additions: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` It found 52 changed leaf paths: ```text age-rating edit iap images create iap images delete iap images list iap images update iap images view iap list iap localizations create iap localizations delete iap localizations list iap localizations update iap setup iap submit iap view review items add review items list review items update review items view review items-add review items-get review items-list review items-update review submissions-get review submissions-list review submissions-update schema subscriptions groups list subscriptions groups localizations create subscriptions groups localizations delete subscriptions groups localizations list subscriptions groups localizations sync subscriptions groups localizations update subscriptions groups localizations view subscriptions groups view subscriptions images create subscriptions images delete subscriptions images list subscriptions images update subscriptions images view subscriptions list subscriptions localizations create subscriptions localizations delete subscriptions localizations list subscriptions localizations sync subscriptions localizations update subscriptions localizations view subscriptions pricing price-points equalizations subscriptions pricing price-points list subscriptions review submit subscriptions review submit-group subscriptions setup subscriptions view ``` There are zero removed leaf paths. `iap submit` remains callable and now appears in parent help with its DEPRECATED migration text; it is counted as changed, not removed. ## Mandatory verification for every behavior PR - Inspect current built `--help` before choosing the command shape. - Validate the exact operation's request attributes, relationships, filters, includes, sparse fields, limits, and response schemas. - Establish RED CLI tests and HTTP method/path/query/body tests. - Cover success, required-flag validation, invalid values, API errors, empty responses, pagination, and upload/artifact failures where applicable. - Assert destructive commands require `--confirm` before authentication or network side effects. - Verify JSON and representative table output by structure, not only strings. - Build a fresh `/tmp/asc` binary and verify stdout, stderr, help, and exit codes. - Use `ASC_BYPASS_KEYCHAIN=1` for every manual CLI test. - Run focused tests after each fix, adjacent packages before commit, and then: ```bash make format make check-docs make lint ASC_BYPASS_KEYCHAIN=1 make test ``` - Prefer read-only live verification. Use disposable app `6759231657` for mutations, record created IDs, clean them up, and report leftovers. - Re-query the latest PR head, thread-aware reviews, required checks, and mergeability before declaring a slice ready. ## Final omission audit The final closeout on exact CLI `main` `48e0d003` independently repeated these checks rather than trusting the per-PR reports. Historical evidence is labelled separately from the final sparse-query, nullable-fidelity, and live checks: - [x] Re-downloaded Apple's official OpenAPI zip and verified the artifact and extracted JSON hashes against the repository snapshot. - [x] Recomputed the 4.4-to-4.4.1 delta: 37 paths, 47 added operations, 47 added schemas, zero removals, 102 directly modified operations, and 61 modified schemas. - [x] Mapped all 47 added operations to exact HTTP method/path tests and discoverable typed command/client surfaces. - [x] Classified all 102 direct plus 71 transitive operation-contract changes: 173 unique existing-operation contracts with no missing or extra ledger item. - [x] Closed all 50 directly modified sparse-query transports: 14 were already handled before the hard follow-up; #1793 added 11 IAP/promoted-purchase transports, #1795 added 17 subscription/pricing transports, and #1796 added eight app-info/age-rating/Xcode Cloud transports. The final matrix has zero missing or extra operations. - [x] On #1792 final head `3f7d1449`, landed as `804624cd`, mapped all 47 added and 61 modified schemas to typed models, documented generic decoding, or an already-reconciled schema-only disposition. This includes table-driven omission/value/null encoding for the seven nullable properties missed by the earlier merged closeout. - [x] Mapped all nine release-note additions and seven deprecated families to commands, compatibility treatment, tests, and migration guidance. - [x] The historical built-help comparison through #1788 exercised all 100 paths changed in that slice: 48 additions and 52 changes, with zero removals. Later sparse-field flag changes were verified by their focused exact-help and generated-command-doc tests; the historical 100-path count is not presented as a final flag-level delta. - [x] Sequentially merged all 13 exact audited CLI heads through #1788, producing `main` `9282e82d`; its integration, Govulncheck, and CodeQL workflows passed. - [x] #1792 merged from exact final head `3f7d14495fcb3b696692bee4955e36fd2f36c63f` and landed on `main` as `804624cd158d1eb8843d8e0be7cf55bc639da0a1`. - [x] Deprecated all 29 public legacy leaves with one exact warning and direct migration help, added conditional setup warnings, and documented all 33 exported legacy client methods without deleting stable behavior. - [x] Exercised all 47 added operations against live App Store Connect on app `6759231657`, including reserve/upload/commit/delete image lifecycles and version/localization mutations. - [x] Recorded live schema differences: adjusted-equalization prerequisites, age-rating dependencies, and Apple's rejection of empty/null v2 localization descriptions. - [x] Verified validator v1 reads see v2-created resources with identical IDs; no runtime validator migration was needed. - [x] Cleaned every disposable resource the API permits and recorded the three unavoidable version/localization remnant pairs. All four review submissions are empty and age-rating fields were restored. - [x] Audited and merged companion workflow-skills PR #51, then closed the one remaining invalid combined Xcode help path in PR #52 at exact head `1aeb0dc607d8fa327501bc4b1d1cf981448512f9`, producing skills `main` `f8f43c29d96a85792b99a8a1f23a7f048f8b312d`. PR #52 had zero review threads; CodeRabbit and Cursor Bugbot passed; all 23 validators and 695 runnable command occurrences passed against CLI `48e0d003`. No runnable deprecated localization or submission teaching remains. - [x] Ran the final read-only live smoke with the exact-`main` `48e0d003` CLI on disposable app `6759231657`: the app read succeeded, all four age fields were `false`, every one of the four review submissions returned no items, all six retained resources remained readable, and all deleted parents and containers remained not found. This read-only smoke is not evidence that Apple accepts explicit null for #1792's seven corrected fields. - [x] Confirmed the fully merged behavior integration through #1796, `main` `48e0d003`, follows green exact-head and post-merge Main Branch, Govulncheck, and CodeQL workflows; #1789 previously landed the ledger at `d6d8d94b`, test-only #1790 landed at `73466720`, docs-only #1791 landed at `e902d375`, nullable-fidelity #1792 landed at `804624cd`, IAP sparse-field #1793 landed at `f6b34d9e`, and subscription/pricing sparse-field #1795 landed at `5bf2d154`. Skills `main` `f8f43c29d96a85792b99a8a1f23a7f048f8b312d` is the landed companion-skills integration; its final PR's CodeRabbit and Cursor Bugbot checks passed. The latest release/tag remains `3.0.0` at the pre-integration commit `839c4da6`. No release, tag, Homebrew/WinGet update, or package publication was performed. ---