Compare commits
16 Commits
138d7c5e4e
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ef5e8e8b42 | |||
| 25ef5af63e | |||
| be271f65c1 | |||
| 2ecfdf1d67 | |||
| 6b4f2e6bd0 | |||
| dc6cb5a0a3 | |||
| 9015e7bd1d | |||
| d88d8b89eb | |||
| b119b92948 | |||
| 580293867d | |||
| 9cb1e0d242 | |||
| 12c32d2fd7 | |||
| 5c76a13b4d | |||
| 91e8d380d0 | |||
| 0a12e9cbcc | |||
| e3ce8abe89 |
@@ -1,45 +1,47 @@
|
||||
# Context: architecture, current and target
|
||||
# Context: architecture
|
||||
|
||||
## Current (no build step)
|
||||
## Current (Astro, static output)
|
||||
|
||||
Ten hand-written HTML pages, each linking its own CSS and one ES module:
|
||||
Ten routes, one `src/pages/` entry each, built to `dist/`:
|
||||
|
||||
| Route | Page | Script | Stylesheets |
|
||||
| -------------------- | -------------------------- | ---------------------- | --------------------------------------------- |
|
||||
| `/` | `index.html` | — | `chapters.css`, `landing.css` |
|
||||
| `/full-guide/` | `full-guide/index.html` | `app.js` (50 KB) | `styles.css`, `responsive.css`, `audit.css` |
|
||||
| `/summary/` | `summary/index.html` | — | `chapters.css` |
|
||||
| `/models/` | `models/index.html` | — | `chapters.css` |
|
||||
| `/agents/` | `agents/index.html` | — | `chapters.css` |
|
||||
| `/skills/` | `skills/index.html` | `skills/app.js` | `skills/styles.css` |
|
||||
| `/rules/` | `rules/index.html` | `rules/app.js` | `rules/styles.css` |
|
||||
| `/skills-review/` | `skills-review/index.html` | `skills-review/app.js` | `skills-review/styles.css`, `change-lens.css` |
|
||||
| `/hands-on/starter/` | lab fixture | own | own |
|
||||
| `/hands-on/rules/` | lab fixture | own | own |
|
||||
| Route | Page | Islands |
|
||||
| -------------------- | ------------------------------- | ----------------------------------------------- |
|
||||
| `/` | `src/pages/index.astro` | — |
|
||||
| `/full-guide/` | `src/pages/full-guide.astro` | `GuideSelector`, `LanguageToggle`, `CopyPrompt` |
|
||||
| `/summary/` | `src/pages/summary.astro` | — |
|
||||
| `/models/` | `src/pages/models.astro` | — |
|
||||
| `/agents/` | `src/pages/agents.astro` | — |
|
||||
| `/skills/` | `src/pages/skills.astro` | `SkillPackageExplorer` |
|
||||
| `/rules/` | `src/pages/rules.astro` | `RulesInteractive` |
|
||||
| `/skills-review/` | `src/pages/skills-review.astro` | `legacy/skills-review/app.js` |
|
||||
| `/hands-on/starter/` | `public/` lab fixture | own |
|
||||
| `/hands-on/rules/` | `public/` lab fixture | own |
|
||||
|
||||
Weight is concentrated: `app.js` 50 KB, `responsive.css` 30 KB,
|
||||
`skills-review/catalog.js` 27 KB, `skills-review/submitted-catalog.js` 18 KB.
|
||||
## What is still unmigrated
|
||||
|
||||
### What each big file actually is
|
||||
`legacy/` holds the parts the migration did not componentize. They are not dead
|
||||
files — the pages listed above import them, and the build fails without them.
|
||||
|
||||
- **`app.js`** — not really application code. It is a **bilingual content
|
||||
database** (`phases`, `handsOnPrompts`, `modelGuide`, `skillSources`,
|
||||
`skillInstallPrompts`, each keyed `{en, pt}`) plus ~12 small `render*`
|
||||
functions that swap `innerHTML` on tab clicks. ~50 `en:` keys. The content
|
||||
should become data; only the tab behaviour is interactive.
|
||||
- **`responsive.css`** — a 30 KB append-only layer of overrides bolted on top of
|
||||
`styles.css`. Expect large parts to be dead once layout moves into components.
|
||||
Do not port it verbatim.
|
||||
- **`skills-review/catalog.js`** — the real data model of the review desk: one
|
||||
entry per submitted skill with `id`, `author`, `title`, `status`, `focus`,
|
||||
`wins[]`, `improve[]`, `extras`, `improved` (full markdown). 24 entries across
|
||||
`catalog.js` + `submitted-catalog.js`. This is already a content collection in
|
||||
all but name.
|
||||
- **`skills-review/files.js` / `submitted-files.js`** — generated file
|
||||
manifests.
|
||||
- **`vote.js`** — the vote widget island; talks to `vote-service/`.
|
||||
- **`legacy/styles/guide.css`** (was `styles.css`) — the editorial visual
|
||||
system, imported by `full-guide.astro`.
|
||||
- **`legacy/styles/audit.css`** (was `full-guide/audit.css`) — responsive audit
|
||||
overrides, imported by `full-guide.astro`.
|
||||
- **`legacy/styles/chapters.css`** — imported by `ChapterLayout.astro`.
|
||||
- **`legacy/styles/skills.css`**, **`skills-review.css`**, **`change-lens.css`**
|
||||
— imported by their respective pages.
|
||||
- **`legacy/skills-review/`** — `app.js` and the module graph under it
|
||||
(`catalog.js`, `submitted-catalog.js`, `files.js`, `submitted-files.js`,
|
||||
`vote.js`). `catalog.js` + `submitted-catalog.js` are the review desk's real
|
||||
data model, 24 entries; they are a content collection in all but name.
|
||||
|
||||
## Target (Astro)
|
||||
These sit outside `src/` deliberately: `check-tokens.mjs` sweeps `src`, and
|
||||
these files are full of raw hex and unnamed breakpoints. Moving one into `src/`
|
||||
means migrating it to tokens in the same change, not adding an exclusion.
|
||||
|
||||
`responsive.css`, `landing.css`, `app.js`, `rules/app.js`, `rules/styles.css`,
|
||||
and `skills/app.js` were deleted at cutover: their content lives in components.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
src/
|
||||
@@ -52,15 +54,15 @@ public/
|
||||
hands-on/ lab fixtures copied verbatim, never processed
|
||||
```
|
||||
|
||||
### Non-negotiables for the target
|
||||
### Non-negotiables
|
||||
|
||||
- **URLs do not change.** `/full-guide/`, `/skills-review/`,
|
||||
`/hands-on/starter/` and the rest must resolve exactly as they do now,
|
||||
trailing slash included. Existing links (including `docs/`, SilverBullet, and
|
||||
shared URLs with `?author=…&skill=…&view=…` query params) must keep working.
|
||||
- **Zero JS by default.** Seven of the ten pages ship no JavaScript today. They
|
||||
must still ship none. Islands are opt-in, per component, and justified.
|
||||
- **`hands-on/` stays vanilla.** It goes in `public/` untouched. It is a lab
|
||||
- **Zero JS by default.** Seven of the ten pages ship no JavaScript. They must
|
||||
still ship none. Islands are opt-in, per component, and justified.
|
||||
- **`hands-on/` stays vanilla.** It lives in `public/` untouched. It is a lab
|
||||
fixture, not a component.
|
||||
- **No external runtime requests.** `audit-ui.mjs` enforces this and it is part
|
||||
of the site's thesis. Self-host anything you add.
|
||||
@@ -70,7 +72,12 @@ public/
|
||||
|
||||
## Companion service
|
||||
|
||||
`vote-service/` is a Go API on its own Kubernetes deploy cycle, reached by the
|
||||
review desk over `window.SKILLS_REVIEW_VOTE_API`. The refactor does not touch
|
||||
it. Keep the global, or replace it with a build-time `PUBLIC_VOTE_API` env var —
|
||||
but if you do, update `vote-service/README.md` in the same change.
|
||||
The vote API is a Go service on its own Kubernetes deploy cycle, reached by the
|
||||
review desk over `window.SKILLS_REVIEW_VOTE_API`. Its source left this
|
||||
repository on 2026-09-06; the deployed service is unchanged, and the review desk
|
||||
still calls it. Keep the global, or replace it with a build-time
|
||||
`PUBLIC_VOTE_API` env var — but if you do, update the service's own README in
|
||||
the same change.
|
||||
|
||||
Its one-vote-per-IP assertion left `verify.mjs` with it. See
|
||||
[`assertion-removals.md`](assertion-removals.md).
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Assertion removal ledger
|
||||
|
||||
`scripts/verify.mjs` may only lose an assertion by adding an entry here. The
|
||||
gate counts the `## ` headings in this file and allows exactly that many
|
||||
removals below the recorded floor — so a reduction is impossible without a
|
||||
written reason landing in the same commit, as a visible diff.
|
||||
|
||||
Adding an entry is not a formality. An assertion pins a real contract; removing
|
||||
one means that contract is now unverified. Say where it moved, or say plainly
|
||||
that nothing checks it any more.
|
||||
|
||||
## vote-service one-vote-per-IP contract
|
||||
|
||||
**Removed:** 2026-09-06, when `vote-service/` was taken out of this repository.
|
||||
|
||||
**What it asserted:** that `vote-service/main.go` contained both
|
||||
`X-Forwarded-For` and `one active vote per skill` — the review desk's only
|
||||
anti-abuse control, one vote per visitor enforced server-side by source IP.
|
||||
|
||||
**Why it went:** there is no file left to read. The check was a substring match
|
||||
against source that now lives elsewhere.
|
||||
|
||||
**Where it must be re-asserted:** in whichever repository holds the service. The
|
||||
deployed service still enforces the contract; nothing in this repository proves
|
||||
it. If `vote-service/` ever comes back here, restore the assertion and delete
|
||||
this entry.
|
||||
@@ -19,7 +19,7 @@ Only these need interactivity. Anything else claiming island status is wrong:
|
||||
| --------------------------------- | ---------------------------------- | ---------------- |
|
||||
| Guide phase/tab switchers | click-driven panel swap | `client:visible` |
|
||||
| Review desk catalog + file viewer | search, filter, fetch source files | `client:load` |
|
||||
| Vote widget | talks to `vote-service/` | `client:visible` |
|
||||
| Vote widget | talks to the vote API | `client:visible` |
|
||||
| Language toggle | swaps EN/PT across the page | `client:idle` |
|
||||
|
||||
## Structure
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env node
|
||||
// Compare the *computed* styles of a legacy page against its Astro
|
||||
// replacement, at several viewport widths.
|
||||
//
|
||||
// node .agents/scripts/computed-style-diff.mjs full-guide
|
||||
// node .agents/scripts/computed-style-diff.mjs full-guide --widths 560,880,1050
|
||||
//
|
||||
// Why this exists: a ported media query can sit in the built stylesheet,
|
||||
// match the viewport, and still do nothing. Astro scopes a component's rules
|
||||
// as `.tree-node[data-astro-cid-lsutp3lb]` (specificity 0,2,0); a rule ported
|
||||
// verbatim as `.tree-node` (0,1,0) loses to it and never applies. Task 15e
|
||||
// attempt 4 shipped exactly that: `@media (max-width: 1050px) .tree-node
|
||||
// { width: 145px }` was present in dist and the node stayed 180px wide.
|
||||
//
|
||||
// Checking that the breakpoint *appears* in the built CSS cannot catch this.
|
||||
// Only asking the browser what it actually computed can.
|
||||
import { spawn } from 'node:child_process';
|
||||
import { cpSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { createServer } from 'node:net';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const route = process.argv[2];
|
||||
if (!route) {
|
||||
console.error('usage: computed-style-diff.mjs <route> [--widths a,b,c]');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const widthsArg = process.argv.indexOf('--widths');
|
||||
const widths =
|
||||
widthsArg === -1
|
||||
? [520, 560, 600, 620, 720, 800, 880, 1050, 1100, 1600]
|
||||
: process.argv[widthsArg + 1].split(',').map(Number);
|
||||
|
||||
// The selectors worth checking are the ones the responsive layer moves at a
|
||||
// breakpoint, so read them out of the legacy stylesheet's @media blocks only.
|
||||
// Taking every class in the file buries the signal under generic ones like
|
||||
// `.active`, whose state the islands own anyway.
|
||||
const responsive = readFileSync(new URL('../../responsive.css', import.meta.url), 'utf8');
|
||||
const mediaBlocks = [];
|
||||
for (const match of responsive.matchAll(/@media[^{]*\{/g)) {
|
||||
let depth = 0;
|
||||
for (let i = match.index; i < responsive.length; i += 1) {
|
||||
if (responsive[i] === '{') depth += 1;
|
||||
else if (responsive[i] === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
mediaBlocks.push(responsive.slice(match.index + match[0].length, i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const selectors = [...new Set(mediaBlocks.join('\n').match(/\.[a-z][a-z0-9-]*/g) || [])].sort();
|
||||
|
||||
// Properties a responsive rule actually moves. Comparing every property would
|
||||
// drown the signal in font stacks and inherited colour.
|
||||
const PROPERTIES = [
|
||||
'display',
|
||||
'grid-template-columns',
|
||||
'grid-template-rows',
|
||||
'flex-direction',
|
||||
'width',
|
||||
'height',
|
||||
'max-width',
|
||||
'padding',
|
||||
'margin',
|
||||
'gap',
|
||||
'font-size',
|
||||
'position',
|
||||
'inset',
|
||||
'overflow',
|
||||
];
|
||||
|
||||
// The legacy pages were deleted at cutover; run this from a pre-cutover
|
||||
// worktree, or the legacy side will 404.
|
||||
const legacyPath = route === 'index' ? 'index.html' : `${route}/index.html`;
|
||||
const astroPath = route === 'index' ? '' : `${route}/`;
|
||||
|
||||
const staging = mkdtempSync(join(tmpdir(), 'af-csd-'));
|
||||
cpSync('dist', join(staging, 'ai-for-dummies'), { recursive: true });
|
||||
|
||||
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('.', legacyPort), serve(staging, astroPort)];
|
||||
const stop = () => {
|
||||
servers.forEach((s) => s.kill());
|
||||
rmSync(staging, { recursive: true, force: true });
|
||||
};
|
||||
|
||||
// Every element matching each selector, so a rule that applies to the first
|
||||
// node and not the rest cannot pass.
|
||||
const collect = ([selectors, properties]) => {
|
||||
const out = {};
|
||||
for (const selector of selectors) {
|
||||
const nodes = [...document.querySelectorAll(selector)];
|
||||
out[selector] = nodes.map((node) => {
|
||||
const style = getComputedStyle(node);
|
||||
return properties
|
||||
.map((property) => `${property}:${style.getPropertyValue(property)}`)
|
||||
.join(';');
|
||||
});
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
let failures = 0;
|
||||
try {
|
||||
const browser = await chromium.launch();
|
||||
const read = async (url, width) => {
|
||||
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
||||
const response = await page.goto(url, { waitUntil: 'load' });
|
||||
if (!response || !response.ok()) {
|
||||
throw new Error(`${url} returned ${response ? response.status() : 'no response'}`);
|
||||
}
|
||||
await page.waitForTimeout(1500);
|
||||
const styles = await page.evaluate(collect, [selectors, PROPERTIES]);
|
||||
await page.close();
|
||||
return styles;
|
||||
};
|
||||
|
||||
for (const width of widths) {
|
||||
const legacy = await read(`http://localhost:${legacyPort}/${legacyPath}`, width);
|
||||
const astro = await read(`http://localhost:${astroPort}/ai-for-dummies/${astroPath}`, width);
|
||||
|
||||
for (const selector of selectors) {
|
||||
const before = legacy[selector];
|
||||
const after = astro[selector];
|
||||
if (before.length === 0 && after.length === 0) continue;
|
||||
if (before.length !== after.length) {
|
||||
console.log(
|
||||
`${width}px ${selector} legacy ${before.length} nodes, astro ${after.length}`,
|
||||
);
|
||||
failures += 1;
|
||||
continue;
|
||||
}
|
||||
let reported = 0;
|
||||
before.forEach((expected, index) => {
|
||||
if (expected === after[index]) return;
|
||||
failures += 1;
|
||||
// Three examples is enough to identify a rule that did not apply.
|
||||
reported += 1;
|
||||
if (reported > 3) return;
|
||||
const differing = expected
|
||||
.split(';')
|
||||
.filter((pair, i) => pair !== after[index].split(';')[i]);
|
||||
const got = after[index].split(';').filter((pair, i) => pair !== expected.split(';')[i]);
|
||||
console.log(`${width}px ${selector}[${index}]`);
|
||||
console.log(` legacy ${differing.join(' ')}`);
|
||||
console.log(` astro ${got.join(' ')}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
console.log(failures === 0 ? 'computed styles match' : `${failures} computed-style differences`);
|
||||
process.exitCode = failures === 0 ? 0 : 1;
|
||||
} finally {
|
||||
stop();
|
||||
}
|
||||
@@ -43,7 +43,20 @@ step "types"
|
||||
pnpm exec astro check
|
||||
|
||||
step "build"
|
||||
pnpm run build
|
||||
# `astro build` exits 0 even when vite fails to resolve an asset: the cutover
|
||||
# left a stale `@import` in a moved stylesheet and every gate stayed green for
|
||||
# it. Treat a logged error as a failed build.
|
||||
build_log=$(mktemp)
|
||||
if ! pnpm run build 2>&1 | tee "$build_log"; then
|
||||
rm -f "$build_log"
|
||||
exit 1
|
||||
fi
|
||||
if grep -q '\[ERROR\]' "$build_log"; then
|
||||
echo "gate: astro build logged an error and still exited 0. See above." >&2
|
||||
rm -f "$build_log"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$build_log"
|
||||
|
||||
step "content contracts"
|
||||
pnpm run verify
|
||||
@@ -51,11 +64,19 @@ pnpm run verify
|
||||
# The assertion count is the thing agents are most tempted to "fix" downward.
|
||||
# Compare against origin/main and refuse a silent reduction.
|
||||
step "assertion coverage"
|
||||
# Task 19 restored the 42 legacy facts and added ten output snapshots: 84 is the
|
||||
# floor, in addition to whatever origin/main currently requires.
|
||||
#
|
||||
# A removal is allowed only by writing a reason into the ledger. The gate counts
|
||||
# its entries and lowers the bar by exactly that many, so the bar cannot move
|
||||
# without a visible diff explaining why. Deleting an entry to buy headroom is
|
||||
# the same offence as deleting the assertion was.
|
||||
ledger=.agents/context/assertion-removals.md
|
||||
current=$(grep -c 'throw new Error' scripts/verify.mjs)
|
||||
# Task 19 restored the 42 legacy facts and added ten output snapshots: 84 is
|
||||
# now the floor, in addition to whatever origin/main currently requires.
|
||||
allowed=$(grep -c '^## ' "$ledger" 2>/dev/null || echo 0)
|
||||
baseline=$(git show origin/main:scripts/verify.mjs 2>/dev/null | grep -c 'throw new Error' || echo 0)
|
||||
if [ "$baseline" -lt 84 ]; then baseline=84; fi
|
||||
baseline=$((baseline - allowed))
|
||||
if [ "$current" -lt "$baseline" ]; then
|
||||
echo "gate: verify.mjs coverage fell from $baseline to $current assertions." >&2
|
||||
echo " Only verification-engineer may reduce it, with a reason per removal." >&2
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the site and publish it to the `pages` branch.
|
||||
#
|
||||
# .agents/scripts/publish-pages.sh # publish
|
||||
# .agents/scripts/publish-pages.sh --dry-run # build and report, push nothing
|
||||
# .agents/scripts/publish-pages.sh --pending X # main is *about* to become X
|
||||
#
|
||||
# `--pending` exists for the pre-push hook. Git has no post-push hook, so the
|
||||
# hook necessarily runs before main lands on the remote and the usual "HEAD must
|
||||
# equal origin/main" check cannot hold yet. The caller asserts the SHA the push
|
||||
# will create, and the hook only asserts it after confirming the push is a
|
||||
# fast-forward.
|
||||
#
|
||||
# `pages` is what the Gitea Pages Server actually serves. Publishing overwrites
|
||||
# the live site. There is no staging environment between here and visitors.
|
||||
#
|
||||
# This never checks `pages` out. It writes a tree straight from `dist/` with
|
||||
# plumbing (`write-tree` + `commit-tree`), so your working tree is untouched and
|
||||
# a failure halfway through leaves nothing behind. The commit is parented on the
|
||||
# current `pages`, so the branch keeps its history and rollback is one push.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
dry_run=0
|
||||
pending=''
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--dry-run) dry_run=1 ;;
|
||||
--pending)
|
||||
shift
|
||||
pending="${1:-}"
|
||||
;;
|
||||
*)
|
||||
echo "publish-pages: unknown argument '$1'" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
fail() {
|
||||
echo "publish-pages: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Publishing a build made from uncommitted work means the live site shows
|
||||
# something no commit describes, and nobody can reproduce it later.
|
||||
[ -z "$(git status --porcelain)" ] || fail 'working tree is dirty; commit or stash first'
|
||||
|
||||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
[ "$branch" = 'main' ] || fail "publishing from '$branch'; only main is publishable"
|
||||
|
||||
git fetch --quiet origin pages
|
||||
head=$(git rev-parse HEAD)
|
||||
if [ -n "$pending" ]; then
|
||||
[ "$head" = "$(git rev-parse "$pending")" ] ||
|
||||
fail "HEAD is $head but the pending push is $pending"
|
||||
else
|
||||
git fetch --quiet origin main
|
||||
[ "$head" = "$(git rev-parse origin/main)" ] ||
|
||||
fail 'HEAD is not origin/main; push main first so the site matches a pushed commit'
|
||||
fi
|
||||
|
||||
previous=$(git rev-parse origin/pages)
|
||||
|
||||
echo "publish-pages: building $head"
|
||||
# `astro build` exits 0 even when vite fails to resolve an asset, so the exit
|
||||
# code alone is not enough to know the build is whole. The gate greps for this
|
||||
# too; repeat it here because this script is also run by hand.
|
||||
build_log=$(mktemp)
|
||||
trap 'rm -f "$build_log"' EXIT
|
||||
pnpm run build >"$build_log" 2>&1 || {
|
||||
cat "$build_log" >&2
|
||||
fail 'astro build failed'
|
||||
}
|
||||
if grep -q '\[ERROR\]' "$build_log"; then
|
||||
cat "$build_log" >&2
|
||||
fail 'astro build logged an error and still exited 0; refusing to publish'
|
||||
fi
|
||||
|
||||
# A build can succeed and still emit a stub -- that is exactly how this site
|
||||
# would go down. Check the routes exist before overwriting anything live.
|
||||
for route in index full-guide/index summary/index models/index agents/index \
|
||||
skills/index rules/index skills-review/index \
|
||||
hands-on/starter/index hands-on/rules/index; do
|
||||
[ -s "dist/$route.html" ] || fail "dist/$route.html missing or empty; refusing to publish"
|
||||
done
|
||||
|
||||
# GIT_INDEX_FILE must name a path that does not exist yet: git reads an existing
|
||||
# empty file as a truncated index and dies with "index file smaller than
|
||||
# expected". mktemp -d gives a private directory to put that path in.
|
||||
index_dir=$(mktemp -d)
|
||||
index="$index_dir/index"
|
||||
trap 'rm -rf "$index_dir"; rm -f "$build_log"' EXIT
|
||||
# `--force` because the repository .gitignore lists `dist`; here `dist` *is* the
|
||||
# work tree, so those rules would otherwise exclude everything we mean to ship.
|
||||
GIT_INDEX_FILE="$index" git --work-tree=dist add --all --force .
|
||||
tree=$(GIT_INDEX_FILE="$index" git write-tree)
|
||||
|
||||
if [ "$tree" = "$(git rev-parse "$previous^{tree}")" ]; then
|
||||
echo "publish-pages: dist is identical to the published tree; nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
subject="chore: publish $(git rev-parse --short "$head")"
|
||||
commit=$(git commit-tree "$tree" -p "$previous" -m "$subject
|
||||
|
||||
Built from main $head
|
||||
$(git log -1 --format=%s "$head")")
|
||||
|
||||
if [ "$dry_run" -eq 1 ]; then
|
||||
echo "publish-pages: would push $commit to pages (previous $previous)"
|
||||
echo "publish-pages: dry run, nothing pushed"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "publish-pages: rollback point is $previous"
|
||||
echo " git push --force origin $previous:refs/heads/pages"
|
||||
|
||||
# AF_PUBLISHING stops the pre-push hook recursing into this script.
|
||||
AF_PUBLISHING=1 git push --force origin "$commit:refs/heads/pages"
|
||||
echo "publish-pages: published $commit"
|
||||
@@ -17,6 +17,10 @@
|
||||
// bilingual pair, so the two sides line up.
|
||||
//
|
||||
// Requires playwright (devDependency) and two static servers; it starts both.
|
||||
//
|
||||
// The legacy pages were deleted at cutover, so this needs a pre-cutover tree:
|
||||
// git worktree add /tmp/vanilla <pre-cutover-sha>
|
||||
// and run from there, or run it from a checkout that still has them.
|
||||
import { spawn } from 'node:child_process';
|
||||
import { cpSync, mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
@@ -12,8 +12,12 @@ description:
|
||||
|
||||
The snapshot is the only objective evidence that no content was lost.
|
||||
|
||||
The vanilla site was deleted at cutover. To compare against it, check the
|
||||
pre-cutover tree out into a scratch worktree first:
|
||||
|
||||
```bash
|
||||
pnpm run serve & # vanilla site on :4173
|
||||
git worktree add /tmp/vanilla <pre-cutover-sha>
|
||||
(cd /tmp/vanilla && python3 -m http.server 4173) &
|
||||
node .agents/scripts/snapshot-route.mjs http://localhost:4173/models/ \
|
||||
> .agents/snapshots/models.txt
|
||||
```
|
||||
|
||||
@@ -21,8 +21,10 @@ a bug.
|
||||
```bash
|
||||
python3 - <<'PY'
|
||||
import re
|
||||
files=['styles.css','chapters.css','landing.css','rules/styles.css','skills/styles.css',
|
||||
'skills-review/styles.css','hands-on/starter/styles.css','hands-on/rules/styles.css']
|
||||
files=['legacy/styles/guide.css','legacy/styles/chapters.css','legacy/styles/skills.css',
|
||||
'legacy/styles/skills-review.css','legacy/styles/change-lens.css',
|
||||
'legacy/styles/audit.css','public/hands-on/starter/styles.css',
|
||||
'public/hands-on/rules/styles.css']
|
||||
seen={}
|
||||
for f in files:
|
||||
for m in re.finditer(r'--([a-z-]+):\s*([^;}]+)', open(f).read()):
|
||||
|
||||
@@ -33,8 +33,9 @@ with sync_playwright() as p:
|
||||
browser.close()
|
||||
```
|
||||
|
||||
Run once against the vanilla site (`pnpm run serve`), once against
|
||||
`pnpm run preview`. Keep both sets.
|
||||
Run once against `pnpm run preview`. To compare against the vanilla site, serve
|
||||
a pre-cutover worktree on :4173 first — those files are no longer on `main`.
|
||||
Keep both sets.
|
||||
|
||||
## Compare
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 377 KiB |
|
Before Width: | Height: | Size: 444 KiB |
|
Before Width: | Height: | Size: 341 KiB |
|
Before Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 465 KiB |
|
Before Width: | Height: | Size: 485 KiB |
|
Before Width: | Height: | Size: 438 KiB |
|
Before Width: | Height: | Size: 458 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 377 KiB |
|
Before Width: | Height: | Size: 444 KiB |
|
Before Width: | Height: | Size: 341 KiB |
|
Before Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 465 KiB |
|
Before Width: | Height: | Size: 485 KiB |
|
Before Width: | Height: | Size: 438 KiB |
|
Before Width: | Height: | Size: 458 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 104 KiB |
@@ -288,5 +288,16 @@
|
||||
"1960px",
|
||||
"2200px",
|
||||
"2880px"
|
||||
],
|
||||
"breakpoints": [
|
||||
"520px",
|
||||
"560px",
|
||||
"600px",
|
||||
"800px",
|
||||
"880px",
|
||||
"1050px",
|
||||
"1100px",
|
||||
"1600px",
|
||||
"2200px"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
[
|
||||
"01 frota",
|
||||
"02 worktrees",
|
||||
"03 modelos",
|
||||
"04 skills",
|
||||
"05 criar",
|
||||
"06 kit de campo",
|
||||
"07 prática",
|
||||
"ENGENHARIA DE IA <i></i> 01 / 2026",
|
||||
"Uma apresentação para quem entrega software",
|
||||
"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.",
|
||||
"NOTA DE CAMPO / 001",
|
||||
"Entregue o<br /><em>sistema.</em>",
|
||||
"Skills · agentes · worktrees · evidências",
|
||||
"modelo forte<br />para ambiguidade",
|
||||
"workers delimitados<br />em paralelo",
|
||||
"iterações<br />com evidências",
|
||||
"Leia isto como um mapa de rota, não como uma receita de prompt.",
|
||||
"REGRA ZERO",
|
||||
"Modelo forte para ambiguidade.<br />Modelo leve para trabalho delimitado.",
|
||||
"Uma pequena frota",
|
||||
"coordenação antes do paralelismo",
|
||||
"ORQUESTRADOR",
|
||||
"Decide o que<br />precisa acontecer.",
|
||||
"Componentes e estados visuais",
|
||||
"Casos de aceitação",
|
||||
"Guia e exemplos",
|
||||
"O orquestrador preserva a intenção, escreve pequenos contratos e reúne resultados verificáveis. Ele não precisa digitar cada linha.",
|
||||
"Por que a fronteira importa",
|
||||
"uma tarefa vaga / três falhas previsíveis",
|
||||
"Sopa de contexto",
|
||||
"Cada worker lê tudo. Ninguém sabe quais fatos são essenciais.",
|
||||
"Colisão de branches",
|
||||
"Dois agentes usam o mesmo checkout. O caminho mais rápido vira resolução de conflitos.",
|
||||
"Desvio confiante",
|
||||
"O diff parece ótimo, mas ninguém verifica se resolveu o problema original.",
|
||||
"O ciclo de subagentes",
|
||||
"Clique em uma fase.<br /><em>Veja a passagem.</em>",
|
||||
"Delegar é mover uma tarefa delimitada para um contexto menor — não abrir mão da responsabilidade.",
|
||||
"O que atravessa contextos",
|
||||
"brief → diff → evidência",
|
||||
"Pacote",
|
||||
"Contém",
|
||||
"Por que importa",
|
||||
"Git worktrees",
|
||||
"Uma branch<br />por <em>mão.</em>",
|
||||
"Um worktree é outro diretório ligado ao mesmo repositório. Cada agente recebe seu próprio checkout e índice; o histórico continua compartilhado.",
|
||||
"Selecione um nó para inspecionar checkout, responsável e próxima ação.",
|
||||
"topologia do repositório",
|
||||
"<i></i> 4 checkouts",
|
||||
"RAIZ",
|
||||
"AGENTE DE UI",
|
||||
"AGENTE DE TESTES",
|
||||
"AGENTE DE DOCS",
|
||||
"● limpo",
|
||||
"3 arquivos · trabalhando",
|
||||
"8 verificações · pronto",
|
||||
"2 páginas · revisão",
|
||||
"Roteamento de modelos",
|
||||
"Não pague por<br />raciocínio onde precisa<br />de <em>ritmo.</em>",
|
||||
"Escolha um trabalho para entender por que o perfil do modelo muda.",
|
||||
"Trabalho",
|
||||
"Perfil",
|
||||
"Formato do prompt",
|
||||
"Planejar",
|
||||
"Construir",
|
||||
"Explorar",
|
||||
"Revisar",
|
||||
"Skills",
|
||||
"Escreva do jeito certo<br /><em>uma vez.</em>",
|
||||
"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.",
|
||||
"01 / defina o gatilho",
|
||||
"02 / carregue detalhes sob demanda",
|
||||
"03 / devolva evidências",
|
||||
"PACOTE DE SKILL",
|
||||
"Skills comuns",
|
||||
"escolha o comportamento antes do modelo",
|
||||
"O kit de campo",
|
||||
"Trabalhos diferentes.<br />Instintos <em>diferentes.</em>",
|
||||
"Uma skill muda como o agente aborda o trabalho. Algumas moldam a comunicação. Outras impõem pesquisa, diagnóstico, revisão ou disciplina de conclusão. Selecione uma para inspecionar sua regra operacional.",
|
||||
"SIMPLIFICAR",
|
||||
"código mínimo que funciona",
|
||||
"COMUNICAR",
|
||||
"sinal sem excesso",
|
||||
"CONCLUIR",
|
||||
"gates e evidências",
|
||||
"INVESTIGAR",
|
||||
"fontes primárias primeiro",
|
||||
"DIAGNOSTICAR",
|
||||
"ciclo curto de feedback",
|
||||
"REVISAR",
|
||||
"padrões × especificação",
|
||||
"ECONOMIZAR",
|
||||
"comprima saídas ruidosas",
|
||||
"UM LOADOUT PRÁTICO",
|
||||
"<b>PLANEJAR</b> unlazy <i>→</i> <b>CONSTRUIR</b> ponytail-lite <i>→</i> <b>DIAGNOSTICAR</b> diagnosing-bugs <i>→</i> <b>REPORTAR</b> caveman",
|
||||
"O PAPEL HUMANO",
|
||||
"O agente pode ser autônomo na execução. Intenção, limites e evidências continuam sendo seus.",
|
||||
"COMECE AQUI",
|
||||
"Comece com um agente e uma skill. Adicione paralelismo apenas quando as tarefas forem realmente independentes.",
|
||||
"Continue aprendendo",
|
||||
"12 novas leituras + documentação primária",
|
||||
"Aprofunde com documentação oficial, casos de produção, Medium e fluxos de praticantes. <a href=\"rules/\">Estudo de caso sobre regras e enforcement →</a> <a href=\"docs/references/README.md\">Referências primárias →</a> <a href=\"docs/references/additional-reading.md\">Trilha com 12 leituras →</a>"
|
||||
]
|
||||
@@ -40,11 +40,11 @@ jobs:
|
||||
# back in once it can actually diff. See task 03's report.
|
||||
|
||||
publish:
|
||||
# Until the migration finishes, `dist/` holds only /summary/ and the two
|
||||
# hands-on fixtures, while the live `pages` branch serves ten pages.
|
||||
# Publishing on every push to main would take the site down to a stub, so
|
||||
# this job runs only when a human asks for it. Make it unconditional on
|
||||
# main again at task 20 (cutover), not before.
|
||||
# `dist/` now holds all ten routes, so the stub hazard that forced this to
|
||||
# manual dispatch is gone. It stays manual anyway: the step below is a
|
||||
# force-push over the live `pages` branch, and making it fire on every push
|
||||
# to main means every merge republishes with no human in the loop. Flipping
|
||||
# it to `push` on main is a deliberate decision, not a leftover TODO.
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.publish
|
||||
needs: gate
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -1,4 +1,40 @@
|
||||
# Tier 2: the real gate. Whole project. Budget < 90s.
|
||||
# Takes a cross-worktree lock so parallel agents queue instead of thrashing.
|
||||
|
||||
exec .agents/scripts/gate.sh
|
||||
# The publish step below re-enters git push. Without this, that inner push would
|
||||
# fire this hook again, run the gate again, and publish again, forever.
|
||||
if [ "${AF_PUBLISHING:-0}" = '1' ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
.agents/scripts/gate.sh || exit 1
|
||||
|
||||
# Publishing to `pages` overwrites the live site. It happens here, on a push of
|
||||
# main to origin, and nowhere else.
|
||||
#
|
||||
# Set AF_NO_PUBLISH=1 to push main without republishing:
|
||||
# AF_NO_PUBLISH=1 git push
|
||||
[ "${AF_NO_PUBLISH:-0}" = '1' ] && exit 0
|
||||
|
||||
remote_name=$1
|
||||
[ "$remote_name" = 'origin' ] || exit 0
|
||||
|
||||
# stdin gives one line per ref being pushed:
|
||||
# <local ref> <local sha> <remote ref> <remote sha>
|
||||
zero='0000000000000000000000000000000000000000'
|
||||
while read -r local_ref local_sha remote_ref remote_sha; do
|
||||
[ "$remote_ref" = 'refs/heads/main' ] || continue
|
||||
# A deletion has no build to publish.
|
||||
[ "$local_sha" = "$zero" ] && continue
|
||||
|
||||
# This hook runs before the push lands, so `pages` would go live ahead of
|
||||
# `main` if the push then failed. Publish only when the push cannot be
|
||||
# rejected as a non-fast-forward: the remote tip must already be an ancestor.
|
||||
if [ "$remote_sha" != "$zero" ] && ! git merge-base --is-ancestor "$remote_sha" "$local_sha"; then
|
||||
echo "pre-push: main is not a fast-forward; not publishing." >&2
|
||||
echo " Push main first, then run .agents/scripts/publish-pages.sh" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
.agents/scripts/publish-pages.sh --pending "$local_sha" || exit 1
|
||||
done
|
||||
|
||||
@@ -9,21 +9,9 @@ vote-service
|
||||
pnpm-lock.yaml
|
||||
public/submitted-skills
|
||||
|
||||
# Legacy site sources, slated for deletion at cutover (task 20). These are
|
||||
# Unmigrated legacy sources, kept verbatim under `legacy/`. These are
|
||||
# hand-written files with very long lines; prettier re-wraps them into hundreds
|
||||
# of changed lines the moment any agent stages one. Task 15 touched app.js to
|
||||
# add four lines and produced an 829-line diff. verify.mjs asserts substrings
|
||||
# against several of these, so a reformat is churn at best and a broken
|
||||
# assertion at worst.
|
||||
#
|
||||
# Root-anchored on purpose: a bare `rules` would also swallow .agents/rules/,
|
||||
# whose markdown we do want formatted.
|
||||
/app.js
|
||||
/styles.css
|
||||
/landing.css
|
||||
/chapters.css
|
||||
/responsive.css
|
||||
/skills-review/
|
||||
/rules/
|
||||
/skills/
|
||||
/full-guide/
|
||||
# of changed lines the moment any agent stages one. Task 15 touched the old
|
||||
# app.js to add four lines and produced an 829-line diff. A reformat here is
|
||||
# churn at best.
|
||||
/legacy/
|
||||
|
||||
@@ -7,23 +7,16 @@ public/submitted-skills
|
||||
skill-reviews
|
||||
vote-service
|
||||
|
||||
# Legacy site sources, slated for deletion at cutover (task 20). Same list and
|
||||
# Unmigrated legacy stylesheets, kept verbatim under `legacy/`. Same list and
|
||||
# same reasoning as .prettierignore: these are minified, single-line
|
||||
# stylesheets. stylelint's `declaration-block-single-line-max-declarations`
|
||||
# fires once per rule in them — ~180 errors for `styles.css` alone — so staging
|
||||
# fires once per rule in them — ~180 errors for `guide.css` alone — so staging
|
||||
# one to change a single declaration blocks the commit outright. The rule is
|
||||
# about hand-written source readability and says nothing useful about minified
|
||||
# output that is about to be deleted.
|
||||
# legacy output. Migrating one of these into `src/` means bringing it up to the
|
||||
# design system in the same change, at which point it gets linted like any
|
||||
# other source file.
|
||||
#
|
||||
# `public/fonts/fonts.css` is deliberately NOT here: it is new, hand-written,
|
||||
# and must stay linted.
|
||||
#
|
||||
# Root-anchored on purpose: a bare `rules` would also swallow .agents/rules/.
|
||||
/styles.css
|
||||
/landing.css
|
||||
/chapters.css
|
||||
/responsive.css
|
||||
/skills-review/
|
||||
/rules/
|
||||
/skills/
|
||||
/full-guide/
|
||||
/legacy/
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
"hands-on/**",
|
||||
"public/hands-on/**",
|
||||
"submitted-skills/**",
|
||||
"skill-reviews/**",
|
||||
"vote-service/**"
|
||||
"skill-reviews/**"
|
||||
],
|
||||
"rules": {
|
||||
"custom-property-pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
|
||||
|
||||
@@ -12,48 +12,54 @@ rules, and verification.** It is published as a static site on a self-hosted
|
||||
Gitea Pages Server, and it doubles as its own teaching artifact: the hands-on
|
||||
labs are dependency-free HTML/CSS/JS that workshop attendees point an agent at.
|
||||
|
||||
- **Current stack**: hand-written HTML + CSS + ES modules, no build step, no
|
||||
dependencies
|
||||
- **Target stack**: Astro (see
|
||||
[`plans/astro-refactor/`](plans/astro-refactor/README.md)) — migration in
|
||||
progress
|
||||
- **Stack**: Astro, static output, no runtime dependencies. The migration
|
||||
recorded in [`plans/astro-refactor/`](plans/astro-refactor/README.md) is
|
||||
complete; the hand-written pages it replaced are gone. What remains unmigrated
|
||||
is the editorial CSS and the review-desk modules under `legacy/`, still
|
||||
imported by the pages that need them.
|
||||
- **Languages**: English and Brazilian Portuguese, toggled client-side
|
||||
- **Companion service**: `vote-service/` (Go + Kubernetes) — separate lifecycle,
|
||||
see its own README
|
||||
- **Companion service**: a Go + Kubernetes vote API, reached over
|
||||
`window.SKILLS_REVIEW_VOTE_API`. Its source is no longer in this repository
|
||||
|
||||
## Essential commands
|
||||
|
||||
```bash
|
||||
pnpm run verify # content + interaction contracts (scripts/verify.mjs) — the gate
|
||||
pnpm run dev # http://localhost:4321/ai-for-dummies/
|
||||
pnpm run build # writes dist/ — every check below reads it
|
||||
bash .agents/scripts/gate.sh # the full gate: check, build, verify, audit, tokens
|
||||
pnpm run verify # content + interaction contracts (scripts/verify.mjs)
|
||||
node scripts/audit-ui.mjs # responsive / no-external-dependency audit
|
||||
node scripts/build-skill-review.mjs # regenerate skill-reviews/improved/ from src/content/reviews/
|
||||
pnpm run serve # python3 -m http.server 4173
|
||||
```
|
||||
|
||||
`pnpm run verify` is not a formality. It is a set of ~42 string-token assertions
|
||||
that pin the site's real content and interactions. **A refactor that "passes" by
|
||||
deleting assertions has failed.** See
|
||||
`pnpm run verify` is not a formality. It is a set of 84 string-token assertions
|
||||
that pin the site's real content and interactions, read from the built output.
|
||||
**A refactor that "passes" by deleting assertions has failed.** See
|
||||
[`.agents/context/verification.md`](.agents/context/verification.md).
|
||||
|
||||
## Publishing
|
||||
|
||||
`main` is the source of truth. The `pages` branch is what the Gitea Pages Server
|
||||
actually serves, and its tree must end up identical to `main`'s. The full
|
||||
procedure — including why `merge --ff-only` does _not_ work here — is in
|
||||
[`docs/operations-guide.md`](docs/operations-guide.md).
|
||||
actually serves, and it now carries **build output**, not a copy of `main`'s
|
||||
tree.
|
||||
|
||||
Adding a build step changes this contract. Read
|
||||
[`.agents/context/publishing.md`](.agents/context/publishing.md) before doing
|
||||
so.
|
||||
**Pushing `main` republishes the live site.** The `pre-push` hook runs the gate,
|
||||
then `.agents/scripts/publish-pages.sh`, which builds and force-pushes `dist/`
|
||||
to `pages`. Use `AF_NO_PUBLISH=1 git push` to land a commit without publishing.
|
||||
`pages` keeps its history, so rollback is a single force-push to an earlier tip;
|
||||
`pages-backup-2026-09-06` is the last commit of the hand-written site. The full
|
||||
procedure is in [`docs/operations-guide.md`](docs/operations-guide.md); read
|
||||
[`.agents/context/publishing.md`](.agents/context/publishing.md) before changing
|
||||
it.
|
||||
|
||||
## Never touch
|
||||
|
||||
- `hands-on/starter/` and `hands-on/rules/` — **lab fixtures.** The exercise
|
||||
_is_ that they are dependency-free vanilla HTML/CSS/JS an attendee can hand to
|
||||
an agent. Componentizing them destroys the lesson. They ship as static assets.
|
||||
- `public/hands-on/starter/` and `public/hands-on/rules/` — **lab fixtures.**
|
||||
The exercise _is_ that they are dependency-free vanilla HTML/CSS/JS an
|
||||
attendee can hand to an agent. Componentizing them destroys the lesson. They
|
||||
ship as static assets.
|
||||
- `submitted-skills/` — other people's submitted work, reproduced verbatim
|
||||
- `skill-reviews/improved/` — generated; edit `src/content/reviews/*.md` instead
|
||||
- `vote-service/` — separate deploy lifecycle; do not fold into the site build
|
||||
- `dist/`, `node_modules/` — build output, never committed
|
||||
- `pnpm-lock.yaml` — **committed, but never hand-edited.** Change it only as a
|
||||
side effect of `pnpm install`. Every worktree spins up with
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
# Gates: review desk privacy and improved-draft audit
|
||||
|
||||
OWNS: skills-review/**, submitted-skills/Anonymous Operational Submission/**,
|
||||
OWNS: src/pages/skills-review.astro, src/components/blocks/{ReviewDetail,
|
||||
ChangeLens,VoteWidget,PreviewPane,FileTabs}.astro, legacy/skills-review/**,
|
||||
submitted-skills/Anonymous Operational Submission/**,
|
||||
skill-reviews/improved/ndo-repro/**, scripts/verify.mjs
|
||||
|
||||
The gate commands below now read `dist/`; run `pnpm run build` before them.
|
||||
|
||||
Scope: Redact the operational submission's identity and URLs from the published
|
||||
review desk, keep package files usable in either preview mode, and explain each
|
||||
improved draft as a concrete diff.
|
||||
|
||||
@@ -15,47 +15,58 @@ skill links to a pinned source with an approval-first installation prompt.
|
||||
|
||||
## Run locally
|
||||
|
||||
This is a dependency-free static site:
|
||||
This is an Astro static site. It builds to `dist/` and ships no runtime
|
||||
dependencies.
|
||||
|
||||
```bash
|
||||
python3 -m http.server 4173
|
||||
pnpm install
|
||||
pnpm run dev # http://localhost:4321/ai-for-dummies/
|
||||
pnpm run build # writes dist/
|
||||
pnpm run preview # serves the built output
|
||||
```
|
||||
|
||||
Then open <http://localhost:4173>.
|
||||
|
||||
Verify the content and interaction contracts with:
|
||||
|
||||
```bash
|
||||
pnpm run verify
|
||||
pnpm run verify # reads dist/, so build first
|
||||
```
|
||||
|
||||
The full gate — `astro check`, `astro build`, `verify.mjs`, `audit-ui.mjs`,
|
||||
`check-tokens.mjs`, and the assertion-count floor — runs as:
|
||||
|
||||
```bash
|
||||
bash .agents/scripts/gate.sh
|
||||
```
|
||||
|
||||
## Project structure
|
||||
|
||||
- `index.html` — default route map and focused chapter navigation
|
||||
- `full-guide/` — the complete bilingual presentation, with responsive audit
|
||||
overrides
|
||||
- `styles.css` / `app.js` — editorial visual system and bilingual field-guide
|
||||
interactions
|
||||
- `responsive.css` — interactive diagrams and Full HD-to-4K adaptations
|
||||
- `src/pages/` — one file per route: the landing route map, the complete
|
||||
bilingual `full-guide`, the chapter pages, `rules`, `skills`, and
|
||||
`skills-review`
|
||||
- `src/components/` — blocks and islands; the interactive diagrams, selectors,
|
||||
and the language toggle
|
||||
- `src/content/` — the content collections every page renders from
|
||||
- `src/styles/tokens.css` — the design tokens
|
||||
- `legacy/` — the editorial visual system and the review-desk modules, not yet
|
||||
migrated into components. Still imported by the pages that need them; see
|
||||
`.agents/context/architecture.md`
|
||||
- `public/` — assets copied to the site root verbatim: fonts, the hands-on labs,
|
||||
and `submitted-skills/`
|
||||
- `docs/references/` — bundled research sources and notes
|
||||
- `docs/operations-guide.md` — canonical SilverBullet operations and skills
|
||||
guide
|
||||
- `hands-on/starter/` — dependency-free Tiny Tasks exercise
|
||||
- `hands-on/rules/` — dependency-free Guardrails lab; toggles rule sources into
|
||||
the prompt
|
||||
- `rules/` — bilingual case study of skills, CLI ratchets, Husky, and PR review
|
||||
- `skills/` — reusable design and rules-case-study skills, plus an interactive
|
||||
package anatomy explorer
|
||||
- `skills-review/` — static review desk for submitted skills; its reader vote
|
||||
widget calls the separate `vote-service`
|
||||
- `vote-service/` — small Go API + Kubernetes manifests backing the
|
||||
skills-review vote widget (see `vote-service/README.md`)
|
||||
- `public/hands-on/starter/` — dependency-free Tiny Tasks exercise
|
||||
- `public/hands-on/rules/` — dependency-free Guardrails lab; toggles rule
|
||||
sources into the prompt
|
||||
- `skills/` — reusable design and rules-case-study skills
|
||||
- `GATES.md` — acceptance ledger for the project
|
||||
|
||||
## Publishing
|
||||
|
||||
The Gitea instance has a Pages Server configured to publish a repository’s
|
||||
`pages` branch under `pages.marcospaulo.dev.br`. The intended site address is:
|
||||
`pages` branch under `pages.marcospaulo.dev.br`. `pages` now carries the
|
||||
**built** site — the contents of `dist/` — not a copy of `main`. The intended
|
||||
site address is:
|
||||
|
||||
<https://netcracker.pages.marcospaulo.dev.br/ai-for-dummies/>
|
||||
|
||||
@@ -70,12 +81,11 @@ skill workflow, see [docs/operations-guide.md](docs/operations-guide.md).
|
||||
## Reader voting on the skills-review desk
|
||||
|
||||
`skills-review/` is static, so its "which draft would you ship?" vote widget
|
||||
calls a separate stateful service — `vote-service/`, a small Go API on its own
|
||||
pod, one vote per visitor enforced server-side by IP (a MAC address is never
|
||||
visible to a server across the internet, so it cannot be used). See
|
||||
[vote-service/README.md](vote-service/README.md) for the API, the anti-abuse
|
||||
design, and the build/push/deploy steps; `skills-review/index.html` sets
|
||||
`window.SKILLS_REVIEW_VOTE_API` to point at it once deployed.
|
||||
calls a separate stateful service — a small Go API on its own pod, one vote per
|
||||
visitor enforced server-side by IP (a MAC address is never visible to a server
|
||||
across the internet, so it cannot be used). Its source no longer lives in this
|
||||
repository; the deployed service is unchanged. `src/pages/skills-review.astro`
|
||||
sets `window.SKILLS_REVIEW_VOTE_API` to point at it.
|
||||
|
||||
## Research
|
||||
|
||||
|
||||