App-Store-Connect-CLI

Fast, scriptable CLI for the App Store Connect API. Automate TestFlight, builds, submissions, signing, analytics, screenshots, subscriptions, and more. JSON-first, no interactive prompts

RAW Doc

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=<ad-account-id>; 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=<id>;; the header account must match the path ID. |
| PUT /v1/ad-accounts/{id} | Requires X-AP-Context: adAccountId=<id>;; 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 <subcommand> [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=<key-id>
- JWT claims:
- iss=<team-id>
- iat=<issued-at>
- exp=<expires-at>
- aud=https://appleid.apple.com
- sub=<client-id>
- 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:<name>
asc:ads-metadata:<name>

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 <access_token>
Accept: application/json
Content-Type: application/json
X-AP-Context: orgId=<org-id>

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=<ad-account-id>;

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=<client-id>.
- 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
- 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 at 1,482 lines
- 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 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
- internal/cli/shared/shared.go
- internal/cli/shared/compat_aliases.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_queries.go
- internal/asc/client.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
- 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_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/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
- 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 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 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 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 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=<bundle-id>. 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/<safe-bundle-id>/<version>-<build>-<first-12-ipa-sha256>.
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:

text
/ 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:

text
/ 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
- 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:

text
/ 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.

---