8 Commits

Author SHA1 Message Date
Marcos Paulo aa28158534 fix: restore the full-guide content task 15d dropped
verify-and-publish / gate (push) Failing after 10m35s
verify-and-publish / publish (push) Has been skipped
Merges refactor/task-15f-full-guide-restore.

Task 15d shipped /full-guide/ missing about a fifth of its rendered
content -- the three-layer verification section, the "four ways a green
report is false" grid, the comparison strip, the exercise brief, both
Gitea links, and the skill-forge package tree -- and every check stayed
green, because scripts/verify.mjs reads the legacy file. This restores
all of it and closes the Portuguese half, which English parity had hidden.

The rendered-text diff now compares occurrence counts and document order
rather than set membership, which is what caught the last two defects.

/full-guide/ is en 432/432 and pt 431/431, missing 0, extra 0, order clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:34:42 +00:00
Marcos Paulo 429b4e2e87 fix(full-guide): match the legacy Portuguese order, and pin deck order
Two defects the order-sensitive rendered-text diff surfaced.

Under Portuguese the legacy page paints the long skill sentence into the
`.skills` eyebrow, above the heading, because its
`.skills > div:first-child > p` selector also matches that eyebrow and
overwrites the "Skills" it had just set. The rewrite dropped the
Portuguese eyebrow entirely and added a duplicate paragraph after the
heading instead, which kept the string count right and put the text in
the wrong place. Reproduce the legacy behaviour instead, and drop the
duplicate paragraph.

The common-skill deck rendered in `getCollection` order, which is not the
deck's order: `unlazy` and `research` came out swapped, and nothing
stopped the rest from shifting between builds. Sort by the card number so
the tabs stay 01..07 as the legacy page has them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:33:56 +00:00
Marcos Paulo c9796a3e7f test: count and order rendered spans, not just their presence
The rendered-text diff compared two *sets* of strings, so it stayed at
"missing 0 · extra 0" while the built page painted a string a different
number of times, or in a different place. That is the same shape of hole
that let task 15d ship a full-guide missing a fifth of its content behind
a green gate.

Three changes:

- tally occurrences instead of set membership, so a string the legacy
  page paints twice has to be painted twice here;
- compare the sequences positionally and report the first divergence,
  which is what caught the Portuguese eyebrow and the reordered skill
  deck fixed in the next commit;
- fail loudly on a non-200 response. A 404 rendered as four spans of
  python's error page and the diff then reported the entire route as
  missing, which reads exactly like a real regression.

