diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a799fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +_site/ diff --git a/README.md b/README.md index ec28047..a3bf5ea 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,24 @@ locally: node scripts/validate.mjs ``` +## Website + +The site at [aips.antseed.com](https://aips.antseed.com) is generated straight +from `AIPS/*.md` — one page per proposal plus an index grouped by status. No +CMS, no server: `scripts/build-site.mjs` renders the markdown with the +templates and styles in `site/`, and Cloudflare Pages builds and deploys on +every merge to `main`. + +Build locally: + +```bash +npm install +npm run build # validates, then renders to _site/ +``` + +Cloudflare Pages settings: build command `npm run build`, output directory +`_site`, production branch `main`. + ## Copyright All AIPs, and this repository's documentation, are released under diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..243fba6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,157 @@ +{ + "name": "antseed-aips", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "antseed-aips", + "dependencies": { + "js-yaml": "^4.1.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^9.2.0" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/markdown-it": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT", + "peer": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/markdown-it": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-anchor": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-9.2.1.tgz", + "integrity": "sha512-p6APiLJDFAW2GEvaavDvhIBn7jrX2jLv77NkBGgNacFTurbORYc4pyYySg/mI6mpR6cHQuAtzKtmqgQr4K8dsQ==", + "license": "Unlicense", + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "license": "MIT" + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..aa4fb2c --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "antseed-aips", + "private": true, + "type": "module", + "description": "AntSeed Improvement Proposals — validator and static site generator", + "scripts": { + "validate": "node scripts/validate.mjs", + "build": "node scripts/validate.mjs && node scripts/build-site.mjs" + }, + "dependencies": { + "js-yaml": "^4.1.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^9.2.0" + } +} diff --git a/scripts/build-site.mjs b/scripts/build-site.mjs new file mode 100644 index 0000000..0d05458 --- /dev/null +++ b/scripts/build-site.mjs @@ -0,0 +1,251 @@ +// Static site generator for aips.antseed.com. +// Reads AIPS/*.md, renders one page per AIP plus an index grouped by status. +// Output: _site/ (deployed by Cloudflare Pages on merges to main). + +import { readdirSync, readFileSync, writeFileSync, mkdirSync, cpSync, existsSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import yaml from 'js-yaml'; +import MarkdownIt from 'markdown-it'; +import anchor from 'markdown-it-anchor'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const out = join(root, '_site'); +const REPO = 'https://github.com/AntSeed/AIPs'; + +const STATUS_ORDER = ['Living', 'Final', 'Last Call', 'Review', 'Draft', 'Stagnant', 'Withdrawn']; +const md = new MarkdownIt({ html: true, linkify: true, typographer: true }).use(anchor, { + permalink: anchor.permalink.linkInsideHeader({ symbol: '#', placement: 'after', class: 'h-anchor' }), +}); + +// Rewrite intra-repo markdown links to site routes. +const defaultLinkOpen = + md.renderer.rules.link_open ?? ((tokens, idx, options, env, self) => self.renderToken(tokens, idx, options)); +md.renderer.rules.link_open = (tokens, idx, options, env, self) => { + const href = tokens[idx].attrGet('href'); + if (href) { + const aip = href.match(/^(?:\.\/|\.\.\/AIPS\/|AIPS\/)?aip-(\d+)(?:\.md)?(#.*)?$/); + if (aip) tokens[idx].attrSet('href', `/aip-${aip[1]}${aip[2] ?? ''}`); + else if (/^(\.\.\/)?LICENSE$/.test(href)) tokens[idx].attrSet('href', `${REPO}/blob/main/LICENSE`); + else if (/^(\.\/|\.\.\/)?aip-template\.md$/.test(href)) tokens[idx].attrSet('href', `${REPO}/blob/main/aip-template.md`); + else if (/^(\.\.\/)?CONTRIBUTING\.md$/.test(href)) tokens[idx].attrSet('href', `${REPO}/blob/main/CONTRIBUTING.md`); + else if (href.startsWith('../assets/') || href.startsWith('assets/')) + tokens[idx].attrSet('href', `/${href.replace(/^\.\.\//, '')}`); + } + return defaultLinkOpen(tokens, idx, options, env, self); +}; + +const esc = (s) => + String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + +function parse(file) { + const raw = readFileSync(join(root, 'AIPS', file), 'utf8'); + const m = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); + if (!m) throw new Error(`${file}: missing frontmatter`); + return { front: yaml.load(m[1]), body: m[2], file }; +} + +// Render "FirstName LastName (@user)" with the handle linked. +const renderAuthors = (author) => + esc(author).replace(/\(@([A-Za-z0-9-]+)\)/g, '(@$1)'); + + +function tocFor(body) { + const items = []; + for (const t of md.parse(body, {})) { + if (t.type === 'heading_open' && t.tag === 'h2') items.push({ open: true }); + else if (t.type === 'inline' && items.length && items[items.length - 1].open) { + const it = items[items.length - 1]; + it.text = t.content; + it.id = t.content.toLowerCase().replace(/[^\w\s-]/g, '').trim().replace(/\s+/g, '-'); + it.open = false; + } + } + return items.filter((i) => i.text); +} + +const css = readFileSync(join(root, 'site', 'style.css'), 'utf8'); + +const page = ({ title, description, canonicalPath, content, extraClass = '' }) => ` + + + + +${esc(title)} + + +' +)}"> + + + + + + + +
+ +
+${content} + + +`; + +// ── collect ────────────────────────────────────────────────────────── +const aips = readdirSync(join(root, 'AIPS')) + .filter((f) => /^aip-\d+\.md$/.test(f)) + .map(parse) + .sort((a, b) => a.front.aip - b.front.aip); + +mkdirSync(out, { recursive: true }); +if (existsSync(join(root, 'assets'))) cpSync(join(root, 'assets'), join(out, 'assets'), { recursive: true }); + +// ── per-AIP pages ──────────────────────────────────────────────────── +const PREAMBLE_ROWS = [ + ['description', 'Description'], + ['author', 'Author'], + ['discussions-to', 'Discussion'], + ['status', 'Status'], + ['type', 'Type'], + ['category', 'Category'], + ['created', 'Created'], + ['requires', 'Requires'], +]; + +for (const { front, body, file } of aips) { + const n = front.aip; + const rows = PREAMBLE_ROWS.filter(([k]) => front[k] != null) + .map(([k, label]) => { + let v; + if (k === 'author') v = renderAuthors(front.author); + else if (k === 'discussions-to') v = `${esc(front[k])}`; + else if (k === 'status') v = esc(front.status); + else if (k === 'requires') + v = String(front.requires) + .split(/,\s*/) + .map((r) => `AIP-${r.trim()}`) + .join(', '); + else if (k === 'created') v = ``; + else v = esc(front[k]); + return `
${label}
${v}
`; + }) + .join('\n'); + + const toc = tocFor(body); + const tocHtml = toc.length + ? `` + : ''; + + const content = ` +
+ + ${tocHtml} +
`; + + mkdirSync(join(out, `aip-${n}`), { recursive: true }); + writeFileSync( + join(out, `aip-${n}`, 'index.html'), + page({ + title: `AIP-${n}: ${front.title} — AntSeed Improvement Proposals`, + description: front.description, + canonicalPath: `/aip-${n}`, + content, + extraClass: 'is-doc', + }) + ); +} + +// ── index ──────────────────────────────────────────────────────────── +const groups = STATUS_ORDER.map((status) => ({ + status, + items: aips.filter((a) => a.front.status === status), +})).filter((g) => g.items.length); + +const tables = groups + .map( + ({ status, items }) => ` +
+

