console plans: guardrails from a real run (2026-10-21): health none label and Sign in details Yes/No + Add details (old labels as altLabels, verify accepts both), finance boxes on step 1, App Privacy Additional Setup Required OK, verify from DOM input states (verify.capture, --from), Save + confirm before every Next on Play pages, IARC Digital purchases boxes and an exact-label in-page helper that refuses patterns and blanket answers; review contact found in parent folders, the profile and more key shapes with the files checked listed; one-line errors for missing required values (Args.required); pitfalls 86-87, tests
This commit is contained in:
parent
a6dab8f3aa
commit
335a4124ab
23 changed files with 486 additions and 74 deletions
|
|
@ -86,8 +86,25 @@ interface Part {
|
|||
guidance: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The plan's in-page snippet for Play declarations: every visible radio / checkbox / option with its label, its
|
||||
* checked or selected state (aria-checked, the checked property, aria-selected) and its group (the first text line of
|
||||
* the nearest radiogroup / fieldset / group), plus the page text. Prints JSON for `console verify <form> --from -`.
|
||||
*/
|
||||
export const INPUT_STATES_CAPTURE = `(() => { const n = s => (s || '').replace(/\\s+/g, ' ').trim();
|
||||
const label = e => n(e.getAttribute('aria-label')) || n(e.labels?.[0]?.innerText) || (() => { for (let a = e.parentElement; a && a !== document.body; a = a.parentElement) { const t = n(a.innerText); if (t) return t; } return ''; })();
|
||||
const role = e => e.getAttribute('role') || e.type;
|
||||
const on = e => e.hasAttribute('aria-checked') ? e.getAttribute('aria-checked') === 'true' : e.hasAttribute('aria-selected') ? e.getAttribute('aria-selected') === 'true' : !!e.checked;
|
||||
const group = e => { const g = e.closest('[role=radiogroup], fieldset, [role=group], material-radio-group'); return g ? n((g.innerText || '').split('\\n')[0]) : ''; };
|
||||
const states = [...document.querySelectorAll('[role=radio], [role=checkbox], [role=option], input[type=radio], input[type=checkbox]')].filter(e => e.getClientRects().length && !(e.matches('input') && e.closest('[role=radio], [role=checkbox]')))
|
||||
.map(e => ({ label: label(e), role: role(e), checked: on(e), group: group(e) }));
|
||||
return JSON.stringify({ url: location.href, states, text: (document.querySelector('main') ?? document.body).innerText }); })()`;
|
||||
|
||||
const PAGE_TEXT = 'document.querySelector("main")?.innerText ?? document.body.innerText';
|
||||
const pageVerify = (form: PlanFormId, expect: string[]): ConsolePlan['verify'] => ({ command: `storebound console verify ${form} --from-text -`, source: 'page-text', capture: PAGE_TEXT, expect });
|
||||
// declarations: the input-state snippet (radios / boxes are not readable from text); the plain page text still works
|
||||
const pageVerify = (form: PlanFormId, expect: string[]): ConsolePlan['verify'] => (['play-store-settings', 'play-privacy-policy', 'play-production-countries', 'play-production-release'].includes(form)
|
||||
? { command: `storebound console verify ${form} --from-text -`, source: 'page-text', capture: PAGE_TEXT, expect }
|
||||
: { command: `storebound console verify ${form} --from -`, source: 'page-text', capture: INPUT_STATES_CAPTURE, expect: [...expect, 'Run verify.capture in the page (one short call; it returns JSON with every radio / checkbox and its checked state) and pipe that JSON to the command.'] });
|
||||
|
||||
const ans = (id: string, question: string, value: string | boolean | null, status: PlanStatus, source: string, extra: Partial<PlanAnswer> = {}): PlanAnswer => ({
|
||||
id, question, value: status === 'ask-owner' || status === 'agreement' ? null : value,
|
||||
|
|
@ -198,7 +215,7 @@ function adsDeclaration(config: ShipConfig, p: PlanIds): Part {
|
|||
{ text: `Open App content > Ads: ${start(config, p, 'app-content/ads-declaration', []).url}` },
|
||||
{ text: 'Pick the radio (JS click on the inner [role=radio]); read aria-checked back.', answers: ['ads'] },
|
||||
{ text: DECLARATION_SAVE },
|
||||
{ text: 'Verify: read the page text and pipe it to `storebound console verify play-ads --from-text -`.' },
|
||||
{ text: 'Verify: run verify.capture in the page (the input states as JSON: radios and boxes are not in page text) and pipe it to `storebound console verify play-ads --from -`.' },
|
||||
]),
|
||||
answers: [a ? ans('ads', 'Does your app contain ads?', a.v, 'certain', a.src, { control: 'radio', action: 'pick', label: yesNo(a.v, yes, no), where: 'Ads' })
|
||||
: ans('ads', 'Does your app contain ads?', null, 'ask-owner', 'console.targetAudience.containsAds', { control: 'radio', where: 'Ads', note: 'Any ad SDK (banner, rewarded, house ads of other apps) is Yes; set console.targetAudience.containsAds.' })],
|
||||
|
|
@ -216,6 +233,11 @@ export const GUEST_ENTRY_NAME = 'Guest play (no account needed)';
|
|||
/** The Add instructions dialog's required box: an agreement, ticked by the owner only. */
|
||||
export const PLAY_FULL_ACCESS_BOX = 'Sign in details in this declaration provide full access to all the features and content within this app, including premium or paid content';
|
||||
|
||||
/** The Sign in details card's first question (2026-10-21: Yes / No radios; before that two long labelled radios, kept as altLabels). */
|
||||
export const PLAY_RESTRICTED_QUESTION = 'Is any part of your app restricted?';
|
||||
/** The dialog opener: "Add details" since 2026-10-21; "Add instructions" before (the fallback). */
|
||||
export const PLAY_ADD_DETAILS = 'Add details', PLAY_ADD_DETAILS_OLD = 'Add instructions';
|
||||
|
||||
function signInDetails(config: ShipConfig, p: PlanIds): Part {
|
||||
const ra = config.reviewAccess;
|
||||
const all = 'All functionality in my app is available without any access restrictions', some = 'All or some functionality in my app is restricted';
|
||||
|
|
@ -225,8 +247,8 @@ function signInDetails(config: ShipConfig, p: PlanIds): Part {
|
|||
// one entry that has no credentials and says how to get in as a guest
|
||||
const guest = !!ra && !ra.signInRequired && !!ra.instructions?.trim();
|
||||
const restricted = !!ra && (ra.signInRequired || guest);
|
||||
const q = 'Is any functionality restricted (sign-in, membership, location)?';
|
||||
const out: PlanAnswer[] = [ra ? ans('access', q, restricted, 'certain', guest ? 'reviewAccess.instructions (guest steps, no sign-in)' : 'reviewAccess.signInRequired', { control: 'radio', action: 'pick', label: restricted ? some : all, where: 'Sign in details (App access)',
|
||||
const q = PLAY_RESTRICTED_QUESTION;
|
||||
const out: PlanAnswer[] = [ra ? ans('access', q, restricted, 'certain', guest ? 'reviewAccess.instructions (guest steps, no sign-in)' : 'reviewAccess.signInRequired', { control: 'radio', action: 'pick', label: restricted ? 'Yes' : 'No', altLabels: [restricted ? some : all], where: 'Sign in details (App access)',
|
||||
...(guest ? { note: 'No sign-in, but guest steps: answered restricted so the reviewer reads them (Play rejected a guest-play game declared unrestricted as a login wall).' } : {}) })
|
||||
: ans('access', q, null, 'ask-owner', 'reviewAccess.signInRequired', { control: 'radio', where: 'Sign in details', note: 'Set reviewAccess { signInRequired } (review-access skill).' })];
|
||||
const long = (t: string) => t.length > PLAY_ACCESS_INFO_MAX ? { note: `${t.length} characters: Play takes at most ${PLAY_ACCESS_INFO_MAX}; shorten reviewAccess.instructions.` } : {};
|
||||
|
|
@ -251,25 +273,25 @@ function signInDetails(config: ShipConfig, p: PlanIds): Part {
|
|||
}
|
||||
// the dialog's required declaration box: an agreement, so the owner ticks it (agents never tick declarations)
|
||||
if (restricted) out.push(ans('fullAccess', 'Do these sign in details give full access to every feature and content, including premium or paid content?', null, 'agreement', 'Play Add instructions dialog (required)', { control: 'checkbox', label: PLAY_FULL_ACCESS_BOX, where: 'Add instructions dialog, required box' }));
|
||||
const add = 'Add instructions';
|
||||
const add = PLAY_ADD_DETAILS, addOld = PLAY_ADD_DETAILS_OLD;
|
||||
return {
|
||||
title: 'Play Console: Sign in details (App access)',
|
||||
start: start(config, p, 'app-content/testing-credentials', appContentReach('Sign in details')),
|
||||
steps: steps([
|
||||
{ text: `Open App content > Sign in details: ${start(config, p, 'app-content/testing-credentials', []).url}` },
|
||||
{ text: 'Pick whether any functionality is restricted (material radio: JS click on the inner [role=radio]).', answers: ['access'] },
|
||||
{ text: `"${PLAY_RESTRICTED_QUESTION}": pick ${restricted ? 'Yes' : 'No'} (material radio: JS click on the inner [role=radio]; older pages show the long labels "${some}" / "${all}" instead). The dialog button is "${add}" (older pages: "${addOld}").`, answers: ['access'] },
|
||||
guest && { text: `Guest steps (no sign-in): press "${add}", type the Name "${GUEST_ENTRY_NAME}", leave Username and Password empty, and type reviewAccess.instructions into "${info}" (at most ${PLAY_ACCESS_INFO_MAX} characters).`, click: [add], answers: ['entryName', 'credentials', 'instructions'] },
|
||||
ra?.signInRequired && { text: p.demoAccount
|
||||
? `Restricted: press "${add}", type the instructions, then type the Username and Password from .storebound/credentials.json (reviewDemoAccount; click each field and type: never echo them in chat or logs).`
|
||||
: `Restricted: press "${add}", type the instructions; the Username and Password are the owner's (no reviewDemoAccount in .storebound/credentials.json: in the owner batch).`, click: [add], answers: ['instructions', 'credentials'] },
|
||||
restricted && { text: `Leave the required box "${PLAY_FULL_ACCESS_BOX}" unticked and stop: the owner ticks it (a declaration). Then press "Add" (closes the dialog; the entry is listed).`, click: ['Add'], answers: ['fullAccess'] },
|
||||
{ text: DECLARATION_SAVE },
|
||||
{ text: 'Verify: read the page text and pipe it to `storebound console verify play-sign-in-details --from-text -`.' },
|
||||
{ text: 'Verify: run verify.capture in the page (the input states as JSON: radios and boxes are not in page text) and pipe it to `storebound console verify play-sign-in-details --from -`.' },
|
||||
]),
|
||||
answers: out,
|
||||
...declarationButtons('Sign in details', [{ button: add, class: 'navigation', where: 'Sign in details: opens the instructions dialog' },
|
||||
...declarationButtons('Sign in details', [{ button: add, class: 'navigation', where: 'Sign in details: opens the details dialog' }, { button: addOld, class: 'navigation', where: 'Sign in details: the same dialog opener on older pages' },
|
||||
...(restricted ? [{ button: 'Add', class: 'navigation', where: 'Add instructions dialog: lists the entry on the card (nothing is stored before Save)' }] : [])]),
|
||||
uiFacts: ['Verified 2026-10-12: the Sign in details card (App access) is /app-content/testing-credentials.', 'Credentials are never in the config, the plan or chat: the agent types them from .storebound/credentials.json (reviewDemoAccount) without printing them; without that entry the owner types them.',
|
||||
uiFacts: ['Verified 2026-10-12: the Sign in details card (App access) is /app-content/testing-credentials.', `Verified 2026-10-21: the first question reads "${PLAY_RESTRICTED_QUESTION}" with Yes / No radios, and the dialog opens with "${add}" (was "${addOld}"; try both).`, 'Credentials are never in the config, the plan or chat: the agent types them from .storebound/credentials.json (reviewDemoAccount) without printing them; without that entry the owner types them.',
|
||||
`The Add instructions dialog: Name, Username, Password, "${info}" (at most ${PLAY_ACCESS_INFO_MAX} characters) and the required box "${PLAY_FULL_ACCESS_BOX}" (the owner's).`,
|
||||
'A guest-play game declared "no restrictions" was rejected as a login wall (empty name field, Continue disabled, an "I have a code" link on the first screen): with guest steps the card says restricted with one entry without credentials.', PLAY_MATERIAL_FACT],
|
||||
verify: pageVerify('play-sign-in-details', [guest ? `Restricted is selected; one entry "${GUEST_ENTRY_NAME}" with no username or password is listed.` : 'The selected radio matches reviewAccess.signInRequired; with a sign-in, one set of instructions is listed.']),
|
||||
|
|
@ -325,7 +347,7 @@ function advertisingId(config: ShipConfig, p: PlanIds): Part {
|
|||
{ text: 'Pick Yes or No (material radio: JS click on the inner [role=radio]).', answers: ['uses'] },
|
||||
uses.v === true && { text: 'Tick exactly the purposes of the sheet (material checkboxes: JS click on the inner [role=checkbox]; read aria-checked back).', answers: out.filter(a => a.id.startsWith('purpose')).map(a => a.id) },
|
||||
{ text: DECLARATION_SAVE },
|
||||
{ text: 'Verify: read the page text and pipe it to `storebound console verify play-advertising-id --from-text -`.' },
|
||||
{ text: 'Verify: run verify.capture in the page (the input states as JSON: radios and boxes are not in page text) and pipe it to `storebound console verify play-advertising-id --from -`.' },
|
||||
]),
|
||||
answers: out,
|
||||
...declarationButtons('Advertising ID'),
|
||||
|
|
@ -344,7 +366,7 @@ function government(config: ShipConfig, p: PlanIds): Part {
|
|||
{ text: `Open App content > Government apps: ${start(config, p, 'app-content/government-apps', []).url}` },
|
||||
{ text: 'Pick whether the app is developed by or on behalf of a government (material radio).', answers: ['government'] },
|
||||
{ text: DECLARATION_SAVE },
|
||||
{ text: 'Verify: read the page text and pipe it to `storebound console verify play-government-apps --from-text -`.' },
|
||||
{ text: 'Verify: run verify.capture in the page (the input states as JSON: radios and boxes are not in page text) and pipe it to `storebound console verify play-government-apps --from -`.' },
|
||||
]),
|
||||
answers: [v !== undefined ? ans('government', 'Is the app developed by or on behalf of a government?', v, 'certain', 'console.appContent.government', { control: 'radio', action: 'pick', label: v ? 'Yes' : 'No', where: 'Government apps' })
|
||||
: ans('government', 'Is the app developed by or on behalf of a government?', null, 'ask-owner', 'console.appContent.government', { control: 'radio', where: 'Government apps', note: 'Set console.appContent.government (true only for a government body or its contractor).' })],
|
||||
|
|
@ -356,31 +378,43 @@ function government(config: ShipConfig, p: PlanIds): Part {
|
|||
}
|
||||
|
||||
/** Financial features / Health apps: two steps, the features on the last one (or the none box). */
|
||||
/** The health card's none option (offered since 2026-10-21). */
|
||||
export const PLAY_NO_HEALTH = 'My app does not have any health features';
|
||||
const TICK_HOW = 'material checkboxes: JS click on the inner [role=checkbox]; read aria-checked back';
|
||||
/** Multi-step Play forms with a Save next to Next: Save each page before Next (an unsaved page is lost on Next). */
|
||||
export const SAVE_EACH_PAGE = 'Press this page\'s "Save" right after answering it and wait for "Your changes have been saved" BEFORE pressing Next (Next does not keep an unsaved page).';
|
||||
|
||||
function featureCard(config: ShipConfig, p: PlanIds, kind: 'finance' | 'health'): Part {
|
||||
const fin = kind === 'finance';
|
||||
const form: PlanOnlyFormId = fin ? 'play-financial-features' : 'play-health-apps';
|
||||
const name = fin ? 'Financial features' : 'Health apps';
|
||||
const key = fin ? 'financialFeatures' : 'healthFeatures';
|
||||
const v = config.console?.appContent?.[key];
|
||||
const none = fin ? 'My app doesn\'t provide any financial features' : 'Other';
|
||||
const none = fin ? 'My app doesn\'t provide any financial features' : PLAY_NO_HEALTH;
|
||||
const out: PlanAnswer[] = v === false
|
||||
? [ans('none', fin ? 'No financial features' : 'No health features', true, 'certain', `console.appContent.${key}`, { control: 'checkbox', action: 'tick', label: none, curlyQuotes: false, where: `${name}, last step`,
|
||||
...(fin ? {} : { note: 'On the verified run (2026-10-12) the no-health-features answer was the last step\'s "Other" box; when the page offers an explicit none option, tick that instead.' }) })]
|
||||
: Array.isArray(v) ? v.map(l => ans(`feature:${l}`, `${name}: ${l}`, true, 'certain', `console.appContent.${key}`, { control: 'checkbox', action: 'tick', label: l, where: `${name}, last step` }))
|
||||
: [ans('features', `${name}: which features does the app provide?`, null, 'ask-owner', `console.appContent.${key}`, { control: 'checkbox', where: `${name}, last step`, note: `Set console.appContent.${key}: false for none, or the console's labels to tick.` })];
|
||||
? [ans('none', fin ? 'No financial features' : 'No health features', true, 'certain', `console.appContent.${key}`, { control: 'checkbox', action: 'tick', label: none, curlyQuotes: false, where: fin ? `${name}, step 1` : `${name}, last step`,
|
||||
...(fin ? {} : { altLabels: ['Other'], note: `Verified 2026-10-21: the page offers "${PLAY_NO_HEALTH}"; tick it. Only an older page without it took "Other" (2026-10-12).` }) })]
|
||||
: Array.isArray(v) ? v.map(l => ans(`feature:${l}`, `${name}: ${l}`, true, 'certain', `console.appContent.${key}`, { control: 'checkbox', action: 'tick', label: l, where: fin ? `${name}, step 1` : `${name}, last step` }))
|
||||
: [ans('features', `${name}: which features does the app provide?`, null, 'ask-owner', `console.appContent.${key}`, { control: 'checkbox', where: fin ? `${name}, step 1` : `${name}, last step`, note: `Set console.appContent.${key}: false for none, or the console's labels to tick.` })];
|
||||
return {
|
||||
title: `Play Console: ${name} (App content)`,
|
||||
start: start(config, p, `app-content/${kind}`, appContentReach(name)),
|
||||
steps: steps([
|
||||
{ text: `Open App content > ${name}: ${start(config, p, `app-content/${kind}`, []).url}` },
|
||||
{ text: 'Step 1: read it and press Next.', click: ['Next'] },
|
||||
{ text: `Last step: tick exactly the boxes of the sheet (material checkboxes: JS click on the inner [role=checkbox]; read aria-checked back)${fin ? '; no financial features: "My app doesn\'t provide any financial features"' : '; no health features: "Other" (as on the verified run) or the none option the page offers'}. Then Next if the page shows one.`, click: ['Next'], answers: out.map(a => a.id) },
|
||||
...(fin ? [
|
||||
{ text: `Step 1: tick exactly the boxes of the sheet (${TICK_HOW}); no financial features: "My app doesn't provide any financial features". ${SAVE_EACH_PAGE} Then Next.`, click: ['Save', 'Next'], answers: out.map(a => a.id) },
|
||||
{ text: 'Step 2: an information page only (no inputs): read it.' },
|
||||
] : [
|
||||
{ text: 'Step 1: read it and press Next.', click: ['Next'] },
|
||||
{ text: `Last step: tick exactly the boxes of the sheet (${TICK_HOW}); no health features: "${PLAY_NO_HEALTH}" (an older page without it: "Other").`, answers: out.map(a => a.id) },
|
||||
]),
|
||||
{ text: DECLARATION_SAVE },
|
||||
{ text: `Verify: read the page text and pipe it to \`storebound console verify ${form} --from-text -\`.` },
|
||||
{ text: `Verify: run verify.capture in the page (the input states as JSON: radios and boxes are not in page text) and pipe it to \`storebound console verify ${form} --from -\`.` },
|
||||
]),
|
||||
answers: out,
|
||||
...declarationButtons(name, [{ button: 'Next', class: 'navigation', where: `${name}: between the two steps` }]),
|
||||
uiFacts: [`Verified 2026-10-12: /app-content/${kind}, two steps; the last step holds ${fin ? '"My app doesn\'t provide any financial features"' : 'the health feature boxes, "Other" among them'}; then Next and Save.`, PLAY_MATERIAL_FACT],
|
||||
uiFacts: [fin ? 'Verified 2026-10-21: /app-content/finance, two steps; the checkboxes ("My app doesn\'t provide any financial features" among them) are on step 1, step 2 is an information page only; then Save.'
|
||||
: `Verified 2026-10-21: /app-content/health, two steps; the last step holds the health feature boxes and "${PLAY_NO_HEALTH}" (before: only "Other" fit); then Save.`, PLAY_MATERIAL_FACT],
|
||||
verify: pageVerify(form, ['The ticked boxes match the sheet.']),
|
||||
guidance: [`Open: Policy and programs > App content > ${name}: ${start(config, p, `app-content/${kind}`, []).url}`],
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue