template compliance answers never reach a store: init writes unanswered (dataSafety, appPrivacy, reviewNotes, ageRating, accounts, reviewAccess), older configs are recognised by the template values; check / validate / scan block, play data-safety (also --dry), console plan / fill, apple review-details and appinfo refuse. onboard --declarations replaces them on a live app: name and copyright from the stores (a copyright with a private term is left out), the rows the code proves (scan proposal, sign-up methods from auth code and login SDKs), the rest as one owner question list. play data-safety --dry prints the answers that change (old -> new, account and deletion first) against the last uploaded CSV; pitfall 60; tests

This commit is contained in:
CRTkafa 2026-10-14 11:36:30 +08:00
parent 34c2fa22e5
commit 7219ded3e8
27 changed files with 659 additions and 19 deletions

View file

@ -33,6 +33,7 @@
// error of Play Console (no policy page; developer.android.com/build/configure-app-module on choosing the id).
// The other Google Play rules (sensitive permissions, prominent disclosure, Families, advertising ID, 16 KB pages,
// testing for personal accounts, Play metadata, ads, payments, impersonation) are in play-rules.ts with their sources.
import { templateBlockers } from '../template-answers';
import { BETA_WORDS, competitorRe, OTHER_PLATFORMS, PLACEHOLDER_MARKERS, PLACEHOLDER_TEXT, PRICE_CLAIMS } from '../listing-rules';
import type { ShipConfig } from '../config';
import { allSubscriptions } from '../subscriptions';
@ -560,6 +561,10 @@ export function scanRules(inp: ScanInput): ScanFinding[] {
playRules(inp, { add: f => add({ ...f, store: 'play' }), cfg, sdkHits, listingHits, creates, restorable, paySdk });
// ---- owner-accepted findings ----
// compliance answers that are still the template's (storebound init): never the owner's, never pushed
if (c) for (const msg of templateBlockers(c)) for (const store of (['apple', 'play'] as const).filter(x => (x === 'apple' ? c.apple : c.play)))
add({ rule: 'template-answers', guideline: store === 'apple' ? '5.1.2 / 2.1' : 'Play: User Data', store, severity: 'blocker', title: `${msg.split(' still holds ')[0]} still holds template answers`,
evidence: cfgHits(inp, /unanswered\s*:|dataSafety\s*:|reviewNotes\s*:/), fix: msg, skill: 'store-declarations' });
applyIgnore(out, c?.scan?.ignore ?? []);
return sortScan(out);
}
@ -595,3 +600,11 @@ const RANK: Record<Severity, number> = { blocker: 0, high: 1, medium: 2, low: 3
export function sortScan(f: ScanFinding[]): ScanFinding[] {
return f.map((x, i) => [x, i] as const).sort(([a, i], [b, j]) => Number(!!a.accepted) - Number(!!b.accepted) || RANK[a.severity] - RANK[b.severity] || i - j).map(([x]) => x);
}
/** The first config line matching `re`, as evidence. */
function cfgHits(inp: ScanInput, re: RegExp): Hit[] {
const f = inp.configFile;
if (!f) return [];
const line = f.text.split('\n').findIndex(l => re.test(l));
return line < 0 ? [] : [{ file: f.path, line: line + 1, text: f.text.split('\n')[line]!.trim().slice(0, 100) }];
}