${esc(status)}

+ + + + ${items + .map(({ front }) => ` + + + + `) + .join('\n')} + +
NumberTitleAuthor
${front.aip}${esc(front.title)}${renderAuthors(front.author)}
+
` + ) + .join('\n'); + +const indexContent = ` +
+
+

AntSeed Improvement Proposals

+

Design documents for the AntSeed peer-to-peer AI services network — + protocol standards, contract specifications, and process. Anyone may author one: + start from AIP-1 and open a pull request against + AntSeed/AIPs.

+
+
+
+${tables} +
`; + +writeFileSync( + join(out, 'index.html'), + page({ + title: 'AntSeed Improvement Proposals', + description: 'Design documents for the AntSeed peer-to-peer AI services network.', + canonicalPath: '/', + content: indexContent, + }) +); + +writeFileSync( + join(out, 'index.json'), + JSON.stringify( + aips.map(({ front }) => ({ ...front, url: `https://aips.antseed.com/aip-${front.aip}` })), + null, + 2 + ) +); + +// Cloudflare Pages: clean-URL 404 fallback. +writeFileSync( + join(out, '404.html'), + page({ + title: 'Not found — AntSeed Improvement Proposals', + description: 'No such proposal.', + canonicalPath: '/404', + content: `

Not found

+

