Files
ai-for-dummies/public/hands-on/rules/app.js
T
2026-09-05 01:39:47 +00:00

125 lines
5.6 KiB
JavaScript

// Five rule sources lifted from netcracker/interview.
// Toggling a rule injects its body into the ruled prompt.
const RULES = [
{
id: 'agents',
name: 'AGENTS.md',
kind: 'Repo-wide instruction',
path: 'AGENTS.md',
en: 'Read AGENTS.md before touching this repo. Stack: pnpm + turbo monorepo, Go API, Next.js apps. Gates run from the monorepo root: pnpm lint, typecheck, test.',
pt: 'Leia AGENTS.md antes de tocar neste repo. Stack: pnpm + turbo monorepo, API em Go, apps Next.js. Gates rodam da raiz: pnpm lint, typecheck, test.'
},
{
id: 'skill',
name: 'gate-discipline skill',
kind: 'Skill body',
path: '.agents/skills/gate-discipline/SKILL.md',
en: 'Skill `gate-discipline`: every gate runs separately with `$?`. No `| tail`. `TURBO_FORCE=true` if a pass looks too cheap. Generated code is regenerated, never hand-edited.',
pt: 'Skill `gate-discipline`: cada gate roda separado com `$?`. Nada de `| tail`. `TURBO_FORCE=true` se o passar for bom demais. Código gerado se regenera, nunca se edita.'
},
{
id: 'husky',
name: 'Husky pre-commit',
kind: 'Git hook (commit time)',
path: '.husky/pre-commit',
en: 'Pre-commit runs `pnpm exec lint-staged`, then `node scripts/check-ui-contract.mjs` (UI ratchet), then commitlint. Counts in the baseline may only go DOWN.',
pt: 'Pre-commit roda `pnpm exec lint-staged`, depois `node scripts/check-ui-contract.mjs` (ratchet de UI), depois commitlint. Contadores do baseline só podem DIMINUIR.'
},
{
id: 'enforcer',
name: 'check-ui-contract.mjs',
kind: 'Custom enforcer (CLI)',
path: 'scripts/check-ui-contract.mjs',
en: 'Enforcer scans for raw <button>, silent catches, pages without h1, hardcoded colors, duplicated components. Fails when a count goes UP. Fix = `--accept` to re-baseline lower.',
pt: 'Enforcer varre <button> cru, catch silencioso, páginas sem h1, cores hardcoded, componentes duplicados. Falha quando contador SOBE. Corrigir = `--accept` para re-baseline menor.'
},
{
id: 'commitlint',
name: 'commitlint',
kind: 'Commit-msg linter',
path: 'commitlint.config.cjs',
en: 'Commitlint enforces Conventional Commits. Format: `<type>(<scope>): <subject>`. Types: feat, fix, docs, refactor, test, chore, build, ci, perf, style.',
pt: 'Commitlint enforça Commits Convencionais. Formato: `<tipo>(<escopo>): <assunto>`. Tipos: feat, fix, docs, refactor, test, chore, build, ci, perf, style.'
}
];
// The task we're asking the agent to perform.
const TASK = {
en: { goal: 'Refactor the `getUserById` endpoint to return 404 instead of throwing.', ctx: 'services/api/internal/user/handler.go. No DB schema change.' },
pt: { goal: 'Refatorar o endpoint `getUserById` para retornar 404 em vez de lançar exceção.', ctx: 'services/api/internal/user/handler.go. Sem mudança de schema.' }
};
const NAIVE = {
en: `Task: ${TASK.en.goal}\nFile: ${TASK.en.ctx}\nPlease make the change and tell me when done.`,
pt: `Tarefa: ${TASK.pt.goal}\nArquivo: ${TASK.pt.ctx}\nFaça a mudança e me avise quando terminar.`
};
const state = { enabled: new Set(['agents']), lang: 'en' };
const ruleList = document.querySelector('#rule-list');
const ruleCount = document.querySelector('#rule-count');
const promptNaive = document.querySelector('#prompt-naive');
const promptRuled = document.querySelector('#prompt-ruled');
const copyBtn = document.querySelector('#copy-btn');
const langButtons = document.querySelectorAll('.lang-switch button');
function renderRules() {
ruleList.innerHTML = RULES.map((rule) => `
<article class="rule" data-id="${rule.id}">
<div>
<h3>${rule.name}</h3>
<p>${rule.kind} <code>${rule.path}</code></p>
</div>
<span class="rule-tag">${rule.id}</span>
<button class="toggle" type="button" data-rule="${rule.id}" aria-pressed="${state.enabled.has(rule.id)}" aria-label="Toggle ${rule.name}"></button>
</article>
`).join('');
ruleCount.textContent = `${state.enabled.size} / ${RULES.length} active`;
}
function renderPrompts() {
const lang = state.lang;
promptNaive.textContent = NAIVE[lang];
const active = RULES.filter((r) => state.enabled.has(r.id));
const blocks = active.map((r) => `# ${r.name} (${r.path})\n${r[lang]}`).join('\n\n');
const head = `Task: ${TASK[lang].goal}\nFile: ${TASK[lang].ctx}`;
const tail = lang === 'en'
? '\n\nRun each gate separately and print $? before claiming done.'
: '\n\nRode cada gate separado e imprima $? antes de dizer que terminou.';
promptRuled.textContent = blocks ? `${head}\n\n${blocks}${tail}` : head + tail;
}
function bind() {
ruleList.addEventListener('click', (e) => {
const btn = e.target.closest('.toggle');
if (!btn) return;
const id = btn.dataset.rule;
if (state.enabled.has(id)) state.enabled.delete(id); else state.enabled.add(id);
btn.setAttribute('aria-pressed', String(state.enabled.has(id)));
ruleCount.textContent = `${state.enabled.size} / ${RULES.length} active`;
renderPrompts();
});
langButtons.forEach((btn) => btn.addEventListener('click', () => {
state.lang = btn.dataset.lang;
langButtons.forEach((b) => b.setAttribute('aria-pressed', String(b === btn)));
renderPrompts();
}));
copyBtn.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(promptRuled.textContent);
copyBtn.dataset.copied = 'true';
copyBtn.textContent = 'Copied';
setTimeout(() => { copyBtn.dataset.copied = 'false'; copyBtn.textContent = 'Copy ruled prompt'; }, 1400);
} catch {
copyBtn.textContent = 'Copy failed — select and copy manually';
}
});
}
renderRules();
renderPrompts();
bind();