feat: scaffold astro publishing pipeline
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
# Hands-on · Rules
|
||||
|
||||
A tiny, zero-dependency demo showing how rule sources reshape the same prompt.
|
||||
|
||||
## Run
|
||||
|
||||
Open `index.html` directly. No build, no server, no `npm install`.
|
||||
|
||||
## What it shows
|
||||
|
||||
- Five rule sources, taken from a real monorepo (`netcracker/interview`):
|
||||
- `AGENTS.md` — repo-wide instruction file.
|
||||
- `.agents/skills/gate-discipline/SKILL.md` — skill body loaded on demand.
|
||||
- `.husky/pre-commit` — git hook that runs other enforcers.
|
||||
- `scripts/check-ui-contract.mjs` — custom CLI enforcer (ratchet).
|
||||
- `commitlint.config.cjs` — commit-msg linter.
|
||||
- Each rule has an on/off switch. Toggling a rule prepends its body to the **ruled** prompt.
|
||||
- EN ↔ PT toggle keeps both languages useful.
|
||||
- "Copy ruled prompt" copies the current ruled-prompt text to clipboard.
|
||||
- Responsive on mobile, Full HD, and 4K (one column under 720 px).
|
||||
|
||||
## Mirror of `/hands-on/starter`
|
||||
|
||||
Same visual system as the starter (`--paper`, `--ink`, `--blue`, `--gold`). Drop-in replacement under `hands-on/rules/`.
|
||||
|
||||
## Token budget
|
||||
|
||||
Page weight: ~5 KB total, no framework, no fetch.
|
||||
@@ -0,0 +1,124 @@
|
||||
// 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();
|
||||
@@ -0,0 +1,43 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Guardrails — Hands-on Rules</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<div><span>HANDS-ON / RULES</span><h1>Guardrails</h1></div>
|
||||
<p>Toggle rules. Same task, different coverage.</p>
|
||||
</header>
|
||||
|
||||
<section aria-labelledby="rules-heading">
|
||||
<div class="section-head"><h2 id="rules-heading">Rule sources</h2><span id="rule-count">0 / 5 active</span></div>
|
||||
<div id="rule-list" class="rule-list"></div>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="prompt-heading">
|
||||
<div class="section-head"><h2 id="prompt-heading">Prompt diff</h2>
|
||||
<div class="lang-switch" role="group" aria-label="Language">
|
||||
<button type="button" data-lang="en" aria-pressed="true">EN</button>
|
||||
<button type="button" data-lang="pt" aria-pressed="false">PT</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prompt-grid">
|
||||
<article class="prompt-card" data-side="naive">
|
||||
<header><span>NAIVE</span><h3>Plain prompt</h3></header>
|
||||
<pre id="prompt-naive"></pre>
|
||||
</article>
|
||||
<article class="prompt-card" data-side="ruled">
|
||||
<header><span>RULED</span><h3>With guardrails</h3></header>
|
||||
<pre id="prompt-ruled"></pre>
|
||||
<button id="copy-btn" type="button">Copy ruled prompt</button>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
:root{--paper:#f4f3ef;--ink:#173044;--muted:#687d8c;--blue:#5683a1;--gold:#efc86d;--line:#d5dde1}*{box-sizing:border-box}body{margin:0;min-width:320px;background:var(--paper);color:var(--ink);font-family:Arial,sans-serif}main{width:min(880px,calc(100% - 40px));margin:0 auto;padding:70px 0}header,.section-head{display:flex;justify-content:space-between;gap:30px;align-items:end}header{padding-bottom:50px;border-bottom:1px solid var(--line)}header span,.prompt-card>header span{font:10px monospace;letter-spacing:.08em}h1{margin:12px 0 0;font-size:clamp(48px,10vw,100px);letter-spacing:-.08em}header p{max-width:220px;color:var(--muted);line-height:1.5}section{padding-top:45px}h2{font-size:24px}.section-head>span{color:var(--blue);font:11px monospace}.rule-list,.prompt-grid{display:grid;gap:1px;background:var(--line);border:1px solid var(--line)}.rule{display:grid;grid-template-columns:1fr auto auto;gap:20px;padding:22px;background:var(--paper);align-items:center}.rule h3{margin:0 0 6px;font-size:17px}.rule p{margin:0;color:var(--muted);font-size:13px;line-height:1.45}.rule code{font:11px ui-monospace,monospace;color:var(--ink);background:var(--paper);padding:1px 5px;border:1px solid var(--line)}.rule-tag{font:10px monospace;letter-spacing:.08em;color:var(--muted);text-transform:uppercase}.toggle{appearance:none;width:44px;height:24px;border:1px solid var(--line);background:var(--paper);border-radius:12px;position:relative;cursor:pointer;transition:background .15s ease}.toggle::after{content:"";position:absolute;top:2px;left:2px;width:18px;height:18px;background:var(--ink);border-radius:50%;transition:transform .15s ease,background .15s ease}.toggle[aria-pressed="true"]{background:var(--blue);border-color:var(--blue)}.toggle[aria-pressed="true"]::after{transform:translateX(20px);background:var(--gold)}.prompt-grid{grid-template-columns:1fr 1fr;margin-top:18px}.prompt-card{background:var(--paper);padding:22px;display:flex;flex-direction:column;gap:14px}.prompt-card>header{display:flex;justify-content:space-between;align-items:end;padding-bottom:0;border-bottom:0}.prompt-card h3{margin:0;font-size:17px}.prompt-card pre{margin:0;font:12px ui-monospace,monospace;white-space:pre-wrap;word-break:break-word;color:var(--ink);background:var(--paper);border:1px solid var(--line);padding:14px;min-height:160px;line-height:1.5}#copy-btn{align-self:flex-start;appearance:none;border:1px solid var(--ink);background:var(--ink);color:var(--paper);font:11px monospace;letter-spacing:.08em;padding:9px 14px;cursor:pointer;text-transform:uppercase}#copy-btn[data-copied="true"]{background:var(--blue);border-color:var(--blue)}.lang-switch{display:flex;gap:1px;border:1px solid var(--line)}.lang-switch button{appearance:none;border:0;background:var(--paper);color:var(--muted);font:11px monospace;letter-spacing:.08em;padding:6px 10px;cursor:pointer;text-transform:uppercase}.lang-switch button[aria-pressed="true"]{background:var(--ink);color:var(--paper)}@media(max-width:720px){.prompt-grid{grid-template-columns:1fr}.rule{grid-template-columns:1fr}.rule-tag{display:none}}@media(max-width:560px){header{display:block}header p{margin-top:24px}.lang-switch{flex:1}.lang-switch button{flex:1}}
|
||||
Reference in New Issue
Block a user