169 lines
22 KiB
Text
169 lines
22 KiB
Text
$ storebound console plan play-content-rating
|
|
Play Console: Content rating (IARC)
|
|
|
|
Owner batch (everything the owner does, in ONE message, in the order the agent needs it):
|
|
1. [before the run] Answer 17 question(s) in ONE batch (askOwner), then write the answers into storebound.config.ts and rerun the plan: Category; Violence; Fear / horror; Sexuality or nudity; Language (profanity); Controlled substances (drugs, alcohol, tobacco); Crude humour; Gambling with real money; Simulated gambling; Users can interact or exchange content; Shares the user's location with other users; Digital purchases; Unrestricted internet access (browser); Miscellaneous: Nazi symbols or references?; Miscellaneous: content about Korean history that may be sensitive?; Miscellaneous: terrorism or extremist content?; Miscellaneous: depicts or encourages crimes?
|
|
2. [before the run] Tick the IARC Terms of Use box @ Play Console > the app > Policy and programs > App content > Content ratings > "Start questionnaire" (/app-content/content-rating-iarc-questionnaire), page 1: the IARC Terms of Use checkbox under the Email address and the Category. Tick it, leave the tab open; the agent fills the rest of page 1 and presses Next (Next does nothing until it is ticked).
|
|
3. [after the run] Press Send changes for review once every App content card and store setting is done @ Publishing overview (left menu): "Submit N changes for review", then "Send changes for review" in the "Send N changes for review?" dialog (a real click)
|
|
|
|
start: https://play.google.com/console
|
|
then: Pick the developer account if asked (set play.developerId for a deep link)
|
|
then: the app (com.example.prismpuddle) in the app list
|
|
then: Policy and programs > App content
|
|
then: Content rating > Start (or Manage)
|
|
|
|
Steps:
|
|
1. Open App content > Content ratings (/app-content/content-rating-overview), then "Start questionnaire": it opens /app-content/content-rating-iarc-questionnaire. [click: "Start questionnaire"]
|
|
2. Page 1: type the email address and pick the category (material radio: JS click on the inner [role=radio]). The IARC Terms of Use checkbox is an agreement: never tick it. The owner ticked it before the run (owner batch); read its aria-checked. Still unticked: Next does nothing (verified 2026-10-13), so do not press it again and again: say once that the box is waiting for the owner and poll aria-checked every 5 s (up to 15 min). Then Next. [click: "Next"]
|
|
3. The questionnaire is ONE long page of sections (Violence, Fear, Sexuality, Gambling, Language, Controlled Substance, Crude Humor, Digital Purchases, Miscellaneous). Answer each section's Yes / No as the sheet says; each Yes reveals follow-ups right below it, so answer top to bottom and re-read the page after every Yes. Run it as one in-page job (plan.inPageLoop) and poll window.__log.
|
|
4. Violence follow-ups (after Yes): the humans / non-humans checkboxes, then setting, childlike style, reactions, presentation checkboxes, blood level, historical war setting, innocent characters, fierce sounds.
|
|
5. Fear, Sexuality, Gambling, Language, Controlled Substance, Crude Humor follow-ups (after Yes): answer the questions shown as the sheet says (details.<section>.other).
|
|
6. Digital Purchases follow-ups (after "purchases of digital goods" Yes): the random items (loot box) question, then trading between players.
|
|
7. Miscellaneous: user interaction (after Yes: block, report, moderation, friends only), location sharing, Nazi symbols, Korea history, terrorism, crimes.
|
|
8. Press Save (stores the draft; nothing goes to IARC yet), then Next: the Summary shows the rating per authority (PEGI, ESRB, USK, ClassInd, Google Play ...). [click: "Save", "Next"]
|
|
9. Summary: read the ratings per authority, then press its Save yourself (a declaration save, "Save", not "Submit"; verified 2026-10-13: it stores the rating declaration, which Google gets only with the next "Send changes for review" in Publishing overview, the owner's). Tell the owner the ratings in the final report. [click: "Save"]
|
|
10. Verify: read the Summary page as text and pipe it to `storebound console verify play-content-rating --from-text -`.
|
|
|
|
Answer sheet:
|
|
[ok] Email address: s***@example.com -> type "Email address" @ Email address and category
|
|
[ask] Category: (you decide) -> pick "Category" @ Email address and category
|
|
Pick the category; set console.contentRating.category to have it filled.
|
|
[you] IARC Terms of Use: the owner ticks it (an agreement) -> untick "IARC Terms of Use" @ Email address and category
|
|
An agreement on page 1 (a checkbox): only the owner ticks it, never the agent or storebound.
|
|
[ask] Violence: (you decide) -> pick "Does the app contain (any )violen" @ Questionnaire (Does the app contain (any )violen)
|
|
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.violence.humans, violence.nonHumans, violence.setting, violence.childlike, violence.reactions, violence.presentation, violence.blood, violence.historicalWar, violence.innocents, violence.fierceSounds too.
|
|
[ask] Fear / horror: (you decide) -> pick "Does the app contain …scary" @ Questionnaire (Does the app contain …scary)
|
|
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.fear.other too.
|
|
[ask] Sexuality or nudity: (you decide) -> pick "Does the app contain …sexual" @ Questionnaire (Does the app contain …sexual)
|
|
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.sexuality.other too.
|
|
[ask] Language (profanity): (you decide) -> pick "Does the app contain …profan" @ Questionnaire (Does the app contain …profan)
|
|
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.language.other too.
|
|
[ask] Controlled substances (drugs, alcohol, tobacco): (you decide) -> pick "Does the app contain …drug" @ Questionnaire (Does the app contain …drug)
|
|
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.controlledSubstances.other too.
|
|
[ask] Crude humour: (you decide) -> pick "crude humour" @ Questionnaire (crude humour)
|
|
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.crudeHumor.other too.
|
|
[ask] Gambling with real money: (you decide) -> pick "Does the app allow …gambl…real money" @ Questionnaire (Does the app allow …gambl…real money)
|
|
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.gambling.other too.
|
|
[ask] Simulated gambling: (you decide) -> pick "simulated gambling" @ Questionnaire (simulated gambling)
|
|
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.gambling.other too.
|
|
[ok] Random paid items (loot boxes): No -> pick "No" @ Questionnaire (random (paid )items)
|
|
[ask] Users can interact or exchange content: (you decide) -> pick "users …interact" @ Questionnaire (users …interact)
|
|
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.userInteraction.block, userInteraction.report, userInteraction.moderation, userInteraction.friendsOnly too.
|
|
[ask] Shares the user's location with other users: (you decide) -> pick "share …(physical )location" @ Questionnaire (share …(physical )location)
|
|
Answer from the app's real content; set console.contentRating.answers to have it filled.
|
|
[ask] Digital purchases: (you decide) -> pick "digital goods" @ Questionnaire (digital goods)
|
|
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.digitalPurchases.playerTrading too.
|
|
[ask] Unrestricted internet access (browser): (you decide) -> pick "unrestricted internet" @ Questionnaire (unrestricted internet)
|
|
Answer from the app's real content; set console.contentRating.answers to have it filled.
|
|
[ask] Miscellaneous: Nazi symbols or references?: (you decide) -> pick "nazi" @ Questionnaire: Miscellaneous follow-ups (nazi)
|
|
Answer from the app's real content; set console.contentRating.details.miscellaneous.naziSymbols to have it filled.
|
|
[ask] Miscellaneous: content about Korean history that may be sensitive?: (you decide) -> pick "korea" @ Questionnaire: Miscellaneous follow-ups (korea)
|
|
Answer from the app's real content; set console.contentRating.details.miscellaneous.koreaHistory to have it filled.
|
|
[ask] Miscellaneous: terrorism or extremist content?: (you decide) -> pick "terror" @ Questionnaire: Miscellaneous follow-ups (terror)
|
|
Answer from the app's real content; set console.contentRating.details.miscellaneous.terrorism to have it filled.
|
|
[ask] Miscellaneous: depicts or encourages crimes?: (you decide) -> pick "crime" @ Questionnaire: Miscellaneous follow-ups (crime)
|
|
Answer from the app's real content; set console.contentRating.details.miscellaneous.crimes to have it filled.
|
|
|
|
Never press: "IARC Terms of Use box", "Send changes for review", "Publish", "Submit", "Create", "Create app", "Send", "Release", "Definitions and Examples", "?", "More information", "Submit N changes for review", "Restart review"
|
|
Final button (the owner's): Send changes for review
|
|
Rule: Press only navigation (Next, Back, Get Started, Edit, Set Up, Start / Manage links), draft saves (Save draft / Save (questionnaire page)) and the App content declaration saves (Save (Summary page): Google gets them only with the owner's Send changes for review). Never press Send changes for review or anything that publishes, submits, creates, sends, restarts a review or accepts; stop and tell the owner.
|
|
|
|
UI facts:
|
|
- Verified 2026-10-12 (an agent with a browser tool on a real game):
|
|
- App content > Content ratings is /app-content/content-rating-overview; "Start questionnaire" opens /app-content/content-rating-iarc-questionnaire.
|
|
- Page 1: email address, the category radio, and an IARC Terms of Use checkbox: an agreement, the owner ticks it, never the agent.
|
|
- Verified 2026-10-13: page 1's Next does nothing until the IARC Terms of Use box is ticked. So the box is in the ONE owner batch before the run (the owner ticks it on page 1 first), never a stop in the middle.
|
|
- The questionnaire is one long page of sections; each Yes reveals follow-ups (Violence: humans / non-humans checkboxes, then setting, childlike style, reactions, presentation checkboxes, blood level, historical war setting, innocent characters, fierce sounds; Digital Purchases: digital goods, then loot boxes, then player trading; Miscellaneous: user interaction then block / report / moderation / friends-only, location sharing, Nazi symbols, Korea history, terrorism, crimes). Follow-up wording here is storebound's: match by topic and the console's own labels.
|
|
- Save on the questionnaire stores it; Next goes to the Summary with per-authority ratings. The Summary's button is "Save" (not "Submit"). Saving an App content declaration is not public (verified 2026-10-13): Google gets it only with the next "Send changes for review" in Publishing overview (the owner's), so the agent presses this Save.
|
|
- Example result (the game of that run): PEGI 7, ESRB E10+, USK 12, ClassInd 18 (paid random items alone made it 18), Google Play 7+.
|
|
- The email address field gets the full app.contactEmail from the config (printed masked here).
|
|
- Play Console radios and checkboxes are <material-radio> / <material-checkbox>: computer-tool clicks and setting the native input (form_input) can leave the Angular model unchanged. Click the inner [role=radio] / [role=checkbox] (or the <input> itself) with a JS el.click(), then read aria-checked (or input.checked). Their labels sit in sibling nodes, not in an aria-label: walk up from the control to the nearest parent whose innerText is not empty and match by "starts with" (verified 2026-10-12).
|
|
- An app's Play Console home is /console/u/0/developers/<developerId>/app/<appId>/app-dashboard (verified 2026-10-12); with play.appId (or the id storebound remembered) the plan starts there.
|
|
|
|
Driving:
|
|
- Use your own browser tool (Claude in Chrome, a built-in browser, Playwright). Read the page as text or an accessibility tree first; click by visible text; adapt when the page differs from these steps (the console changes without notice), but never press anything in buttons.neverPress.
|
|
- Long flows run INSIDE the page: one Runtime.evaluate / javascript call times out at about 45 s on big Play pages (the IARC questionnaire, App content), so never loop over many controls in one call. Paste plan.inPageLoop once per page (one short call; it defines window.__sb), start the job with window.__sb.run([{ role: "radio", label: "No", within: "<question text>" }, { role: "checkbox", label: "<box label>", want: true }, ...]) (it returns at once), then poll window.__log.slice(-20) and window.__done in short calls every 2-5 s until the log says done. The helper only picks radios and ticks boxes (never agreements); press Next / Save yourself afterwards, by their visible text.
|
|
- Wait for each step to render (poll for the next heading or input, up to ~10 s) before acting; never act on a step you have not read.
|
|
- Text fields: click the field and TYPE the text (key presses), then read the value back. Never set it programmatically (form_input, el.value = …): App Store Connect's New App kept its Name and SKU empty after a value set (verified 2026-10-13).
|
|
- Owner steps: hand the owner plan.ownerBatch in ONE message before you start (the "before" items: answers, the IARC Terms of Use box) and run without stopping until the "after" items (the final button).
|
|
- An answer with status ask-owner: ask the owner in chat before filling it (AGENTS.md rule 5), or leave it empty and tell the owner. Never guess.
|
|
- Stop before the final button (buttons.final) and tell the owner exactly what to press; then run the verify command.
|
|
- Never sign in (never type the owner's own password), accept an agreement or tick a declaration: the owner does that in the browser. The one password you type is the App Review demo account from .storebound/credentials.json (reviewDemoAccount) on Play's Sign in details card, never printed.
|
|
|
|
In-page helper (plan.inPageLoop: paste it once per page in one short call, then window.__sb.run([...]) and poll window.__log):
|
|
(() => {
|
|
const norm = (t) => (t || "").replace(/\s+/g, " ").trim();
|
|
const log = (m) => { window.__log.push(new Date().toISOString().slice(11, 19) + " " + m); };
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
const labelOf = (el) => {
|
|
const own = norm(el.getAttribute("aria-label"));
|
|
if (own) return own;
|
|
for (let a = el; a && a !== document.body; a = a.parentElement) { const t = norm(a.innerText); if (t) return t; }
|
|
return "";
|
|
};
|
|
const NEVER = /terms of use|terms of service|i agree|agree to|declaration|developer program polic|export laws/i;
|
|
const CONTROLS = "[role=radio], [role=checkbox], input[type=radio], input[type=checkbox]";
|
|
const scopeOf = (text) => {
|
|
const want = norm(text).toLowerCase(); let best = null;
|
|
for (const el of document.querySelectorAll("section, fieldset, div, [role=radiogroup], [role=group]"))
|
|
if (norm(el.innerText).toLowerCase().includes(want) && el.querySelector(CONTROLS) && (!best || best.contains(el))) best = el;
|
|
return best;
|
|
};
|
|
const find = (role, label, scope) => {
|
|
const want = norm(label).toLowerCase();
|
|
const hits = [...(scope || document).querySelectorAll("[role=" + role + "], input[type=" + role + "]")]
|
|
.filter((el) => el.getClientRects().length && labelOf(el).toLowerCase().startsWith(want));
|
|
return hits.length === 1 ? hits[0] : null;
|
|
};
|
|
const set = async (s) => {
|
|
const scope = s.within ? scopeOf(s.within) : null;
|
|
if (s.within && !scope) return "no block with \"" + s.within + "\"";
|
|
const el = find(s.role, s.label, scope);
|
|
if (!el) return "not found or ambiguous";
|
|
if (NEVER.test(labelOf(el))) return "refused: an agreement (the owner ticks it)";
|
|
const host = el.closest("material-radio, material-checkbox");
|
|
const inner = el.matches("[role=radio], [role=checkbox]") ? el : (host && host.querySelector("[role=radio], [role=checkbox]")) || el;
|
|
const state = () => (inner.hasAttribute("aria-checked") ? inner.getAttribute("aria-checked") === "true" : !!inner.checked);
|
|
const want = s.want !== false;
|
|
if (state() !== want) inner.click();
|
|
await sleep(150);
|
|
return state() === want ? "ok" : "did not change (read the control, try the <input> itself)";
|
|
};
|
|
window.__log = []; window.__done = false;
|
|
window.__sb = { log, sleep, labelOf, scopeOf, find, set };
|
|
window.__sb.run = (steps) => {
|
|
window.__done = false;
|
|
window.__run = (async () => {
|
|
for (let i = 0; i < steps.length; i++) {
|
|
const s = steps[i], tag = (i + 1) + "/" + steps.length + " " + s.role + " \"" + s.label + "\"" + (s.within ? " in \"" + s.within + "\"" : "");
|
|
try { log(tag + ": " + await set(s)); } catch (e) { log(tag + ": error " + e.message); }
|
|
}
|
|
log("done"); window.__done = true;
|
|
})();
|
|
return "started " + steps.length + " steps: poll window.__log";
|
|
};
|
|
return "storebound helper ready: window.__sb.run([...]), then poll window.__log.slice(-20) and window.__done";
|
|
})()
|
|
|
|
Verify: storebound console verify play-content-rating --from-text -
|
|
- The Summary shows each question with the Yes / No of the answer sheet, and a rating per authority.
|
|
|
|
Ask the owner first:
|
|
- Category: ask the owner (Pick the category; set console.contentRating.category to have it filled.)
|
|
- IARC Terms of Use: an agreement, the owner ticks it
|
|
- Violence: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.violence.humans, violence.nonHumans, violence.setting, violence.childlike, violence.reactions, violence.presentation, violence.blood, violence.historicalWar, violence.innocents, violence.fierceSounds too.)
|
|
- Fear / horror: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.fear.other too.)
|
|
- Sexuality or nudity: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.sexuality.other too.)
|
|
- Language (profanity): ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.language.other too.)
|
|
- Controlled substances (drugs, alcohol, tobacco): ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.controlledSubstances.other too.)
|
|
- Crude humour: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.crudeHumor.other too.)
|
|
- Gambling with real money: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.gambling.other too.)
|
|
- Simulated gambling: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.gambling.other too.)
|
|
- Users can interact or exchange content: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.userInteraction.block, userInteraction.report, userInteraction.moderation, userInteraction.friendsOnly too.)
|
|
- Shares the user's location with other users: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled.)
|
|
- Digital purchases: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.digitalPurchases.playerTrading too.)
|
|
- Unrestricted internet access (browser): ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled.)
|
|
- Miscellaneous: Nazi symbols or references?: ask the owner (Answer from the app's real content; set console.contentRating.details.miscellaneous.naziSymbols to have it filled.)
|
|
- Miscellaneous: content about Korean history that may be sensitive?: ask the owner (Answer from the app's real content; set console.contentRating.details.miscellaneous.koreaHistory to have it filled.)
|
|
- Miscellaneous: terrorism or extremist content?: ask the owner (Answer from the app's real content; set console.contentRating.details.miscellaneous.terrorism to have it filled.)
|
|
- Miscellaneous: depicts or encourages crimes?: ask the owner (Answer from the app's real content; set console.contentRating.details.miscellaneous.crimes to have it filled.)
|