desktop: a Fix button on scan and validate findings storebound can fix itself. Findings carry action { method, args } (cli/lib/fix-actions.ts); privacy-labels runs scan.write (scan --write-only, the dialog shows the config diff), Play listing gaps play.listing, a missing App Store localization or copyright apple.listing (editable version only), the App Review contact apple.review_details; each a dry run, the native dialog, then the confirmed run. --write-only is forbidden for MCP tools; tests pin the mapping, the round trip on a fixture and that no fix reaches a submit, release or publish

This commit is contained in:
CRTkafa 2026-10-15 06:14:49 +08:00
parent bd83d036ba
commit 992f5bcc0e
18 changed files with 257 additions and 37 deletions

View file

@ -339,6 +339,7 @@ const CATALOG: Record<string, CatalogDiff> = {
const VALIDATE: Record<string, ValidateResult> = {
'p-prism': {
blockers: 1, findings: [
{ store: 'play', level: 'blocker', code: 'listing', msg: 'ja-JP: missing shortDescription', fix: 'storebound play listing', action: { method: 'play.listing', args: {} } },
{ store: 'play', level: 'blocker', code: 'product-missing', msg: 'starter_pack is in the config but not on Google Play', fix: 'storebound play products --only starter_pack (dry run first)' },
{ store: 'apple', level: 'warn', code: 'screenshots-ipad', msg: 'ar-SA has no 13" iPad screenshots', fix: 'storebound shots render --lang ar, then storebound apple screenshots --only ar-SA' },
{ store: 'apple', level: 'warn', code: 'whats-new', msg: '2.4.0 has no "What\'s New" for ja', fix: 'storebound whats-new --since v2.3.0' },
@ -363,6 +364,9 @@ const SCAN_PRISM: ScanResult['findings'] = [
evidence: [{ file: 'storebound.config.ts', line: 41, text: 'no_ads (non_consumable)' }], fix: 'Add a visible "Restore Purchases" button on the store screen or in Settings.', skill: 'store-iap-setup', ref: 'https://developer.apple.com/app-store/review/guidelines/' },
{ rule: 'demo-account', guideline: '2.1(a)', store: 'apple', severity: 'blocker', title: 'Sign-in is required but there is no demo account for App Review',
evidence: [{ file: 'storebound.config.ts', line: 17, text: 'reviewAccess: { signInRequired: true }' }], fix: 'Add the App Review demo account in .storebound/ (review-access skill).', skill: 'review-access', ref: 'https://developer.apple.com/app-store/review/guidelines/', accepted: 'leaderboards only; the game plays without signing in' },
// a deterministic fix (scan.write: the detected answers into the config)
{ rule: 'privacy-labels', guideline: 'Play: User Data (Data safety)', store: 'play', severity: 'medium', title: 'Data safety likely misses PSL_DEVICE_ID: an SDK collects it, dataSafety does not declare it',
evidence: [{ file: 'Packages/manifest.json', line: 4, text: 'com.applovin.mediation.ads 8.1.0' }], fix: 'Confirm with the SDK\'s Data safety guidance, add it with `storebound scan --write` (or an edit of the config), then `storebound play data-safety`.', skill: 'store-declarations', ref: 'https://support.google.com/googleplay/android-developer/answer/10787469', action: { method: 'scan.write', args: {} } },
];
const SCAN: Record<string, ScanResult> = {
'p-prism': { stacks: ['unity'], sdks: [{ id: 'applovin', name: 'AppLovin MAX', category: 'ads', tracking: true }, { id: 'unity-ads', name: 'Unity Ads', category: 'ads', tracking: true }, { id: 'gameanalytics', name: 'GameAnalytics', category: 'analytics', tracking: false }],
@ -593,6 +597,9 @@ async function answer(cmd: string, a: Record<string, unknown>): Promise<unknown>
if (method === 'reviews.approve' && d) d.approved = !args.undo;
if (method === 'reviews.reply' && d) { d.reply = String(args.text); d.approved = false; d.issues = []; }
if (method === 'owner.reply_send' && d) d.sent = iso(0);
// a Fix button's write: the finding it fixed is gone on the next read
if (method === 'scan.write') { const i = SCAN_PRISM.findIndex(f => f.rule === 'privacy-labels'); if (i >= 0) SCAN_PRISM.splice(i, 1); SCAN['p-prism']!.counts = scanCounts(SCAN_PRISM); }
if (method === 'play.listing') { const v = VALIDATE[a.projectId as string]; if (v) v.findings = v.findings.filter(f => f.code !== 'listing'); }
const res: ToolResult = { method, ok: true, exitCode: 0, dryRun: false, command: `storebound ${method.replace('.', ' ')}`, output: `[${method}] done (fixture)` };
return { confirmed: true, stage: 'done', dryRun: { ...res, dryRun: true }, result: res };
}