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
|
|
@ -6,12 +6,16 @@
|
|||
row per data type as plain <p> texts: the type label, a "?" help button (aria-label "More information") and
|
||||
"Set Up <type>", a <p> inside a div with the click handler (no button role, no link). Purchase History's row has
|
||||
only its "Set Up" (the console shows the category "Purchases" above it).
|
||||
The per-type DIALOG is MODELED from Apple's help page "Manage app privacy"
|
||||
(https://developer.apple.com/help/app-store-connect/manage-app-information/manage-app-privacy: for each data type,
|
||||
the purposes, whether it is linked to the user's identity, whether it is used for tracking; Next between, Save at the
|
||||
end) in the markup of the real Data Collection dialog: <dialog role="dialog"> titled by <h2><span>, checkboxes whose
|
||||
<label> holds the label and its help text as two <p>, radios "<strong>Yes</strong>, ..." / "<strong>No</strong>, ...",
|
||||
Back / Cancel / Next / Save. Saving shows the type's purposes in its row and an Edit instead of "Set Up".
|
||||
The per-type DIALOG follows the flow verified on a real console (October 2026, structure only): a <dialog> whose h2
|
||||
is the type name; step 1 purposes: 6 checkboxes in Apple's order with NO accessible name (the label and its help
|
||||
text sit in a sibling <label> without for=, run together), Cancel / Next; step 2 linked: radios "Yes, <type lower>
|
||||
data collected from this app is linked to the user’s identity" / "No, ...", Back / Cancel / Next; step 3 an info
|
||||
page "Let’s define two important terms..." and step 4 an info page "To help put tracking into context, here are a
|
||||
few examples:" (no inputs, Back / Cancel / Next); step 5 tracking: radios "Yes, we use <type lower> data for
|
||||
tracking purposes" / "No, we do not use ...", "Definitions and Examples" (help, never clicked) / Back / Cancel /
|
||||
Save. Saving closes the dialog; the row then shows an Edit and the summary ("Used for <purposes>", "Linked to the
|
||||
user’s identity" or "Not linked to the user’s identity", "Used for tracking purposes" or "Not used for tracking
|
||||
purposes"), which `console verify` reads.
|
||||
?unknown=<type>: that type's second step is one storebound does not know (switches). ?preset=<type>:<purpose>|...
|
||||
(repeatable): that type was set up before with these purposes.
|
||||
window.__setups: the "Set Up" texts clicked; window.__answers: what each Save stored; window.__saves. -->
|
||||
|
|
@ -65,10 +69,13 @@ function setUpOpen(type) {
|
|||
function setUpDone(type, purposes) {
|
||||
const a = document.querySelector(`[data-type="${type}"] .action`);
|
||||
a.onclick = null;
|
||||
a.innerHTML = `<p>${purposes.join(', ')}</p><button type="button" role="button">Edit</button>`;
|
||||
const ans = window.__answers[type];
|
||||
a.innerHTML = `<p>Used for ${purposes.join(', ')}</p>`
|
||||
+ (ans ? `<p>${ans.linked === 'yes' ? 'Linked' : 'Not linked'} to the user’s identity</p><p>${ans.tracking === 'yes' ? 'Used' : 'Not used'} for tracking purposes</p>` : '')
|
||||
+ `<button type="button" role="button">Edit</button>`;
|
||||
a.querySelector('button').addEventListener('click', () => openType(type));
|
||||
}
|
||||
// the per-type dialog: purposes > Next > linked > Next > definitions (text only, real console) > Next > tracking > Save
|
||||
// the per-type dialog: purposes > Next > linked > Next > definitions > Next > examples (both text only) > Next > tracking > Save
|
||||
function openType(type) {
|
||||
const low = type.toLowerCase();
|
||||
const d = document.createElement('dialog');
|
||||
|
|
@ -76,16 +83,17 @@ function openType(type) {
|
|||
const radio = (name, v, html) => `<div><input type="radio" name="${name}" id="${name}_${v}" value="${v}"><div><label for="${name}_${v}"><span>${html}</span></label></div></div>`;
|
||||
const step2 = unknown === type
|
||||
? `<p>Which kinds of identity does ${low} carry?</p><span role="switch" aria-checked="false" tabindex="0">Account identity</span><span role="switch" aria-checked="false" tabindex="0">Device identity</span>`
|
||||
: radio('linked', 'yes', `<strong>Yes</strong>, ${low} collected from this app is linked to the user’s identity`) + radio('linked', 'no', `<strong>No</strong>, ${low} collected from this app is not linked to the user’s identity`);
|
||||
: radio('linked', 'yes', `<strong>Yes</strong>, ${low} data collected from this app is linked to the user’s identity`) + radio('linked', 'no', `<strong>No</strong>, ${low} data collected from this app is not linked to the user’s identity`);
|
||||
d.innerHTML = `<div role="document"><h2 id="modal-header"><span>${type}</span></h2>`
|
||||
+ `<div data-p="1"><p>Indicate how ${low} collected from this app is being used by you or your third-party partners.</p>`
|
||||
+ PURPOSES.map((p, i) => `<div><input type="checkbox" id="purpose_${i}"><div><label for="purpose_${i}"><p>${p}</p><p>Description of ${p}</p></label></div></div>`).join('')
|
||||
+ PURPOSES.map((p, i) => `<div><input type="checkbox" id="purpose_${i}"><div><label><p>${p}</p><p>Description of ${p}</p></label></div></div>`).join('')
|
||||
+ `<div><button type="button">Cancel</button><button type="button" data-go="2">Next</button></div></div>`
|
||||
+ `<div data-p="2" hidden>${step2}<div><button type="button" data-go="1">Back</button><button type="button">Cancel</button><button type="button" data-go="3">Next</button></div></div>`
|
||||
+ `<div data-p="3" hidden><p>Let’s define two important terms before continuing.</p><h5>Tracking</h5><p>Linking data collected from your app with third-party data.</p><h5>Third-Party Data</h5><p>Data about a user collected from other companies.</p><div><button type="button" data-go="2">Back</button><button type="button">Cancel</button><button type="button" data-go="4">Next</button></div></div>`
|
||||
+ `<div data-p="4" hidden>`
|
||||
+ radio('tracking', 'yes', `<strong>Yes</strong>, we use ${low} for tracking purposes`) + radio('tracking', 'no', `<strong>No</strong>, we do not use ${low} for tracking purposes`)
|
||||
+ `<div><button type="button" data-go="3">Back</button><button type="button">Cancel</button><button type="button" data-save>Save</button></div></div></div>`;
|
||||
+ `<div data-p="4" hidden><p>To help put tracking into context, here are a few examples:</p><ul><li>Displaying targeted advertisements.</li><li>Sharing location data with a data broker.</li></ul><div><button type="button" data-go="3">Back</button><button type="button">Cancel</button><button type="button" data-go="5">Next</button></div></div>`
|
||||
+ `<div data-p="5" hidden>`
|
||||
+ radio('tracking', 'yes', `<strong>Yes</strong>, we use ${low} data for tracking purposes`) + radio('tracking', 'no', `<strong>No</strong>, we do not use ${low} data for tracking purposes`)
|
||||
+ `<div><button type="button" data-help>Definitions and Examples</button><button type="button" data-go="4">Back</button><button type="button">Cancel</button><button type="button" data-save>Save</button></div></div></div>`;
|
||||
d.setAttribute('open', '');
|
||||
d.addEventListener('click', e => {
|
||||
const go = e.target.closest('[data-go]')?.getAttribute('data-go');
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
<h1>Create app</h1>
|
||||
<form onsubmit="return false">
|
||||
<p><label>App name <input type="text" maxlength="30"></label></p>
|
||||
<p><label>App package name <input type="text" maxlength="150"></label></p>
|
||||
<p><label>Store language <select><option value="">Choose a language</option><option>English (United States) – en-US</option></select></label></p>
|
||||
<fieldset><legend>App or game</legend>
|
||||
<label><input type="radio" name="type"> App</label> <label><input type="radio" name="type"> Game</label>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
<form onsubmit="return false">
|
||||
<h2>Uygulama ayrıntıları</h2>
|
||||
<p><label>Uygulama adı <input type="text" maxlength="30"></label></p>
|
||||
<p><label>Uygulama paket adı <input type="text" maxlength="150"></label></p>
|
||||
<p><label>Varsayılan dil <select><option value="">Dil seçin</option><option>İngilizce (Birleşik Krallık) – en-GB</option><option>İngilizce (ABD) – en-US</option><option>Türkçe – tr-TR</option></select></label></p>
|
||||
<fieldset><legend>Uygulama veya oyun</legend>
|
||||
<label><input type="radio" name="type"> Uygulama</label> <label><input type="radio" name="type"> Oyun</label>
|
||||
|
|
|
|||
46
tests/fixtures/console/play-create-app.html
vendored
46
tests/fixtures/console/play-create-app.html
vendored
|
|
@ -1,21 +1,30 @@
|
|||
<!doctype html>
|
||||
<!-- MODELED, not recorded: Play Console > Create app (App details: App name, Default language; App or game; Free or
|
||||
paid; Declarations: Developer Program Policies, US export laws), written from Google's public help "Create and set
|
||||
up your app". The declarations are agreements storebound never ticks; the final "Create app" is the owner's click.
|
||||
Placeholder data only. -->
|
||||
<!-- Play Console > Create app (/console/u/0/developers/<id>/create-new-app). Structure verified on a real console
|
||||
(2026-10-12, driven through Claude in Chrome; structure only, no data copied): App name (textbox, 30 max), App
|
||||
package name (textbox, 150 max) with a read-only "Check availability" button that answers "Package name
|
||||
available", Default language, App or game and Free or paid as <material-radio> elements (an inner [role=radio]
|
||||
with aria-checked; the native input inside does NOT update the form's model: until the inner [role=radio] is
|
||||
clicked the form shows "Select app or game" / "Select free or paid"), and the two declarations (agreements the
|
||||
owner ticks). The final "Create app" is the owner's click. Placeholder data only.
|
||||
window.__model: the form's model (what Angular would hold). -->
|
||||
<html lang="en"><head><meta charset="utf-8"><title>Create app - Play Console</title><script src="/_common.js"></script></head>
|
||||
<body>
|
||||
<h1>Create app</h1>
|
||||
<form onsubmit="return false">
|
||||
<h2>App details</h2>
|
||||
<p><label>App name <input type="text" maxlength="30"></label></p>
|
||||
<p><label>App package name <input type="text" maxlength="150" id="pkg"></label> <button type="button" id="check">Check availability</button> <span id="avail"></span></p>
|
||||
<p><label>Default language <select><option value="">Choose a language</option><option>English (United Kingdom) – en-GB</option><option>English (United States) – en-US</option><option>Turkish – tr-TR</option></select></label></p>
|
||||
<fieldset><legend>App or game</legend>
|
||||
<label><input type="radio" name="type"> App</label> <label><input type="radio" name="type"> Game</label>
|
||||
</fieldset>
|
||||
<fieldset><legend>Free or paid</legend>
|
||||
<label><input type="radio" name="price"> Free</label> <label><input type="radio" name="price"> Paid</label>
|
||||
</fieldset>
|
||||
<div role="radiogroup" aria-label="App or game">
|
||||
<material-radio><div role="radio" aria-checked="false" tabindex="0" aria-labelledby="t-app"><input type="radio" name="type" tabindex="-1" style="opacity:0;position:absolute;width:1px;height:1px"></div><span id="t-app">App</span></material-radio>
|
||||
<material-radio><div role="radio" aria-checked="false" tabindex="0" aria-labelledby="t-game"><input type="radio" name="type" tabindex="-1" style="opacity:0;position:absolute;width:1px;height:1px"></div><span id="t-game">Game</span></material-radio>
|
||||
<p class="hint" data-group="type">Select app or game</p>
|
||||
</div>
|
||||
<div role="radiogroup" aria-label="Free or paid">
|
||||
<material-radio><div role="radio" aria-checked="false" tabindex="0" aria-labelledby="p-free"><input type="radio" name="price" tabindex="-1" style="opacity:0;position:absolute;width:1px;height:1px"></div><span id="p-free">Free</span></material-radio>
|
||||
<material-radio><div role="radio" aria-checked="false" tabindex="0" aria-labelledby="p-paid"><input type="radio" name="price" tabindex="-1" style="opacity:0;position:absolute;width:1px;height:1px"></div><span id="p-paid">Paid</span></material-radio>
|
||||
<p class="hint" data-group="price">Select free or paid</p>
|
||||
</div>
|
||||
<h2>Declarations</h2>
|
||||
<fieldset><legend>Developer Program Policies</legend>
|
||||
<label><input type="checkbox"> Confirm app meets the Developer Program Policies</label>
|
||||
|
|
@ -25,4 +34,21 @@
|
|||
</fieldset>
|
||||
<button type="button">Cancel</button> <button type="submit">Create app</button>
|
||||
</form>
|
||||
<style>material-radio { display: inline-block; margin-right: 12px } [role=radio] { display: inline-block; width: 14px; height: 14px; border: 1px solid #555; border-radius: 50%; vertical-align: middle } [role=radio][aria-checked=true] { background: #1a73e8 }</style>
|
||||
<script>
|
||||
window.__model = { type: null, price: null };
|
||||
document.querySelectorAll('[role=radiogroup]').forEach(g => {
|
||||
const key = g.querySelector('input').name;
|
||||
g.querySelectorAll('material-radio [role=radio]').forEach(r => r.addEventListener('click', () => {
|
||||
// only a click on the inner [role=radio] updates the model (the native input does not)
|
||||
g.querySelectorAll('[role=radio]').forEach(x => x.setAttribute('aria-checked', String(x === r)));
|
||||
r.querySelector('input').checked = true;
|
||||
window.__model[key] = document.getElementById(r.getAttribute('aria-labelledby')).textContent;
|
||||
g.querySelector('.hint').hidden = true;
|
||||
}));
|
||||
});
|
||||
document.getElementById('check').addEventListener('click', () => {
|
||||
document.getElementById('avail').textContent = document.getElementById('pkg').value ? 'Package name available' : 'Enter a package name';
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue