apple submit --build <n> (first submission, the resubmit steps; plan table with --json under serve); desktop owner actions to send for review: owner.apple_submit, owner.apple_resubmit, owner.apple_iap_submit, owner.play_production, and the price gate owner actions (owner.prices_play_products / _play_subscriptions / _apple_subscriptions; owner.prices_import passes --allow-price-changes after the dialog); OWNER_ALLOWED_ARGS ties each forbidden flag to its one action, dry runs never carry owner flags, MCP still has no submit or production release; submit dialog text in the host; system.tools androidSdk, project.inspect playRelease, declarations dataDeletion

This commit is contained in:
CRTkafa 2026-10-12 12:07:55 +08:00
parent 2e7dd44ae6
commit 33eb24a3dc
17 changed files with 335 additions and 49 deletions

View file

@ -6,7 +6,7 @@
// config does not prove is an "ask the owner" item, never a guess (AGENTS.md rule 5). Pure.
import { parseCsv } from './csv';
import { type AccountSignUp, type DataSafety, type PlayPurpose, profileOf, type ShipConfig } from './config';
import { CATEGORIES, dataSafetyCsv, dataSafetyIssues, dataSafetyRows } from './data-safety';
import { CATEGORIES, DATA_DELETION_RESPONSE, dataDeletionResponse, dataSafetyCsv, dataSafetyIssues, dataSafetyRows } from './data-safety';
import type { DeclarationsResult, DeclAsk } from './desktop-protocol';
import { NOTES_MAX_BYTES, reviewNotes } from './review-details';
@ -112,6 +112,16 @@ export interface DeclarationInputs {
uploaded: { csv: string; at: string } | null;
}
/** The general data deletion question (PSL_SUPPORT_DATA_DELETION_BY_USER) as the CSV answers it, where the answer
* comes from and the "Delete data URL" it sends. Pure. */
export function deletionAnswer(ds: DataSafety, deletionUrl?: string): { dataDeletion: 'yes' | 'no' | 'auto90' | null; dataDeletionSource: string | null; dataDeletionUrl: string | null } {
const r = dataDeletionResponse(ds, deletionUrl ? { deletionUrl } : undefined);
const dataDeletion = (Object.entries(DATA_DELETION_RESPONSE).find(([, v]) => v === r)?.[0] ?? null) as 'yes' | 'no' | 'auto90' | null;
const dataDeletionSource = ds.dataDeletion ? 'dataSafety.dataDeletion' : deletionUrl ? 'accounts.deletionUrl' : ds.deletionRequest !== undefined ? 'dataSafety.deletionRequest'
: ds.partialDeletion === true || ds.partialDeletion === 'auto90' ? 'dataSafety.partialDeletion' : null;
return { dataDeletion, dataDeletionSource, dataDeletionUrl: dataDeletion === 'yes' ? ds.partialDeletionUrl ?? deletionUrl ?? null : null };
}
export function declarations(config: ShipConfig, inp: DeclarationInputs): DeclarationsResult {
const ask: DeclAsk[] = [];
const play = !!config.play, apple = !!config.apple;
@ -128,6 +138,7 @@ export function declarations(config: ShipConfig, inp: DeclarationInputs): Declar
const types = Object.entries(ds.data).map(([id, u]) => ({ id, category: categoryOf(id), shared: !!u.shared, ephemeral: !!u.ephemeral, optional: !!u.optional, collect: [...u.collect], share: [...(u.shared ? u.share ?? [] : [])] }));
dataSafety = {
collectsData: ds.collectsData, encryptedInTransit: ds.encryptedInTransit, partialDeletion: ds.partialDeletion ?? null, accountCreation: ds.accountCreation ?? ['PSL_ACM_NONE'],
...deletionAnswer(ds, config.accounts?.deletionUrl),
types, extra: ds.extra?.length ?? 0, csv, rows: csv ? csv.trim().split('\n').length - 1 : 0,
uploaded: inp.uploaded && csv ? { at: inp.uploaded.at, changes: dataSafetyChanges(inp.uploaded.csv, csv) } : null,
};

View file

@ -100,6 +100,9 @@ export interface ProjectSummary {
variants?: string[];
/** play.reportsBucket is set (Play statistics for analytics snapshots). */
playReports?: boolean;
/** Play production settings from the config: play.managedPublishing (null: not set, Play's default off), the number
* of play.countries (null: all countries / regions) and the App category (play.category or the App Store match). */
playRelease?: { managedPublishing: boolean | null; countries: number | null; category: string | null } | null;
}
// ---- App-centric model (one app = one product on up to two platforms; a workspace holds many apps) ----
@ -271,6 +274,12 @@ export interface DeclarationsResult {
collectsData: boolean; encryptedInTransit: boolean; accountCreation: string[];
/** dataSafety.partialDeletion (PSL_SUPPORT_DATA_DELETION_BY_USER; asked once there are accounts); null: not answered. */
partialDeletion: boolean | 'auto90' | null;
/** PSL_SUPPORT_DATA_DELETION_BY_USER ("can users request that their data is deleted?") as the CSV answers it
* (null: not answered), where the answer comes from (dataSafety.dataDeletion, accounts.deletionUrl, ...) and the
* Delete data URL it sends. Optional: older sidecars do not send them. */
dataDeletion?: 'yes' | 'no' | 'auto90' | null;
dataDeletionSource?: string | null;
dataDeletionUrl?: string | null;
types: { id: string; category: string; shared: boolean; ephemeral: boolean; optional: boolean; collect: string[]; share: string[] }[];
/** Extra CSV rows of questions storebound does not model. */
extra: number;