Fix/sdk screenshot gate parity (#2164)

* fix(sdk): grade docs/screenshot.png exactly, like the registry gate

* docs(wiki): document plugin MCP tools and the exact screenshot gate

* test(sdk): cover the preflight screenshot probe
This commit is contained in:
jubnl
2026-08-31 14:32:53 +02:00
committed by GitHub
parent e615011e73
commit 54b0e4eed4
9 changed files with 339 additions and 89 deletions
+20 -26
View File
@@ -25,7 +25,7 @@ import { listZipNames } from '../../zip.js';
import { verifyAuthorSignature, checkSignatureShape, SignatureError } from '../verify-signature.js';
import type { NetworkCheck, CheckContext, RegistryEntry, RegistryEntryVersion } from './types.js';
import { pass, fail, skip } from './types.js';
import { REQUIRED_SECTIONS, MIN_PROSE_CHARS, missingSections, placeholders, proseLength, images, undocumentedPermissions } from './readme.js';
import { REQUIRED_SECTIONS, MIN_PROSE_CHARS, missingSections, placeholders, proseLength, undocumentedPermissions } from './readme.js';
export const DEFAULT_REGISTRY = 'liketrek/TREK-Plugins';
@@ -246,32 +246,26 @@ const readmeAtCommit: NetworkCheck = {
const n = proseLength(md);
if (n < MIN_PROSE_CHARS) problems.push(`• only ${n}/${MIN_PROSE_CHARS} chars of prose`);
// Resolve every image against the pinned commit. A relative path that exists in your tree
// but was never committed 404s here — which is exactly the failure this catches.
const imgs = images(md);
if (!imgs.length) problems.push('• no screenshot (the registry requires at least one real image)');
else {
let anyOk = false;
const reasons: string[] = [];
for (const src of imgs) {
const url = /^https?:\/\//.test(src)
? src.includes('github.com') && src.includes('/blob/')
? src.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/')
: src
: rawUrl(c.entry.repo, v.commitSha, src);
try {
const r = await fetch(url, { headers: { 'User-Agent': 'trek-plugin-preflight', Range: 'bytes=0-2047' } });
const ct = r.headers.get('content-type') || '';
if (r.ok && ct.startsWith('image/')) {
anyOk = true;
break;
}
reasons.push(`${src} → ${r.status} ${ct || 'no content-type'}`);
} catch {
reasons.push(`${src} → unreachable`);
}
// The store cover, at the pinned commit. The registry's check-readme.mjs fetches EXACTLY
// `docs/screenshot.png` here — that precise path is what the store card loads — so a file
// that exists in your tree but was never committed 404s, which is exactly the failure this
// catches. README image links are irrelevant to this gate; don't scan them.
try {
const r = await fetch(rawUrl(c.entry.repo, v.commitSha, 'docs/screenshot.png'), {
headers: { 'User-Agent': 'trek-plugin-preflight', Range: 'bytes=0-2047' },
});
const ct = r.headers.get('content-type') || '';
if (!r.ok || !ct.startsWith('image/')) {
problems.push(
`• docs/screenshot.png does not resolve to an image at ${v.commitSha.slice(0, 8)} ` +
`(got ${r.status} ${ct || 'no content-type'}) — this exact file is the store cover; ` +
'run `trek-plugin shot`, commit it, and re-tag',
);
}
if (!anyOk) problems.push('• no screenshot resolved to a real image:\n ' + reasons.join('\n '));
} catch {
problems.push(
`• docs/screenshot.png is unreachable at ${v.commitSha.slice(0, 8)} — this exact file is the store cover`,
);
}
// Permission parity, against the manifest AT THE COMMIT (not the tree — they can differ).
+14 -30
View File
@@ -26,7 +26,6 @@ import {
missingSections,
placeholders,
proseLength,
images,
undocumentedPermissions,
} from './readme.js';
@@ -405,44 +404,29 @@ const docsReadmeProse: OfflineCheck = {
};
/**
* A screenshot that RESOLVES.
* The store cover: `docs/screenshot.png`, ON DISK.
*
* The old check regexed the README for an image *link* and passed if it found one. The scaffold
* writes `![screenshot](./docs/screenshot.png)` and never creates docs/ — so the old check passed
* on a link to a file that does not exist, and the author learned the truth from CI, after the
* release was immutable. Resolve the path on disk instead.
* Two generations of this check drifted lenient. The first regexed the README for an image
* *link* and passed on the scaffold's dangling `![screenshot](./docs/screenshot.png)`. The
* second resolved README image paths — but the registry (check-readme.mjs, TREK-Plugins #40)
* does not read the README for this gate at all: it fetches **exactly `docs/screenshot.png`**
* at the pinned commit, because that precise path is what the store card loads
* (`raw…/<commit>/docs/screenshot.png`). A README whose only images had other names went
* green here and red in CI, after the release was immutable. Check the file the store reads.
*/
const docsScreenshot: OfflineCheck = {
id: 'docs.screenshot',
stage: 'docs',
depth: 'offline',
severity: 'error',
title: 'README screenshot resolves to a real image',
title: 'docs/screenshot.png exists (the store cover)',
run: (c) => {
if (c.readme === undefined) return skip();
const imgs = images(c.readme);
if (!imgs.length) {
return fail(
'no images in the README',
'The store card shows this image, and the registry rejects a plugin without one. Capture a 16:9 shot\n' +
'(1600×900 is ideal — the card crops the edges) and commit it as docs/screenshot.png.',
'trek-plugin shot',
);
}
const remote = imgs.filter((u) => /^https?:\/\//.test(u));
const local = imgs.filter((u) => !/^https?:\/\//.test(u));
const resolved = local.filter((u) => c.exists(u.replace(/^\.?\//, '')));
if (resolved.length) return pass(resolved[0]);
// Nothing local resolves. A remote URL might still satisfy CI, but we cannot know offline —
// don't claim a pass we haven't earned, and don't claim a failure we can't prove.
if (remote.length) return pass(`${remote.length} remote image(s) — preflight verifies they resolve`);
const missing = local.filter((u) => !c.exists(u.replace(/^\.?\//, '')));
if (c.exists('docs/screenshot.png')) return pass('docs/screenshot.png');
return fail(
`${missing.join(', ')} ${missing.length === 1 ? 'does' : 'do'} not exist`,
'The README links this image but the file is not there, so the store card would be a broken image and the\n' +
'registry rejects it. Capture a 16:9 shot (1600×900 is ideal — the card crops the edges).',
'docs/screenshot.png does not exist',
'The registry fetches exactly this path at the pinned commit — it is the store card image, and a README\n' +
'that links other image names does not satisfy the gate. Capture a 16:9 shot (1600×900 is ideal — the\n' +
'card crops the edges) and commit it as docs/screenshot.png.',
'trek-plugin shot',
);
},
-7
View File
@@ -85,13 +85,6 @@ export function proseLength(md: string): number {
.trim().length;
}
/** Every image src the README references — markdown and <img>, minus data: URIs (CI ignores those). */
export function images(md: string): string[] {
const mdImgs = [...md.matchAll(/!\[[^\]]*\]\(\s*([^)\s]+)/g)].map((m) => m[1]);
const htmlImgs = [...md.matchAll(/<img[^>]+src\s*=\s*["']([^"']+)["']/gi)].map((m) => m[1]);
return [...new Set([...mdImgs, ...htmlImgs])].filter((u) => !u.startsWith('data:'));
}
/** Manifest permissions that are never named anywhere in the README. CI rejects these. */
export function undocumentedPermissions(md: string, permissions: string[]): string[] {
const lower = md.toLowerCase();