play data-safety: PSL_SUPPORT_DATA_DELETION_BY_USER as Play's single choice (partialDeletion true | false | 'auto90'), drop the removed PSL_DATA_COLLECTION_USER_REQUEST_DELETE (deletionRequest deprecated, check notes it); ship Google's empty form (cli/data/play-data-safety-form.csv, exported 2026-10-12), take every Answer requirement from it and report pairs not in it (formPairIssues) in check, declarations and play data-safety. Pitfall 21

This commit is contained in:
CRTkafa 2026-10-12 09:33:56 +08:00
parent dfc28c15f8
commit b5b8f2da9d
35 changed files with 1824 additions and 859 deletions

View file

@ -57,7 +57,7 @@ export const DECLARATIONS: Record<string, DeclarationsResult> = {
'p-prism': {
profile: 'game', stores: { apple: true, play: true },
dataSafety: {
collectsData: true, encryptedInTransit: true, deletionRequest: false, accountCreation: ['PSL_ACM_NONE'], extra: 0, csv: PRISM_CSV, rows: 431,
collectsData: true, encryptedInTransit: true, accountCreation: ['PSL_ACM_NONE'], partialDeletion: null, extra: 0, csv: PRISM_CSV, rows: 431,
types: [
{ id: 'PSL_DEVICE_ID', category: 'identifiers', shared: true, ephemeral: false, optional: false, collect: ['advertising', 'analytics'], share: ['advertising'] },
{ id: 'PSL_APPROX_LOCATION', category: 'location', shared: true, ephemeral: false, optional: false, collect: ['advertising'], share: ['advertising'] },
@ -89,7 +89,7 @@ export const DECLARATIONS: Record<string, DeclarationsResult> = {
'p-tidy': {
profile: 'app', stores: { apple: true, play: true },
dataSafety: {
collectsData: true, encryptedInTransit: true, deletionRequest: true, accountCreation: ['PSL_ACM_USER_ID_PASSWORD'], extra: 1, csv: PRISM_CSV, rows: 432,
collectsData: true, encryptedInTransit: true, accountCreation: ['PSL_ACM_USER_ID_PASSWORD'], partialDeletion: false, extra: 1, csv: PRISM_CSV, rows: 432,
types: [
{ id: 'PSL_EMAIL', category: 'personal', shared: false, ephemeral: false, optional: false, collect: ['functionality', 'account'], share: [] },
{ id: 'PSL_USER_ACCOUNT', category: 'personal', shared: false, ephemeral: false, optional: false, collect: ['functionality', 'account'], share: [] },

View file

@ -1153,7 +1153,8 @@ export const en = {
'ai.dsNoneText': 'Add dataSafety to storebound.config.ts (store-declarations skill): what the app and every SDK collect, why, and whether it is shared.',
'ai.dsCollects': 'Collects or shares data',
'ai.dsEncrypted': 'Encrypted in transit',
'ai.dsDeletion': 'Users can ask for deletion',
'ai.dsDeletion': 'Data deletion without deleting the account',
'ai.dsAuto90': 'No, deleted automatically within 90 days',
'ai.dsAccounts': 'Account creation',
'ai.dsNever': 'Play cannot read this form back. Once you upload from here, this tab shows what changed since.',
'ai.dsSame': 'Same as the last upload ({when}).',

View file

@ -1155,7 +1155,8 @@ export const tr: Record<keyof typeof en, string> = {
'ai.dsNoneText': 'storebound.config.ts içine dataSafety ekleyin (store-declarations becerisi): uygulama ve her SDK neyi, neden topluyor ve paylaşıyor mu.',
'ai.dsCollects': 'Veri topluyor veya paylaşıyor',
'ai.dsEncrypted': 'Aktarımda şifreli',
'ai.dsDeletion': 'Kullanıcılar silme isteyebilir',
'ai.dsDeletion': 'Hesabı silmeden veri silme',
'ai.dsAuto90': 'Hayır, 90 gün içinde otomatik silinir',
'ai.dsAccounts': 'Hesap oluşturma',
'ai.dsNever': 'Play bu formu geri okuyamaz. Buradan bir kez yükledikten sonra bu sekme o zamandan beri neyin değiştiğini gösterir.',
'ai.dsSame': 'Son yüklemeyle aynı ({when}).',

View file

@ -94,7 +94,7 @@ export function declarationsMd(app: string, d: DeclarationsResult): string {
const yes = (b: boolean | null | undefined) => (b === null || b === undefined ? 'not answered' : b ? 'yes' : 'no');
const out = [`# ${app}: store declarations`, '', 'From storebound.config.ts and .storebound/ (contact and demo account masked).', ''];
if (d.ask.length) out.push('## Ask the owner', '', mdTable(['area', 'question', 'how to answer'], d.ask.map(a => [a.area, a.text, a.fix])));
if (d.dataSafety) out.push('## Google Play Data safety', '', `Collects data: ${yes(d.dataSafety.collectsData)} · encrypted in transit: ${yes(d.dataSafety.encryptedInTransit)} · deletion request: ${yes(d.dataSafety.deletionRequest)}`, '',
if (d.dataSafety) out.push('## Google Play Data safety', '', `Collects data: ${yes(d.dataSafety.collectsData)} · encrypted in transit: ${yes(d.dataSafety.encryptedInTransit)}${d.dataSafety.accountCreation.includes('PSL_ACM_NONE') ? '' : ` · data deletion without deleting the account: ${d.dataSafety.partialDeletion === 'auto90' ? 'no, deleted automatically within 90 days' : yes(d.dataSafety.partialDeletion)}`}`, '',
mdTable(['data type', 'category', 'collected for', 'shared for', 'optional', 'ephemeral'], d.dataSafety.types.map(x => [x.id, x.category, x.collect.join(', '), x.shared ? x.share.join(', ') : 'not shared', yes(x.optional), yes(x.ephemeral)])));
if (d.appPrivacy) out.push('## App Store App Privacy (enter in App Store Connect)', '', mdTable(['data type', 'category', 'purposes', 'third party', 'from'], d.appPrivacy.items.map(x => [x.apple, x.category, x.purposes.join(', '), yes(x.thirdParty), x.from.join(', ')])));
if (d.appInfo) out.push('## App Store app info', '', `Category: ${d.appInfo.categories ? [d.appInfo.categories.primary, d.appInfo.categories.primarySubOne, d.appInfo.categories.primarySubTwo].filter(Boolean).join(' / ') : 'not set'}`,

View file

@ -64,7 +64,7 @@ function DataSafety({ project, d, onDone }: { project: Project; d: DeclarationsR
<dl className="kv">
<dt>{t('ai.dsCollects')}</dt><dd>{yes(ds.collectsData)}</dd>
<dt>{t('ai.dsEncrypted')}</dt><dd>{yes(ds.encryptedInTransit)}</dd>
<dt>{t('ai.dsDeletion')}</dt><dd>{yes(ds.deletionRequest)}</dd>
{!ds.accountCreation.includes('PSL_ACM_NONE') && <><dt>{t('ai.dsDeletion')}</dt><dd>{ds.partialDeletion === 'auto90' ? t('ai.dsAuto90') : yes(ds.partialDeletion)}</dd></>}
<dt>{t('ai.dsAccounts')}</dt><dd className="small">{ds.accountCreation.map(x => typeLabel(x.replace('ACM_', ''))).join(', ')}</dd>
</dl>
{up === null ? <Notice tone="info" icon="info">{t('ai.dsNever')}</Notice>