No proposal lives at this address. Browse the index.

`, + }) +); + +console.log(`built ${aips.length} AIP page(s) + index → _site/`); diff --git a/site/style.css b/site/style.css new file mode 100644 index 0000000..de5fa05 --- /dev/null +++ b/site/style.css @@ -0,0 +1,162 @@ +/* aips.antseed.com — plain paper-and-ink document site. + General Sans + Geist Mono, one emerald accent for links. */ + +:root { + --paper: #fdfdfc; + --paper-raised: #f5f5f3; + --ink: #17181a; + --ink-soft: #63666b; + --hairline: #e2e2df; + --hairline-soft: #ededea; + --accent: #008359; + --code-bg: #f4f4f2; + + --font-sans: 'General Sans', system-ui, sans-serif; + --font-mono: 'Geist Mono', ui-monospace, 'Fira Code', monospace; +} + +@media (prefers-color-scheme: dark) { + :root { + --paper: #131416; + --paper-raised: #1b1c1f; + --ink: #e9e9e6; + --ink-soft: #9a9da3; + --hairline: #2b2d31; + --hairline-soft: #222327; + --accent: #2fbf8f; + --code-bg: #1c1d20; + } +} + +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +body { + margin: 0; + background: var(--paper); + color: var(--ink); + font-family: var(--font-sans); + font-weight: 500; + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; text-underline-offset: 3px; } +.mono { font-family: var(--font-mono); font-size: 0.92em; } +.shell { max-width: 68rem; margin: 0 auto; padding: 0 clamp(1.1rem, 4vw, 2rem); } + +/* masthead */ +.masthead { border-bottom: 1px solid var(--hairline); } +.masthead-row { + display: flex; align-items: center; justify-content: space-between; + gap: 1.5rem; padding-top: 0.95rem; padding-bottom: 0.95rem; +} +.wordmark { color: var(--ink); font-weight: 600; font-size: 0.98rem; letter-spacing: -0.01em; } +.wordmark em { font-style: normal; font-weight: 500; color: var(--ink-soft); } +.wordmark:hover { text-decoration: none; } +.mast-nav { display: flex; gap: 1.5rem; } +.mast-nav a { + color: var(--ink-soft); font-family: var(--font-mono); + font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.08em; +} +.mast-nav a:hover { color: var(--ink); text-decoration: none; } + +/* hero */ +.hero { padding: 2.6rem 0 2.2rem; border-bottom: 1px solid var(--hairline); } +.hero h1 { margin: 0; font-size: 1.9rem; font-weight: 700; letter-spacing: -0.02em; } +.hero-sub { max-width: 42rem; margin: 0.8rem 0 0; color: var(--ink-soft); font-size: 0.98rem; } + +/* index */ +.status-group { padding: 2rem 0 0.5rem; } +.status-group:last-child { padding-bottom: 3rem; } +.group-title { margin: 0 0 0.7rem; font-size: 1.15rem; font-weight: 600; } +.aip-table { width: 100%; border-collapse: collapse; } +.aip-table th { + text-align: left; font-family: var(--font-mono); font-size: 0.68rem; + text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-soft); + font-weight: 500; padding: 0.4rem 0.8rem; border-bottom: 1px solid var(--ink); +} +.aip-table td { padding: 0.65rem 0.8rem; border-bottom: 1px solid var(--hairline); vertical-align: baseline; } +.col-num { width: 5rem; } +.col-auth { width: 17rem; font-size: 0.88rem; color: var(--ink-soft); } +.row-title { font-weight: 500; color: var(--ink); } +.row-title:hover { color: var(--accent); } +@media (max-width: 640px) { .col-auth { display: none; } } + +/* document pages */ +.doc-shell { + display: grid; grid-template-columns: minmax(0, 1fr) 14rem; + gap: 3rem; padding-top: 2.2rem; padding-bottom: 4rem; +} +@media (max-width: 900px) { .doc-shell { grid-template-columns: minmax(0, 1fr); } .toc { display: none; } } +.doc { min-width: 0; } +.doc-kicker { + font-family: var(--font-mono); font-size: 0.74rem; letter-spacing: 0.05em; + text-transform: uppercase; color: var(--ink-soft); margin-bottom: 0.6rem; +} +.doc-kicker a { color: var(--ink-soft); } +.doc-title { margin: 0 0 1.4rem; font-size: 1.9rem; line-height: 1.15; letter-spacing: -0.02em; font-weight: 700; } + +.preamble { margin: 0 0 2.2rem; padding: 0.3rem 0; border-top: 1px solid var(--ink); border-bottom: 1px solid var(--hairline); } +.pre-row { display: grid; grid-template-columns: 8rem minmax(0, 1fr); gap: 1rem; padding: 0.38rem 0; } +.pre-row + .pre-row { border-top: 1px solid var(--hairline-soft); } +.preamble dt { + font-family: var(--font-mono); font-size: 0.68rem; text-transform: uppercase; + letter-spacing: 0.1em; color: var(--ink-soft); padding-top: 0.22rem; +} +.preamble dd { margin: 0; font-size: 0.92rem; overflow-wrap: anywhere; } + +/* prose */ +.prose { max-width: 44rem; font-size: 0.98rem; line-height: 1.7; } +.prose h2, .prose h3, .prose h4 { letter-spacing: -0.01em; line-height: 1.25; margin: 2.2rem 0 0.8rem; font-weight: 600; } +.prose h2 { font-size: 1.35rem; } +.prose h3 { font-size: 1.1rem; } +.prose h4 { font-size: 0.98rem; } +.h-anchor { margin-left: 0.4rem; color: var(--hairline); font-family: var(--font-mono); font-size: 0.8em; } +.prose h2:hover .h-anchor, .prose h3:hover .h-anchor, .prose h4:hover .h-anchor { color: var(--accent); } +.prose p { margin: 0 0 1rem; } +.prose strong { font-weight: 600; } +.prose ul, .prose ol { padding-left: 1.4rem; margin: 0 0 1rem; } +.prose li { margin: 0.3rem 0; } +.prose blockquote { + margin: 1.3rem 0; padding: 0.8rem 1.1rem; + border-left: 3px solid var(--hairline); background: var(--paper-raised); font-size: 0.94rem; +} +.prose blockquote p:last-child { margin-bottom: 0; } +.prose code { + font-family: var(--font-mono); font-size: 0.85em; + background: var(--code-bg); border-radius: 4px; padding: 0.08em 0.3em; overflow-wrap: anywhere; +} +.prose pre { + background: var(--code-bg); border: 1px solid var(--hairline-soft); border-radius: 6px; + padding: 1rem 1.2rem; overflow-x: auto; margin: 1.3rem 0; font-size: 0.82rem; line-height: 1.6; +} +.prose pre code { background: none; padding: 0; font-size: inherit; } +.prose table { border-collapse: collapse; width: 100%; margin: 1.3rem 0; font-size: 0.9rem; display: block; overflow-x: auto; } +.prose th, .prose td { border: 1px solid var(--hairline); padding: 0.45rem 0.75rem; text-align: left; } +.prose th { font-size: 0.84rem; background: var(--paper-raised); } +.prose hr { border: 0; border-top: 1px solid var(--hairline); margin: 2.2rem 0; } +.prose img { max-width: 100%; } +.doc-source { margin-top: 2.6rem; font-family: var(--font-mono); font-size: 0.78rem; } + +/* toc */ +.toc { position: sticky; top: 1.8rem; align-self: start; padding-top: 2.9rem; } +.toc h2 { + margin: 0 0 0.6rem; font-family: var(--font-mono); font-size: 0.66rem; + text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-soft); font-weight: 500; +} +.toc ol { list-style: none; margin: 0; padding: 0; border-left: 1px solid var(--hairline); } +.toc li a { + display: block; padding: 0.24rem 0 0.24rem 0.85rem; font-size: 0.82rem; + color: var(--ink-soft); border-left: 2px solid transparent; margin-left: -1px; +} +.toc li a:hover { color: var(--ink); border-left-color: var(--accent); text-decoration: none; } + +/* footer */ +.colophon { border-top: 1px solid var(--hairline); margin-top: 2rem; } +.colophon-row { + display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap; + padding-top: 1.1rem; padding-bottom: 1.1rem; + font-family: var(--font-mono); font-size: 0.74rem; color: var(--ink-soft); +} +.colophon a { color: var(--ink-soft); text-decoration: underline; text-underline-offset: 3px; } +.colophon a:hover { color: var(--accent); }