desktop: long findings wrap inside the Store readiness card (a build-folder path pushed the store chips out); the release timeline's rail no longer breaks after the current step; the rollout bar stays under its stage. scan: function words of the listing languages (German eine / der, French les, Turkish bir, ...) are not repeated keywords; the dev fixture has a long-path finding
This commit is contained in:
parent
04647c079e
commit
bd83d036ba
5 changed files with 37 additions and 7 deletions
|
|
@ -2,6 +2,12 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- **Desktop:** long findings (a file path in a build folder) wrap inside the Store readiness card instead of pushing
|
||||
the store chips out of it; the release timeline's rail no longer breaks after the current step; the rollout bar
|
||||
stays under its stage.
|
||||
- **`scan`: function words of the listing languages** (German "eine", "der", French "les", Turkish "bir", ...) no
|
||||
longer count as repeated keywords in the Play full description.
|
||||
|
||||
- **`import type { ShipConfig } from 'storebound'` type-checks outside Bun.** The package ships declarations
|
||||
for ES module and CommonJS projects (`dist/types`, `dist/types-cjs`) and a JS entry for Node; Bun keeps the .ts source. 0.8.5 named only
|
||||
`cli/lib/config.ts`, so tsc (`next build`, vue-tsc, ts-loader, `tsc --noEmit`) failed on a scaffolded
|
||||
|
|
|
|||
|
|
@ -342,6 +342,8 @@ const VALIDATE: Record<string, ValidateResult> = {
|
|||
{ 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' },
|
||||
// a long unbreakable path (a merged manifest of a local build) must wrap inside the card
|
||||
{ store: 'play', level: 'warn', code: 'scan', msg: 'Play: Foreground service types: FOREGROUND_SERVICE is requested but no <service> declares android:foregroundServiceType (apps/native/android/app/build/intermediates/merged_manifest/release/processReleaseMainManifest/AndroidManifest.xml:74)', fix: 'Give each foreground service a type and its FOREGROUND_SERVICE_<TYPE> permission.' },
|
||||
],
|
||||
},
|
||||
'p-tidy': { blockers: 0, findings: [] },
|
||||
|
|
|
|||
|
|
@ -342,9 +342,11 @@ svg { flex: none; }
|
|||
.tl-step::before { content: ""; position: absolute; top: 50%; left: 0; right: 0; height: 2px; margin-top: -9px; background: var(--line-2); }
|
||||
.tl-step.first::before { left: 50%; }
|
||||
.tl-step:last-child::before { right: 50%; }
|
||||
.tl-step.done::before, .tl-step.cur::before { background: var(--accent-solid); }
|
||||
.tl-step.cur::before { right: 50%; }
|
||||
.tl-step.cur.first::before { display: none; }
|
||||
/* the current step: done (accent) up to its node, still to come (grey) after it, so the rail never breaks */
|
||||
.tl-step.done::before { background: var(--accent-solid); }
|
||||
.tl-step.cur::before { background: linear-gradient(to right, var(--accent-solid) 50%, var(--line-2) 50%); }
|
||||
.tl-step.cur.first::before { left: 50%; background: var(--line-2); }
|
||||
.tl-step.cur:last-child::before { background: var(--accent-solid); }
|
||||
.tl-node { position: relative; z-index: 1; width: 18px; height: 18px; margin-top: -18px; border-radius: 50%; background: var(--surface); border: 2px solid var(--line-2); display: grid; place-items: center; }
|
||||
.tl-node svg { width: 10px; height: 10px; color: #fff; }
|
||||
.tl-step.done .tl-node { background: var(--accent-solid); border-color: var(--accent-solid); }
|
||||
|
|
@ -354,7 +356,7 @@ svg { flex: none; }
|
|||
.tl-step.cur.accent .tl-node { border-color: var(--accent-solid); } .tl-step.cur.neutral .tl-node { border-color: var(--neutral-solid); }
|
||||
.tl-cap { position: absolute; top: calc(50% + 4px); font-size: var(--t-micro); color: var(--muted); white-space: nowrap; font-weight: 550; }
|
||||
.tl-step.cur .tl-cap { color: var(--ink); font-weight: 650; }
|
||||
.tl-roll { width: 76%; display: grid; gap: 4px; position: absolute; top: calc(50% + 2px); }
|
||||
.tl-roll { width: 60%; display: grid; gap: 4px; position: absolute; top: calc(50% + 2px); }
|
||||
.tl-roll .rbar { height: 6px; }
|
||||
.tl-roll .pct { font-size: var(--t-micro); text-align: center; font-weight: 650; font-variant-numeric: tabular-nums; }
|
||||
.tl-none { grid-column: 2 / -1; color: var(--muted); font-size: var(--t-meta); text-align: center; border: 1px dashed var(--line-2); border-radius: 8px; padding: 10px; }
|
||||
|
|
@ -475,8 +477,8 @@ svg { flex: none; }
|
|||
.li:first-child { border-top: 0; }
|
||||
.li .li-icon { width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; flex: none; }
|
||||
.li .li-icon svg, .li .li-icon .store-mark { width: 15px; height: 15px; }
|
||||
.li .li-main { flex: 1; min-width: 0; display: grid; gap: 2px; }
|
||||
.li .li-title { font-weight: 600; }
|
||||
.li .li-main { flex: 1; min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 2px; }
|
||||
.li .li-title { font-weight: 600; overflow-wrap: anywhere; }
|
||||
.li .li-sub { color: var(--muted); font-size: var(--t-meta); }
|
||||
.li.clickable { cursor: pointer; }
|
||||
.li.clickable:hover { background: var(--hover); }
|
||||
|
|
|
|||
|
|
@ -125,7 +125,24 @@ function fieldBlocks(file: { path: string; text: string } | undefined, field: Re
|
|||
return out;
|
||||
}
|
||||
|
||||
const STOP = new Set(['app', 'apps', 'game', 'games', 'the', 'and', 'for', 'with', 'your', 'you', 'that', 'this', 'from', 'have', 'are', 'will', 'can', 'all', 'more', 'just', 'into', 'every', 'each', 'one', 'when', 'what', 'they', 'them', 'their', 'our', 'out', 'not', 'any', 'also']);
|
||||
// function words of the listing languages (articles, pronouns, prepositions, conjunctions): repeating them is grammar,
|
||||
// not keyword stuffing (a German description used "eine" 10x and was flagged)
|
||||
const STOP = new Set([
|
||||
'app', 'apps', 'game', 'games', 'spiel', 'spiele', 'jeu', 'jeux', 'juego', 'juegos', 'gioco', 'giochi', 'jogo', 'jogos', 'oyun', 'spel',
|
||||
'the', 'and', 'for', 'with', 'your', 'you', 'that', 'this', 'from', 'have', 'are', 'will', 'can', 'all', 'more', 'just', 'into', 'every', 'each', 'one', 'when', 'what', 'they', 'them', 'their', 'our', 'out', 'not', 'any', 'also',
|
||||
// de
|
||||
'eine', 'einen', 'einem', 'einer', 'eines', 'der', 'die', 'das', 'dem', 'den', 'des', 'und', 'oder', 'aber', 'mit', 'für', 'von', 'auf', 'aus', 'bei', 'nach', 'über', 'unter', 'durch', 'ohne', 'gegen', 'dein', 'deine', 'deinen', 'deinem', 'deiner', 'ihre', 'ihren', 'ihr', 'sie', 'wir', 'ist', 'sind', 'wird', 'werden', 'kann', 'kannst', 'können', 'nicht', 'auch', 'noch', 'wie', 'was', 'wenn', 'dass', 'diese', 'dieser', 'dieses', 'jede', 'jeden', 'jeder', 'jedes', 'alle', 'mehr', 'sich', 'dich', 'dir', 'uns', 'zum', 'zur', 'im', 'ins', 'vom', 'beim', 'hat', 'hast', 'haben', 'bist', 'sein', 'seine',
|
||||
// fr
|
||||
'les', 'des', 'une', 'pour', 'avec', 'dans', 'sur', 'par', 'pas', 'plus', 'vous', 'votre', 'vos', 'nous', 'notre', 'nos', 'ton', 'ta', 'tes', 'est', 'sont', 'qui', 'que', 'quoi', 'mais', 'ou', 'chaque', 'tout', 'tous', 'toutes', 'toute', 'cette', 'ces', 'son', 'ses', 'leur', 'leurs', 'aux', 'du',
|
||||
// es / pt / it
|
||||
'una', 'uno', 'unos', 'unas', 'los', 'las', 'del', 'con', 'para', 'por', 'sin', 'sobre', 'entre', 'como', 'más', 'tus', 'tu', 'sus', 'que', 'cada', 'todo', 'todos', 'todas', 'esta', 'este', 'estos', 'estas', 'nuestro', 'nuestra', 'puedes', 'puede', 'pero', 'cuando',
|
||||
'uma', 'umas', 'uns', 'dos', 'das', 'com', 'pelo', 'pela', 'seu', 'sua', 'seus', 'suas', 'você', 'mais', 'muito', 'isso', 'essa', 'esse', 'pode', 'quando', 'também', 'também',
|
||||
'gli', 'della', 'delle', 'dello', 'degli', 'nel', 'nella', 'nelle', 'con', 'per', 'tra', 'fra', 'tuo', 'tua', 'tuoi', 'tue', 'ogni', 'tutto', 'tutti', 'tutte', 'questo', 'questa', 'sono', 'anche', 'quando', 'puoi', 'molto',
|
||||
// nl
|
||||
'een', 'het', 'van', 'voor', 'met', 'naar', 'jouw', 'jij', 'je', 'zijn', 'wordt', 'niet', 'ook', 'elke', 'alle', 'meer', 'deze', 'dit', 'als', 'maar', 'kan', 'kunt',
|
||||
// tr
|
||||
'bir', 've', 'ile', 'için', 'bu', 'şu', 'her', 'daha', 'çok', 'gibi', 'kadar', 'olan', 'olarak', 'veya', 'ama', 'sen', 'senin', 'sana', 'seni', 'biz', 'bizim', 'onu', 'onun', 'tüm', 'bütün', 'hem', 'da', 'de', 'ki',
|
||||
]);
|
||||
|
||||
export function playRules(inp: ScanInput, x: PlayCtx): void {
|
||||
const { facts: p, config: c } = inp;
|
||||
|
|
|
|||
|
|
@ -233,6 +233,9 @@ describe('Google Play rules', () => {
|
|||
for (const t of ['longer than 30 characters', 'Emoji', 'Rank, performance', 'ALL CAPS', 'Other apps or brands', 'Repeated keywords ("photo"'])
|
||||
expect(m.some(x => x.includes(t))).toBe(true);
|
||||
expect(m.find(x => x.includes('Repeated'))).toContain('and a keyword list');
|
||||
// function words of other languages are grammar, not stuffing (a German description used "eine" 10x)
|
||||
const de = await scanVariant('play-listing', { 'store/listing.md': s => s.replace(/\bphotos?\b/gi, w => `${w} eine der und eine`) });
|
||||
expect(de.findings.filter(f => f.rule === 'play-metadata').map(f => f.title).join('\n')).not.toMatch(/"(eine|der|und)"/);
|
||||
expect(find(r.findings, 'play-payments')!.evidence[0]).toMatchObject({ file: 'store/listing.md', line: 22 });
|
||||
expect(find(r.findings, 'play-impersonation')).toMatchObject({ severity: 'low', title: expect.stringContaining('"instagram"') });
|
||||
const ok = await scanVariant('play-listing', { 'storebound.config.ts': s => s.replace('org.example.instagramsaver', 'org.example.snapsaver') });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue