console: agent-first mode (console plan / console verify), verified App Privacy and Play Create app facts
- `storebound console plan <form> [--json] [--lookup]`: read-only plan for an AI agent that drives the browser itself: start URL (deep links), ordered steps, answer sheet with the console's exact labels (certain / ask owner, curly quotes noted), never-press list, verified UI facts, driving guidance (in-page async loop + window.__log), verify instructions. All six forms. - `storebound console verify <form> [--from-text <file|->]`: App Privacy per-type summaries from page text, Play Create app from input states, content rating / target audience summaries, App Store record and age rating from the API; exit 1 on a mismatch. - MCP: console_plan, console_verify (read-only); console fill stays off MCP. - App Privacy (verified 2026-10-12): two information steps, unlabeled purpose checkboxes (nearest parent text), "Definitions and Examples" never pressed; fixture and engine follow. - Play Create app (verified 2026-10-12): /developers/<id>/create-new-app, App package name + read-only Check availability, material-radio inner [role=radio] click, app dashboard URL and play.appId, service account "Add app" as an owner step. - console-autofill skill rewritten agent-first; agents-core rule 10; README; pitfalls 12, 18 (confirmed sign-out), 19; CHANGELOG; play-first-release.
This commit is contained in:
parent
a9261a8052
commit
c9a7f095c4
34 changed files with 1430 additions and 113 deletions
|
|
@ -97,6 +97,7 @@ export function formDef(id: FormId, answers: Answer[]): FormDef {
|
|||
case 'apple-age-rating': return { id, sections: [] };
|
||||
case 'play-app-record': return { id, sections: [{ title: 'Create app', fields: [
|
||||
{ id: 'name', kind: 'text', names: ['App name', 'Uygulama adı'] },
|
||||
{ id: 'packageName', kind: 'text', names: ['App package name', 'Package name', 'Uygulama paket adı', 'Paket adı'] },
|
||||
{ id: 'language', kind: 'select', names: ['Default language', 'Varsayılan dil'] },
|
||||
{ id: 'type', kind: 'radio', names: ['App or game', 'Uygulama veya oyun'], options: { App: ['App', 'Uygulama'], Game: ['Game', 'Oyun'] } },
|
||||
{ id: 'price', kind: 'radio', names: ['Free or paid', 'Ücretsiz veya ücretli'], options: { Free: ['Free', 'Ücretsiz'], Paid: ['Paid', 'Ücretli'] } },
|
||||
|
|
@ -141,13 +142,19 @@ export function formDef(id: FormId, answers: Answer[]): FormDef {
|
|||
* Turkish console wording). English words match whole words; Turkish stems match word starts (agglutinative suffixes).
|
||||
*/
|
||||
const EN = ['save', 'saved', 'submit', 'publish', 'create', 'send', 'apply', 'confirm', 'done', 'release', 'rollout', 'roll out', 'start', 'delete',
|
||||
'remove', 'next', 'continue', 'finish', 'agree', 'accept', 'upload', 'deploy', 'discard', 'update', 'add for review', 'review', 'sign', 'log in', 'login'];
|
||||
'remove', 'next', 'continue', 'finish', 'agree', 'accept', 'upload', 'deploy', 'discard', 'update', 'add for review', 'review', 'sign', 'log in', 'login',
|
||||
// App Privacy's tracking step: a help panel, never needed (verified on the real dialog, October 2026)
|
||||
'definitions and examples'];
|
||||
const TR = ['kaydet', 'gönder', 'yayınla', 'yayımla', 'oluştur', 'onayla', 'tamam', 'bitti', 'bitir', 'sil', 'kaldır', 'başlat', 'sürüm',
|
||||
'ileri', 'devam', 'sonraki', 'kabul', 'yükle', 'güncelle', 'incelemeye', 'giriş'];
|
||||
const L = '\\p{L}\\p{N}';
|
||||
// "uygula" (apply) is refused in every form, but not "uygulama" (app: Yeni Uygulama, Uygulama içeriği)
|
||||
const FORBIDDEN = new RegExp(`(?<![${L}])(?:(?:${EN.map(esc).join('|')})(?![${L}])|(?:${TR.map(esc).join("|")}|uygula(?!ma)))`, 'iu');
|
||||
|
||||
/** Controls never pressed by storebound or by an agent following `console plan`, whatever the form: help panels and
|
||||
* the final buttons (the agent-facing never-press list is built from this and FORM_BUTTONS). */
|
||||
export const NEVER_PRESS = ['Publish', 'Submit', 'Create', 'Create app', 'Send', 'Release', 'Definitions and Examples', '?', 'More information'];
|
||||
|
||||
/** true when storebound must not click a control with this accessible name. */
|
||||
export const forbiddenControl = (name: string): boolean => FORBIDDEN.test(name.toLocaleLowerCase('tr').normalize('NFC')) || FORBIDDEN.test(name.toLowerCase());
|
||||
|
||||
|
|
@ -284,10 +291,12 @@ export function navPlan(id: FormId, config: ShipConfig, bases: NavBases = NAV_BA
|
|||
{ kind: 'wait', label: 'the New App dialog', roles: ['dialog'], names: ['New App', 'Yeni Uygulama', /^new app\b/i, /^yeni uygulama\b/iu] },
|
||||
];
|
||||
case 'apple-age-rating': return [];
|
||||
case 'play-app-record': return [
|
||||
config.play?.developerId
|
||||
? { kind: 'goto', label: 'open the app list of the developer account', url: playAppList(play, config.play.developerId) }
|
||||
: { kind: 'goto', label: 'open Play Console', url: play },
|
||||
case 'play-app-record': return config.play?.developerId ? [
|
||||
// verified 2026-10-12: the Create app form of one developer account has its own URL
|
||||
{ kind: 'goto', label: 'open the Create app form of the developer account', url: playCreateApp(play, config.play.developerId) },
|
||||
{ kind: 'wait', label: 'the Create app form', roles: ['textbox'], names: CREATE_FORM_FIELD },
|
||||
] : [
|
||||
{ kind: 'goto', label: 'open Play Console', url: play },
|
||||
{ kind: 'click', label: 'Create app on the app list', roles: ['button', 'link'], names: CREATE_APP_OPENERS, createApp: true },
|
||||
{ kind: 'wait', label: 'the Create app form', roles: ['textbox'], names: CREATE_FORM_FIELD },
|
||||
];
|
||||
|
|
@ -359,12 +368,14 @@ const EDIT: Name[] = ['Edit', 'Düzenle'];
|
|||
const SAVE: Name[] = ['Save', 'Kaydet'];
|
||||
const SAVE_DRAFT: Name[] = ['Save draft', 'Taslağı kaydet', 'Taslak olarak kaydet'];
|
||||
const DONE: Name[] = ['Done', 'Bitti', 'Tamam'];
|
||||
/** Play Create app: the read-only package name check next to App package name (verified 2026-10-12). */
|
||||
const CHECK_AVAILABILITY: Name[] = ['Check availability', 'Kullanılabilirliği kontrol et', 'Uygunluğu kontrol et'];
|
||||
/** The heading of App Privacy's Data Types section (Edit and the data types sit in it). */
|
||||
export const DATA_TYPES_HEADING: Name[] = ['Data Types', 'Veri Türleri', 'Veri türleri'];
|
||||
/** The App Privacy page heading. */
|
||||
export const APP_PRIVACY_HEADING: Name[] = ['App Privacy', 'Uygulama Gizliliği', /^app privacy$/i];
|
||||
/** Names the engine looks for. */
|
||||
export const FLOW_NAMES = { next: NEXT, save: SAVE, saveDraft: SAVE_DRAFT, getStarted: GET_STARTED, edit: EDIT };
|
||||
export const FLOW_NAMES = { next: NEXT, save: SAVE, saveDraft: SAVE_DRAFT, getStarted: GET_STARTED, edit: EDIT, checkAvailability: CHECK_AVAILABILITY };
|
||||
|
||||
/** Every button of every console form, classified (the console-autofill skill's table is written from this). */
|
||||
export const FORM_BUTTONS: Record<FormId, ButtonRule[]> = {
|
||||
|
|
@ -376,6 +387,7 @@ export const FORM_BUTTONS: Record<FormId, ButtonRule[]> = {
|
|||
{ button: '? / More information', class: 'navigation', where: 'next to each heading and data type: help only (never needed)', auto: false },
|
||||
{ button: 'Next / Continue', class: 'navigation', where: 'questionnaire dialogs', auto: true },
|
||||
{ button: 'Back', class: 'navigation', where: 'questionnaire dialogs (never needed)', auto: false },
|
||||
{ button: 'Definitions and Examples', class: 'navigation', where: 'a data type\'s tracking step: help only (never clicked)', auto: false },
|
||||
{ button: 'Save', class: 'draft-save', where: 'questionnaire dialogs: stores the answers, not public until Publish', auto: true },
|
||||
{ button: 'Publish (and its confirmation dialog)', class: 'final-submit', where: 'top right of the App Privacy page', auto: false },
|
||||
],
|
||||
|
|
@ -391,6 +403,7 @@ export const FORM_BUTTONS: Record<FormId, ButtonRule[]> = {
|
|||
],
|
||||
'play-app-record': [
|
||||
{ button: 'Create app (app list)', class: 'navigation', where: 'Home / All apps: opens the Create app form', auto: true },
|
||||
{ button: 'Check availability', class: 'navigation', where: 'the Create app form, next to App package name: read-only check ("Package name available")', auto: true },
|
||||
{ button: 'Developer Program Policies / US export laws boxes', class: 'final-submit', where: 'the Create app form: agreements, never ticked', auto: false },
|
||||
{ button: 'Create app (form)', class: 'final-submit', where: 'the Create app form: creates the app', auto: false },
|
||||
],
|
||||
|
|
@ -430,6 +443,7 @@ export function flowClass(form: FormId, name: string, role: string, where: FlowW
|
|||
if (!n || !['button', 'link'].includes(role)) return null;
|
||||
if (FINAL.test(n.toLocaleLowerCase('tr')) || FINAL.test(n)) return 'final-submit';
|
||||
if (nameIs(n, NEXT) || nameIs(n, BACK)) return 'navigation';
|
||||
if (form === 'play-app-record' && nameIs(n, CHECK_AVAILABILITY)) return 'navigation';
|
||||
if (form === 'apple-app-privacy') {
|
||||
if (nameIs(n, GET_STARTED) && where.privacyPage && !where.inDialog) return 'navigation';
|
||||
if (nameIs(n, EDIT) && where.inDataTypes && !where.inDialog) return 'navigation';
|
||||
|
|
@ -479,9 +493,19 @@ export function flowAllowed(form: FormId, name: string, role: string, where: Flo
|
|||
// the team menu (exact name), is a link / menu item / option / row / button, and is never a sign-out or a deny-listed
|
||||
// control (forbiddenControl stays absolute, so a studio named "Next Games" is not clicked: the owner picks it).
|
||||
|
||||
/** Play Console's Create app form of one developer account (verified 2026-10-12). */
|
||||
export const playCreateApp = (playBase: string, developerId: string): string => `${playBase.replace(/\/$/, '')}/u/0/developers/${encodeURIComponent(developerId)}/create-new-app`;
|
||||
|
||||
/** Play Console's app list of one developer account. */
|
||||
export const playAppList = (playBase: string, developerId: string): string => `${playBase.replace(/\/$/, '')}/u/0/developers/${encodeURIComponent(developerId)}/app-list`;
|
||||
|
||||
/** The Play Console app id in a URL (/developers/<id>/app/<digits>/...), or null. */
|
||||
export function playAppIdOf(url: string): string | null {
|
||||
try { return new URL(url).pathname.match(/\/developers\/\d{1,25}\/app\/(\d{1,25})(?:\/|$)/)?.[1] ?? null; } catch { return null; }
|
||||
}
|
||||
/** An app's Play Console dashboard (verified 2026-10-12: where Create app lands). */
|
||||
export const playAppDashboard = (playBase: string, developerId: string, appId: string): string => `${playBase.replace(/\/$/, '')}/u/0/developers/${encodeURIComponent(developerId)}/app/${encodeURIComponent(appId)}/app-dashboard`;
|
||||
|
||||
/** The developer account id in a Play Console URL (/developers/<digits>), or null. */
|
||||
export function playDeveloperId(url: string): string | null {
|
||||
try { return new URL(url).pathname.match(/\/developers\/(\d{1,25})(?:\/|$)/)?.[1] ?? null; } catch { return null; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue