mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-01 19:25:10 +02:00
a468374272
* fix: enrich command descriptions and snapshot flags for LLM eval quality 14 command descriptions enriched with specific arg formats, valid values, error behavior, and return types. Fixed header usage from <name> <value> to <name>:<value>. Added cookie usage syntax. Snapshot flags now show long names, ref numbering, and output format examples. * refactor: auto-generate server.ts help text from COMMAND_DESCRIPTIONS Replace hand-maintained help block with generateHelpText() that reads from COMMAND_DESCRIPTIONS and SNAPSHOT_FLAGS. Eliminates help text drift from source of truth. * test: add usage consistency and pipe guard tests Usage consistency test cross-checks Usage: patterns in implementation against COMMAND_DESCRIPTIONS using structural skeleton comparison. Pipe guard test ensures descriptions don't contain | which would break markdown table rendering. * chore: upgrade eval judge to Sonnet 4.6, update changelog Switch LLM-as-judge evals from Haiku to Sonnet 4.6 for more stable, nuanced scoring. Add changelog entry for all eval improvements. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
175 lines
6.2 KiB
TypeScript
175 lines
6.2 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* Generate SKILL.md files from .tmpl templates.
|
|
*
|
|
* Pipeline:
|
|
* read .tmpl → find {{PLACEHOLDERS}} → resolve from source → format → write .md
|
|
*
|
|
* Supports --dry-run: generate to memory, exit 1 if different from committed file.
|
|
* Used by skill:check and CI freshness checks.
|
|
*/
|
|
|
|
import { COMMAND_DESCRIPTIONS } from '../browse/src/commands';
|
|
import { SNAPSHOT_FLAGS } from '../browse/src/snapshot';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const DRY_RUN = process.argv.includes('--dry-run');
|
|
|
|
// ─── Placeholder Resolvers ──────────────────────────────────
|
|
|
|
function generateCommandReference(): string {
|
|
// Group commands by category
|
|
const groups = new Map<string, Array<{ command: string; description: string; usage?: string }>>();
|
|
for (const [cmd, meta] of Object.entries(COMMAND_DESCRIPTIONS)) {
|
|
const list = groups.get(meta.category) || [];
|
|
list.push({ command: cmd, description: meta.description, usage: meta.usage });
|
|
groups.set(meta.category, list);
|
|
}
|
|
|
|
// Category display order
|
|
const categoryOrder = [
|
|
'Navigation', 'Reading', 'Interaction', 'Inspection',
|
|
'Visual', 'Snapshot', 'Meta', 'Tabs', 'Server',
|
|
];
|
|
|
|
const sections: string[] = [];
|
|
for (const category of categoryOrder) {
|
|
const commands = groups.get(category);
|
|
if (!commands || commands.length === 0) continue;
|
|
|
|
// Sort alphabetically within category
|
|
commands.sort((a, b) => a.command.localeCompare(b.command));
|
|
|
|
sections.push(`### ${category}`);
|
|
sections.push('| Command | Description |');
|
|
sections.push('|---------|-------------|');
|
|
for (const cmd of commands) {
|
|
const display = cmd.usage ? `\`${cmd.usage}\`` : `\`${cmd.command}\``;
|
|
sections.push(`| ${display} | ${cmd.description} |`);
|
|
}
|
|
sections.push('');
|
|
}
|
|
|
|
return sections.join('\n').trimEnd();
|
|
}
|
|
|
|
function generateSnapshotFlags(): string {
|
|
const lines: string[] = [
|
|
'The snapshot is your primary tool for understanding and interacting with pages.',
|
|
'',
|
|
'```',
|
|
];
|
|
|
|
for (const flag of SNAPSHOT_FLAGS) {
|
|
const label = flag.valueHint ? `${flag.short} ${flag.valueHint}` : flag.short;
|
|
lines.push(`${label.padEnd(10)}${flag.long.padEnd(24)}${flag.description}`);
|
|
}
|
|
|
|
lines.push('```');
|
|
lines.push('');
|
|
lines.push('All flags can be combined freely. `-o` only applies when `-a` is also used.');
|
|
lines.push('Example: `$B snapshot -i -a -C -o /tmp/annotated.png`');
|
|
lines.push('');
|
|
lines.push('**Ref numbering:** @e refs are assigned sequentially (@e1, @e2, ...) in tree order.');
|
|
lines.push('@c refs from `-C` are numbered separately (@c1, @c2, ...).');
|
|
lines.push('');
|
|
lines.push('After snapshot, use @refs as selectors in any command:');
|
|
lines.push('```bash');
|
|
lines.push('$B click @e3 $B fill @e4 "value" $B hover @e1');
|
|
lines.push('$B html @e2 $B css @e5 "color" $B attrs @e6');
|
|
lines.push('$B click @c1 # cursor-interactive ref (from -C)');
|
|
lines.push('```');
|
|
lines.push('');
|
|
lines.push('**Output format:** indented accessibility tree with @ref IDs, one element per line.');
|
|
lines.push('```');
|
|
lines.push(' @e1 [heading] "Welcome" [level=1]');
|
|
lines.push(' @e2 [textbox] "Email"');
|
|
lines.push(' @e3 [button] "Submit"');
|
|
lines.push('```');
|
|
lines.push('');
|
|
lines.push('Refs are invalidated on navigation — run `snapshot` again after `goto`.');
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
const RESOLVERS: Record<string, () => string> = {
|
|
COMMAND_REFERENCE: generateCommandReference,
|
|
SNAPSHOT_FLAGS: generateSnapshotFlags,
|
|
};
|
|
|
|
// ─── Template Processing ────────────────────────────────────
|
|
|
|
const GENERATED_HEADER = `<!-- AUTO-GENERATED from {{SOURCE}} — do not edit directly -->\n<!-- Regenerate: bun run gen:skill-docs -->\n`;
|
|
|
|
function processTemplate(tmplPath: string): { outputPath: string; content: string } {
|
|
const tmplContent = fs.readFileSync(tmplPath, 'utf-8');
|
|
const relTmplPath = path.relative(ROOT, tmplPath);
|
|
const outputPath = tmplPath.replace(/\.tmpl$/, '');
|
|
|
|
// Replace placeholders
|
|
let content = tmplContent.replace(/\{\{(\w+)\}\}/g, (match, name) => {
|
|
const resolver = RESOLVERS[name];
|
|
if (!resolver) throw new Error(`Unknown placeholder {{${name}}} in ${relTmplPath}`);
|
|
return resolver();
|
|
});
|
|
|
|
// Check for any remaining unresolved placeholders
|
|
const remaining = content.match(/\{\{(\w+)\}\}/g);
|
|
if (remaining) {
|
|
throw new Error(`Unresolved placeholders in ${relTmplPath}: ${remaining.join(', ')}`);
|
|
}
|
|
|
|
// Prepend generated header (after frontmatter)
|
|
const header = GENERATED_HEADER.replace('{{SOURCE}}', path.basename(tmplPath));
|
|
const fmEnd = content.indexOf('---', content.indexOf('---') + 3);
|
|
if (fmEnd !== -1) {
|
|
const insertAt = content.indexOf('\n', fmEnd) + 1;
|
|
content = content.slice(0, insertAt) + header + content.slice(insertAt);
|
|
} else {
|
|
content = header + content;
|
|
}
|
|
|
|
return { outputPath, content };
|
|
}
|
|
|
|
// ─── Main ───────────────────────────────────────────────────
|
|
|
|
function findTemplates(): string[] {
|
|
const templates: string[] = [];
|
|
const candidates = [
|
|
path.join(ROOT, 'SKILL.md.tmpl'),
|
|
path.join(ROOT, 'browse', 'SKILL.md.tmpl'),
|
|
];
|
|
for (const p of candidates) {
|
|
if (fs.existsSync(p)) templates.push(p);
|
|
}
|
|
return templates;
|
|
}
|
|
|
|
let hasChanges = false;
|
|
|
|
for (const tmplPath of findTemplates()) {
|
|
const { outputPath, content } = processTemplate(tmplPath);
|
|
const relOutput = path.relative(ROOT, outputPath);
|
|
|
|
if (DRY_RUN) {
|
|
const existing = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, 'utf-8') : '';
|
|
if (existing !== content) {
|
|
console.log(`STALE: ${relOutput}`);
|
|
hasChanges = true;
|
|
} else {
|
|
console.log(`FRESH: ${relOutput}`);
|
|
}
|
|
} else {
|
|
fs.writeFileSync(outputPath, content);
|
|
console.log(`GENERATED: ${relOutput}`);
|
|
}
|
|
}
|
|
|
|
if (DRY_RUN && hasChanges) {
|
|
console.error('\nGenerated SKILL.md files are stale. Run: bun run gen:skill-docs');
|
|
process.exit(1);
|
|
}
|