Two robustness fixes behind those: ask the kernel for a free port rather
than pinning 4196/4197 (back-to-back runs collided with the previous
run's server, which was still holding the port after its staging
directory had been deleted), and read the DOM until two consecutive
reads agree instead of once after a fixed wait.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:33:44 +00:00
Marcos Paulo 9486188983 fix(full-guide): restore Portuguese guide copy 2026-09-06 00:26:21 +00:00
Marcos Paulo 2560437bfa Merge remote-tracking branch 'origin/main' into refactor/task-15f-full-guide-restore 2026-09-06 00:12:55 +00:00
Marcos Paulo 18862e8e13 Merge remote-tracking branch 'origin/main' into refactor/task-15f-full-guide-restore 2026-09-06 00:11:17 +00:00
Marcos Paulo bd3805f883 Merge remote-tracking branch 'origin/main' into refactor/task-15f-full-guide-restore 2026-09-06 00:03:07 +00:00
Marcos Paulo 8f82304758 fix(full-guide): restore omitted guide sections
Restore the builder, hands-on, and verification content lost by task 15d, including the legacy bilingual pairs.\n\nDo not alter legacy files or verification assertions.
2026-09-06 00:02:51 +00:00
3 changed files with 460 additions and 62 deletions
+74 -10
View File
@@ -21,6 +21,7 @@ import { spawn } from 'node:child_process';
import { cpSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createServer } from 'node:net';
import { chromium } from 'playwright';
// `index` is the landing page: it lives at the repository root, not in a
@@ -43,9 +44,26 @@ const astroPath = route === 'index' ? '' : `${route}/`;
const staging = mkdtempSync(join(tmpdir(), 'af-rtd-'));
cpSync('dist', join(staging, 'ai-for-dummies'), { recursive: true });
// Ask the kernel for a free port rather than pinning one. Back-to-back runs
// used to collide: the previous run's server was still holding the fixed port
// while its staging directory had already been deleted, so every page came
// back as a 404 and the diff reported the whole route missing.
const freePort = () =>
new Promise((resolve, reject) => {
const probe = createServer();
probe.on('error', reject);
probe.listen(0, '127.0.0.1', () => {
const { port } = probe.address();
probe.close(() => resolve(port));
});
});
const legacyPort = await freePort();
const astroPort = await freePort();
const serve = (dir, port) =>
spawn('python3', ['-m', 'http.server', String(port), '-d', dir], { stdio: 'ignore' });
const servers = [serve('.', 4197), serve(staging, 4196)];
const servers = [serve('.', legacyPort), serve(staging, astroPort)];
const stop = () => {
servers.forEach((s) => s.kill());
rmSync(staging, { recursive: true, force: true });
@@ -77,7 +95,13 @@ try {
const browser = await chromium.launch();
const grab = async (url) => {
const page = await browser.newPage({ viewport: { width: 1400, height: 1000 } });
await page.goto(url, { waitUntil: 'load' });
const response = await page.goto(url, { waitUntil: 'load' });
// A 404 renders as four spans of python's error page and the diff then
// reports the entire route as missing, which reads exactly like a real
// regression. Fail loudly instead.
if (!response || !response.ok()) {
throw new Error(`${url} returned ${response ? response.status() : 'no response'}`);
}
// The islands hydrate and render their initial panel on load; without this
// every panel's copy reads as missing.
await page.waitForTimeout(1200);
@@ -87,23 +111,58 @@ try {
await toggle.click();
await page.waitForTimeout(1200);
}
const spans = await page.evaluate(visibleText);
// Islands hydrate at their own pace, and the language toggle repaints in
// more than one frame. A single read after a fixed wait is flaky, so read
// until two consecutive reads agree.
let spans = await page.evaluate(visibleText);
for (let i = 0; i < 10; i += 1) {
await page.waitForTimeout(300);
const next = await page.evaluate(visibleText);
if (next.length === spans.length && next.every((span, j) => span === spans[j])) {
spans = next;
break;
}
spans = next;
}
await page.close();
return spans;
};
const legacy = await grab(`http://localhost:4197/${legacyPath}`);
const astro = await grab(`http://localhost:4196/ai-for-dummies/${astroPath}`);
const legacy = await grab(`http://localhost:${legacyPort}/${legacyPath}`);
const astro = await grab(`http://localhost:${astroPort}/ai-for-dummies/${astroPath}`);
await browser.close();
const rendered = new Set(astro);
const missing = legacy.filter((span) => !rendered.has(span));
// Count occurrences, not membership. A set comparison reports zero when a
// string the legacy page paints four times is painted three times here --
// exactly the kind of near-miss that got past the earlier checks.
const tally = (spans) => {
const counts = new Map();
for (const span of spans) counts.set(span, (counts.get(span) || 0) + 1);
return counts;
};
const legacyCounts = tally(legacy);
const astroCounts = tally(astro);
const missing = [];
for (const [span, count] of legacyCounts) {
const short = count - (astroCounts.get(span) || 0);
for (let i = 0; i < short; i += 1) missing.push(span);
}
// Both directions. A string the Astro page paints and the legacy page does
// not is just as wrong: it means a translation was invented, or an English
// string was left standing where the legacy page swaps it.
const legacySpans = new Set(legacy);
const extra = astro.filter((span) => !legacySpans.has(span));
const extra = [];
for (const [span, count] of astroCounts) {
const over = count - (legacyCounts.get(span) || 0);
for (let i = 0; i < over; i += 1) extra.push(span);
}
// Order counts too. Both pages can paint the same strings while a block
// sits in the wrong place -- the Portuguese eyebrow, or a reordered card
// deck -- and a count-only comparison calls that clean.
const firstOutOfOrder = legacy.findIndex((span, i) => astro[i] !== span);
const mode = portuguese ? 'pt' : 'en';
console.log(
@@ -111,7 +170,12 @@ try {
);
for (const span of missing) console.log(` - ${span}`);
for (const span of extra) console.log(` + ${span}`);
process.exitCode = missing.length === 0 && extra.length === 0 ? 0 : 1;
if (firstOutOfOrder !== -1) {
console.log(` order diverges at span ${firstOutOfOrder}`);
console.log(` legacy: ${legacy[firstOutOfOrder]}`);
console.log(` astro: ${astro[firstOutOfOrder]}`);
}
process.exitCode = missing.length === 0 && extra.length === 0 && firstOutOfOrder === -1 ? 0 : 1;
} finally {
stop();
}
+2 -1
View File
@@ -121,7 +121,8 @@ const { target } = Astro.props;
if (!id) return;
const target = document.querySelector('#' + id);
if (!target) return;
const value = target.textContent || '';
const visible = target.querySelector(':scope > [data-language-content]:not([hidden])');
const value = visible?.textContent || target.textContent || '';
if (
typeof navigator !== 'undefined' &&
+372 -39
View File
@@ -55,6 +55,9 @@ const efforts = toRecord(effortEntries);
const skillFiles = toRecord(skillFileEntries);
const skillWorkflow = toRecord(workflowEntries);
const commonSkills = toRecord(commonSkillEntries);
const sortedCommonSkills = Object.values(commonSkills).sort((a, b) =>
a.number.localeCompare(b.number),
);
const handsOn = promptEntries.find(({ data }) => data.id === 'tiny-tasks')?.data;
const install = installEntries.find(({ data }) => data.id === 'install')?.data;
if (!handsOn || !install) throw new Error('full-guide content collections are incomplete');
@@ -82,6 +85,31 @@ const labels = {
source: { en: 'GITHUB SOURCE ↗', pt: 'FONTE NO GITHUB ↗' },
},
};
const builderSteps: Record<
string,
{ title: { en: string; pt: string }; tagline: { en: string; pt: string } }
> = {
observe: {
title: { en: 'Observe', pt: 'Observar' },
tagline: { en: 'find repeated friction', pt: 'encontre atrito repetido' },
},
trigger: {
title: { en: 'Define trigger', pt: 'Definir gatilho' },
tagline: { en: 'route precisely', pt: 'roteie com precisão' },
},
scaffold: {
title: { en: 'Choose anatomy', pt: 'Escolher anatomia' },
tagline: { en: 'only needed files', pt: 'apenas arquivos necessários' },
},
write: {
title: { en: 'Write guidance', pt: 'Escrever orientação' },
tagline: { en: 'decisions, not trivia', pt: 'decisões, não trivialidades' },
},
validate: {
title: { en: 'Validate', pt: 'Validar' },
tagline: { en: 'test real behavior', pt: 'teste comportamento real' },
},
};
const selectorData = {
phases,
workers,
@@ -235,7 +263,7 @@ const base = import.meta.env.BASE_URL;
<section class="hero" id="top-pt" data-language-content="pt" hidden>
<div>
<p class="eyebrow">Uma apresentação para quem entrega software</p><h1>
IA para<br /><em>iniciantes.</em>
AI for<br /><em>dummies.</em>
</h1><p class="lede">
Você não precisa de um exército de modelos. Precisa de um sistema: uma mente para
enquadrar o trabalho, várias mãos para executá-lo e uma fronteira clara entre cada tarefa.
@@ -307,7 +335,7 @@ const base = import.meta.env.BASE_URL;
>Modelo forte para ambiguidade.<br />Modelo leve para trabalho delimitado.</strong
>
</div><div class="signal" aria-hidden="true">
<b>PENSE</b><i></i><i></i><i></i><b>FAÇA</b>
<b>THINK</b><i></i><i></i><i></i><b>MAKE</b>
</div>
</section>
@@ -418,7 +446,8 @@ const base = import.meta.env.BASE_URL;
data-language-content="pt"
hidden>Clique em uma fase.<br /><em>Veja a passagem.</em></span
>
</h2><p>
</h2><!-- The legacy PT render paints this paragraph twice; preserve that output. -->
<p>
<span data-language-content="en"
>Delegation means moving one bounded task into a smaller contextnot giving away
responsibility.</span
@@ -690,7 +719,16 @@ const base = import.meta.env.BASE_URL;
></small
></button
><button class="active" data-effort="medium" role="tab" aria-selected="true"
>MEDIUM</button
><b
><span data-language-content="en">MEDIUM</span><span data-language-content="pt" hidden
>MÉDIO</span
></b
><small
><span data-language-content="en">default start</span><span
data-language-content="pt"
hidden>ponto inicial</span
></small
></button
><button data-effort="high" role="tab" aria-selected="false"
><b
><span data-language-content="en">HIGH</span><span data-language-content="pt" hidden
@@ -731,8 +769,15 @@ const base = import.meta.env.BASE_URL;
<section class="skills" id="skills">
<div>
<p class="eyebrow">
{
/* Under Portuguese the legacy page paints the long skill sentence
here, not "Skills": its `.skills > div:first-child > p` selector
also matches this eyebrow and overwrites it. Reproduced on
purpose -- see .agents/rules/content-i18n.md. */
}
<span data-language-content="en">Skills</span><span data-language-content="pt" hidden
>Skills</span
>Uma skill é um procedimento reutilizável. Ela pode carregar instruções, referências,
scripts e assets. Não é memória mágica e não substitui critérios de aceitação.</span
>
</p><h2>
<span data-language-content="en">Write the right way<br /><em>once.</em></span><span
@@ -778,16 +823,51 @@ const base = import.meta.env.BASE_URL;
>
</div>
</article>
</div>
</div><pre><code>name: review-ui · check focus, mobile, reduced motion · run verification · return evidence</code></pre>
</section>
<section class="skill-builder" id="create-skill">
<div class="section-label">
<span>Create a skill</span><span>repeatable pain reusable judgment</span>
<span
><span data-language-content="en">Create a skill</span><span
data-language-content="pt"
hidden>Criar uma skill</span
></span
><span
><span data-language-content="en">repeatable pain reusable judgment</span><span
data-language-content="pt"
hidden>atrito repetido julgamento reutilizável</span
></span
>
</div><div class="builder-intro">
<div>
<p class="eyebrow">
<span data-language-content="en">The skill forge</span><span
data-language-content="pt"
hidden>A forja de skills</span
>
</p><h2>
<span data-language-content="en"
>Teach the decision.<br />Keep the context <em>light.</em></span
><span data-language-content="pt" hidden
>Ensine a decisão.<br />Mantenha o contexto <em>leve.</em></span
>
</h2>
</div><p>
<span data-language-content="en"
>Do not package everything you know. Capture the non-obvious choices that repeatedly
improve an outcome, then prove the skill changes behavior.</span
><span data-language-content="pt" hidden
>Não empacote tudo o que você sabe. Capture as escolhas não óbvias que melhoram
resultados repetidamente e prove que a skill muda o comportamento.</span
>
</p>
</div><div class="builder-workbench">
<nav class="builder-steps" role="tablist" aria-label="Skill creation workflow">
{
Object.values(skillWorkflow).map((step) => (
['observe', 'trigger', 'scaffold', 'write', 'validate'].map((id) => {
const step = skillWorkflow[id];
return (
<button
class:list={{ active: step.id === 'observe' }}
data-skill-step={step.id}
@@ -796,10 +876,28 @@ const base = import.meta.env.BASE_URL;
>
<>
<b>{step.number}</b>
<span>{step.title.en}</span>
<>
<span>
<>
<span data-language-content="en">{builderSteps[step.id].title.en}</span>
<span data-language-content="pt" hidden>
{builderSteps[step.id].title.pt}
</span>
</>
</span>
<small>
<>
<span data-language-content="en">{builderSteps[step.id].tagline.en}</span>
<span data-language-content="pt" hidden>
{builderSteps[step.id].tagline.pt}
</span>
</>
</small>
</>
</>
</button>
))
);
})
}
</nav><article class="builder-detail" id="builder-detail" aria-live="polite">
<header>
@@ -819,7 +917,39 @@ const base = import.meta.env.BASE_URL;
>
</div>
</footer>
</article>
</article><aside class="builder-artifact">
<div class="artifact-head">
<span
><span data-language-content="en">OUTPUT / SKILL PACKAGE</span><span
data-language-content="pt"
hidden>SAÍDA / PACOTE DE SKILL</span
></span
><i></i>
</div><pre
aria-label="Example skill structure"><code>review-ui/<br /> SKILL.md<br /> agents/<br /> openai.yaml<br /> references/<br /> accessibility.md<br /> scripts/<br /> verify.mjs</code></pre><div
class="artifact-command"
>
<span
><span data-language-content="en">VALIDATE</span><span
data-language-content="pt"
hidden>VALIDAR</span
></span
><code>quick_validate.py ./review-ui</code>
</div>
</aside>
</div><div class="builder-loop">
<span
><span data-language-content="en">AFTER REAL USE</span><span
data-language-content="pt"
hidden>APÓS USO REAL</span
></span
><div data-language-content="en">
<b>observe failure</b><i></i><b>sharpen one rule</b><i></i><b>retest behavior</b><i></i
><b>keep it narrow</b>
</div><div data-language-content="pt" hidden>
<b>observar falha</b><i></i><b>refinar uma regra</b><i></i><b>retestar comportamento</b
><i></i><b>manter estreita</b>
</div>
</div>
</section>
@@ -866,7 +996,10 @@ const base = import.meta.env.BASE_URL;
</div><div class="skill-deck">
<div class="skill-index" role="tablist" aria-label="Common agent skills">
{
Object.values(commonSkills).map((skill) => (
/* getCollection's order is not the deck's order. Sort by the
card number so the tabs stay 01..07 as the legacy page has
them, instead of shifting between builds. */
sortedCommonSkills.map((skill) => (
<button
class:list={{ active: skill.id === 'ponytail' }}
data-common-skill={skill.id}
@@ -934,53 +1067,184 @@ const base = import.meta.env.BASE_URL;
</div><article class="install-skills">
<header>
<div>
<span>INSTALL PACK</span><strong
>Ask your coding agent to verify, install, and validate the skills.</strong
<span
><span data-language-content="en">INSTALL PACK</span><span
data-language-content="pt"
hidden>PACOTE DE INSTALAÇÃO</span
></span
><strong
><span data-language-content="en"
>Ask your coding agent to verify, install, and validate the skills.</span
><span data-language-content="pt" hidden
>Peça ao seu agente para verificar, instalar e validar as skills.</span
></strong
>
</div><CopyPrompt target="prompt-install-skills" />
</header><pre><code id="prompt-install-skills">{install.en}</code></pre><footer>
Review every source before installation. Existing local skills must be preserved.
</header><pre><code id="prompt-install-skills"><span data-language-content="en">{install.en}</span><span data-language-content="pt" hidden>{install.pt}</span></code></pre><footer
>
<span data-language-content="en"
>Review every source before installation. Existing local skills must be preserved.</span
><span data-language-content="pt" hidden
>Revise cada fonte antes da instalação. Skills locais existentes devem ser preservadas.</span
>
</footer>
</article>
</section>
<section class="hands-on" id="hands-on">
<div class="section-label">
<span>Hands-on</span><span>10 minutes / one missing feature</span>
<span
><span data-language-content="en">Hands-on</span><span data-language-content="pt" hidden
>Prática</span
></span
><span
><span data-language-content="en">10 minutes / one missing feature</span><span
data-language-content="pt"
hidden>10 minutos / uma feature ausente</span
></span
>
</div><div class="hands-intro">
<div>
<p class="eyebrow">Tiny Tasks lab</p><h2>
Same task.<br />Better <em>operating system.</em>
<p class="eyebrow">
<span data-language-content="en">Tiny Tasks lab</span><span
data-language-content="pt"
hidden>Laboratório Tiny Tasks</span
>
</p><h2>
<span data-language-content="en">Same task.<br />Better <em>operating system.</em></span
><span data-language-content="pt" hidden
>Mesma tarefa.<br />Melhor <em>sistema operacional.</em></span
>
</h2>
</div><div>
<p>
<p data-language-content="en">
Start with a deliberately incomplete static task board. Run one prompt as written,
reset, then run the skill-enabled version.
reset, then run the skill-enabled version. Compare diff size, verification evidence, and
unnecessary complexity.
</p><p data-language-content="pt" hidden>
Comece com um quadro estático propositalmente incompleto. Execute um prompt, restaure e
execute a versão com skills. Compare tamanho do diff, evidências e complexidade
desnecessária.
</p><div class="starter-links">
<a href={`${base}hands-on/starter/`} class="starter-link">Open the starter </a><a
href={`${base}hands-on/rules/`}
class="starter-link">Open the rules lab </a
<div class="starter-link-group">
<a href={`${base}hands-on/starter/`} class="starter-link"
><span data-language-content="en">Open the starter </span><span
data-language-content="pt"
hidden>Abrir o projeto inicial </span
></a
><a
href="https://git.marcospaulo.dev.br/netcracker/ai-for-dummies/src/branch/pages/hands-on/starter"
class="starter-link starter-link-source"
><span data-language-content="en">Clone from Gitea </span><span
data-language-content="pt"
hidden>Abrir o projeto inicial </span
></a
>
</div><div class="starter-link-group">
<a href={`${base}hands-on/rules/`} class="starter-link"
><span data-language-content="en">Open the rules lab </span><span
data-language-content="pt"
hidden>Abrir o projeto inicial </span
></a
><a
href="https://git.marcospaulo.dev.br/netcracker/ai-for-dummies/src/branch/pages/hands-on/rules"
class="starter-link starter-link-source"
><span data-language-content="en">Clone from Gitea </span><span
data-language-content="pt"
hidden>Abrir o projeto inicial </span
></a
>
</div>
</div>
</div>
</div><div class="exercise-brief">
<span
><span data-language-content="en">THE MISSING FEATURE</span><span
data-language-content="pt"
hidden>A FEATURE AUSENTE</span
></span
><strong
><span data-language-content="en"
>Add All / Open / Done filters that survive reload and browser navigation.</span
><span data-language-content="pt" hidden
>Adicione filtros Todos / Abertos / Concluídos que sobrevivem reload e navegação.</span
></strong
><div>
<span data-language-content="en"
><b>STACK</b> HTML · CSS · JavaScript <b>DEPENDENCIES</b> none <b>FILES</b> 3</span
><span data-language-content="pt" hidden
><b>STACK</b> HTML · CSS · JavaScript <b>DEPENDÊNCIAS</b> nenhuma <b>ARQUIVOS</b> 3</span
>
</div>
</div><div class="prompt-compare">
<article class="prompt-card">
<header>
<div><span>RUN A</span><strong>Good prompt</strong></div><CopyPrompt
target="prompt-basic"
/>
</header><pre><code id="prompt-basic">{handsOn.en.basic}</code></pre><footer>
Clear context · constraints · acceptance · evidence
<div>
<span>RUN A</span><strong
><span data-language-content="en">Good prompt</span><span
data-language-content="pt"
hidden>Bom prompt</span
></strong
>
</div><CopyPrompt target="prompt-basic" />
</header><pre><code id="prompt-basic"><span data-language-content="en">{handsOn.en.basic}</span><span data-language-content="pt" hidden>{handsOn.pt.basic}</span></code></pre><footer
>
<span data-language-content="en"
>Clear context · constraints · acceptance · evidence</span
><span data-language-content="pt" hidden
>Contexto claro · restrições · aceitação · evidência</span
>
</footer>
</article><article class="prompt-card enhanced">
<header>
<div><span>RUN B</span><strong>Good prompt + skills</strong></div><CopyPrompt
target="prompt-skills"
/>
</header><pre><code id="prompt-skills">{handsOn.en.skills}</code></pre><footer>
Same contract · explicit working methods · stronger proof
<div>
<span>RUN B</span><strong
><span data-language-content="en">Good prompt + skills</span><span
data-language-content="pt"
hidden>Bom prompt + skills</span
></strong
>
</div><CopyPrompt target="prompt-skills" />
</header><pre><code id="prompt-skills"><span data-language-content="en">{handsOn.en.skills}</span><span data-language-content="pt" hidden>{handsOn.pt.skills}</span></code></pre><footer
>
<span data-language-content="en"
>Same contract · explicit working methods · stronger proof</span
><span data-language-content="pt" hidden
>Mesmo contrato · métodos explícitos · prova mais forte</span
>
</footer>
</article>
</div><div class="comparison-strip">
<span
><span data-language-content="en">COMPARE THE RUNS</span><span
data-language-content="pt"
hidden>COMPARE AS EXECUÇÕES</span
></span
><div>
<b>01</b>
<span data-language-content="en">Files changed</span><span
data-language-content="pt"
hidden>Arquivos alterados</span
>
</div><div>
<b>02</b>
<span data-language-content="en">New dependencies</span><span
data-language-content="pt"
hidden>Novas dependências</span
>
</div><div>
<b>03</b>
<span data-language-content="en">Checks actually run</span><span
data-language-content="pt"
hidden>Checks executados</span
>
</div><div>
<b>04</b>
<span data-language-content="en">Evidence returned</span><span
data-language-content="pt"
hidden>Evidências retornadas</span
>
</div>
</div><p class="copy-status" id="copy-status" role="status" aria-live="polite"></p>
</section>
@@ -1017,21 +1281,90 @@ const base = import.meta.env.BASE_URL;
<section class="verification" id="verification">
<div class="section-label">
<span>Verification</span><span>run each gate separately</span>
</div><div class="verify-intro">
<div>
<p class="eyebrow">Checks become evidence</p><h2>
Three layers.<br />Run each one alone.
</h2>
</div><p>
Run a gate on its own line, print its exit code, attach the output. The result is the
deliverable.
</p>
</div><div class="verify-layers">
<article>
<span>01 · STATIC</span><h3>Lint and types</h3><p>
Format, lint, type-check. Fast and scoped to one file.
</p><code>pnpm lint; echo "lint=$?"</code>
Format, lint, type-check. Fast and scoped to one file. Run on every save.
</p><code>pnpm lint; echo "lint=$?" pnpm typecheck; echo "typecheck=$?"</code>
</article><article>
<span>02 · BEHAVIOR</span><h3>Unit and contract</h3><p>
Tests that repeat. Run before claiming done.
</p><code>pnpm test; echo "test=$?"</code>
</p><code>pnpm test; echo "test=$?" cd services/api && go test ./...</code>
</article><article>
<span>03 · INTEGRATION</span><h3>Real UI and API</h3><p>
Drive the actual UI, API, or browser.
</p><code>pnpm check:ui; echo "ui=$?"</code>
Drive the actual UI, API, or browser. Slower and flakier only this catches mobile
overflow and a missing 404.
</p><code>pnpm check:ui; echo "ui=$?" TURBO_FORCE=true pnpm e2e</code>
</article>
</div><div class="verify-antipatterns">
<span>FOUR WAYS A GREEN REPORT IS FALSE</span><div class="ap-grid">
<article>
<b>1</b><div>
<strong>Pipe a gate</strong><p>
tail, grep, or head hide the real exit code a pipeline returns the last command's
status.
</p>
</div>
</article><article>
<b>2</b><div>
<strong>Swallow a rejection</strong><p>
A silent <code>{'.catch(() => {})'}</code> hides a panic, an upstream limit, or a partial
failure.
</p>
</div>
</article><article>
<b>3</b><div>
<strong>Trust the cache</strong><p>
Turbo caches results. A gate that "passes" may not have run use <code
>TURBO_FORCE=true</code
>.
</p>
</div>
</article><article>
<b>4</b><div>
<strong>Skip the third layer</strong><p>
Lint and unit can both be green while the page breaks on mobile and the API never
returns 404.
</p>
</div>
</article>
</div>
</div><article class="verify-cta">
<span>RUN IT YOURSELF · two labs, under 10 minutes each</span><div class="verify-cta-grid">
<a href={`${base}hands-on/starter/`} class="verify-card"
><strong>Path A · verification lab</strong><p>
Fill the four-row comparison strip on the starter. Run A naively, Run B with <code
>$gate-discipline</code
> and <code>$webapp-testing</code>.
</p><small>Open the starter </small><small class="verify-card-source"
>Clone <span>git.marcospaulo.dev.br/.../src/branch/pages/hands-on/starter</span
></small
></a
><a href={`${base}hands-on/rules/`} class="verify-card"
><strong>Path B · rules lab</strong><p>
Toggle every rule off, run the prompt. Toggle every rule on, run it again. Compare
diff size, gate invocations, and the names of checks the agent names back.
</p><small
><span data-language-content="en">Open the rules lab </span><span
data-language-content="pt"
hidden>Open the rules lab </span
></small
><small class="verify-card-source"
>Clone <span>git.marcospaulo.dev.br/.../src/branch/pages/hands-on/rules</span
></small
></a
>
</div>
</article>
</section>
<section class="sources">
<div class="section-label">