Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/banned-terms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Terms this corpus does not use, one ripgrep pattern per line. Run with
# --pcre2 over the stripped copy of every page, so a real action name, a real
# payload, and a real account name inside code are already gone before a line
# reaches this file. A finding here is prose.

# The category head term. Mirrors the site's own vocabulary source, which the
# built-output gate compiles from the same string, so the two cannot drift:
# packages/atomic-brand/src/vocabulary.ts. The protocol's own nouns
# (collection, schema, template, asset, offer, sale) carry the meaning.
\bnfts?\b|non[-_\s]?fungible

# Era branding rather than a protocol fact. Name the chain, the contract, or
# the API instead.
(?i)\bweb ?3(\.0)?\b

# An asset is an asset. A template is a template. "Token instance" is neither
# contract vocabulary nor API vocabulary.
(?i)\btoken instances?\b

# Wallet where the referent is an account. On Antelope the holder of an asset
# or a token balance is an account, and a wallet is the signing software in
# front of it, so the word points a reader at the wrong layer.
#
# Three senses are sanctioned and none of them takes these constructions:
# Cloud Wallet is a product name, "wallet" naming signing software is the word
# used correctly, and a wallet address is an address. Sample account names
# built on the word sit in code and the stripping step has already removed
# them.
(?i)\b(?:\w+'s|their|its|your|our|his|her|my)\s+wallets?\b(?!\s+addresses?\b)
(?i)\b(?:in|into|inside|from|to|between|out\s+of)\s+(?:the\s+|an?\s+)?wallets?\b(?!\s+addresses?\b)
(?i)\bwallets?\s+(?:holds?|contains?|owns?|receives?|stores?|keeps?)\b
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# The only dependencies this repository declares are the actions its workflow
# pins by commit. Dependabot keeps those pins moving; the corpus itself has no
# package manifest to watch.
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
commit-message:
prefix: ci
95 changes: 95 additions & 0 deletions .github/scripts/check-frontmatter.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env node
/**
* The frontmatter arms that a JSON Schema cannot express, plus the extraction
* ajv validates.
*
* Each arm mirrors a failure that would otherwise surface in the docs-site
* repository at pin-bump time, where the person who caused it is not looking:
* the site takes a page title from the leading H1 and a meta description from
* `scope`, and it throws on a page whose body does not open with an H1.
*
* Usage: node .github/scripts/check-frontmatter.mjs <outdir> [root]
*/
import { access, mkdir, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { leadingHeading, pagesUnder, readPage, FrontmatterError } from './lib/pages.mjs';

/** Every tree the schema binds. `skills/` carries a skill's own frontmatter and README.md carries none. */
const TREES = ['reference', 'guides', 'tutorials', 'concepts', 'learning'];
const ROOT_PAGES = ['AGENTS.md', 'CLAUDE.md', 'validation-log.md'];

/**
* The trees the docs site renders as routes. The band below is a meta
* description budget, so it binds a page that becomes one and says nothing
* about a page the site excludes from rendering.
*/
const RENDERED = ['reference/', 'guides/', 'tutorials/', 'concepts/'];

/** The site composes a title from the H1 and fails its own build over the budget. */
const MAX_HEADING = 40;

/** The meta description band the site's SEO gate holds a rendered page to. */
const SCOPE_BAND = { min: 140, max: 160 };

const [outdir, root = process.cwd()] = process.argv.slice(2);

if (outdir === undefined) {
console.error('usage: node .github/scripts/check-frontmatter.mjs <outdir> [root]');
process.exit(2);
}

const from = resolve(root);
const to = resolve(outdir);
const findings = [];

async function exists(path) {
try {
await access(join(from, path));

return true;
} catch {
return false;
}
}

const pages = [];
for (const tree of TREES) pages.push(...(await pagesUnder(from, tree)));
for (const page of ROOT_PAGES) if (await exists(page)) pages.push(page);

await mkdir(to, { recursive: true });

for (const page of pages) {
let read;
try {
read = await readPage(from, page);
} catch (error) {
if (!(error instanceof FrontmatterError)) throw error;
findings.push(error.message);
continue;
}

await writeFile(join(to, `${page.replaceAll('/', '__').replace(/\.md$/, '')}.yml`), `${read.block}\n`);

const heading = leadingHeading(read.body);
if (heading === null) findings.push(`${page} body does not open with an H1`);
else if (heading.length > MAX_HEADING) {
findings.push(`${page} H1 is ${heading.length} characters, over ${MAX_HEADING}: ${heading}`);
}

const scope = read.values.get('scope');
if (typeof scope === 'string' && RENDERED.some((tree) => page.startsWith(tree))) {
if (scope.length < SCOPE_BAND.min || scope.length > SCOPE_BAND.max) {
findings.push(`${page} scope is ${scope.length} characters, outside ${SCOPE_BAND.min} to ${SCOPE_BAND.max}`);
}
}

for (const entry of read.values.get('depends-on') ?? []) {
if (!(await exists(entry))) findings.push(`${page} depends-on names a page that does not exist: ${entry}`);
}
}

console.log(`frontmatter: read ${pages.length} pages, wrote ${pages.length} frontmatter blocks for ajv`);

for (const finding of findings) console.error(`error: ${finding}`);

process.exit(findings.length === 0 ? 0 : 1);
115 changes: 115 additions & 0 deletions .github/scripts/check-validation-consistency.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env node
/**
* Holds the provenance ledger and the pages to each other. This is the check
* nobody downstream can run: a consumer of this corpus sees a page and a tier,
* and cannot tell that the tier belongs to a page that no longer exists or that
* a page was never graded at all.
*
* Three arms:
* - a reference or guides page with no row in the ledger,
* - a ledger row naming a page that does not exist,
* - a page whose `key-modules` names a baseline the ledger does not pin.
*
* Usage: node .github/scripts/check-validation-consistency.mjs [root]
*/
import { readFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { pagesUnder, readPage } from './lib/pages.mjs';

/** The trees the ledger grades. Tutorials and concepts carry no tier by design. */
const GRADED = ['reference', 'guides'];

/** Every tree that carries `key-modules`, so a new one is covered when it lands. */
const PINNED = ['reference', 'guides', 'tutorials', 'concepts'];

/** The ledger's own path. U8 moves it into the rendered tree; both spellings resolve. */
const LEDGER = ['validation-log.md', 'reference/validation.md'];

const root = resolve(process.argv[2] ?? process.cwd());

/** The body of one `## ` section, by its exact heading text. */
function section(source, heading) {
const pattern = new RegExp(String.raw`^## ${heading}\s*$([\s\S]*?)(?=^## |\Z)`, 'm');
const found = pattern.exec(source);

return found === null ? null : found[1];
}

async function readLedger() {
for (const path of LEDGER) {
try {
return { path, source: await readFile(join(root, path), 'utf8') };
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
}

return null;
}

const ledger = await readLedger();
const findings = [];

if (ledger === null) {
console.error(`error: no provenance ledger at ${LEDGER.join(' or ')}`);
process.exit(1);
}

const baselinesSection = section(ledger.source, 'Pinned baselines');
const pagesSection = section(ledger.source, 'Pages');

if (baselinesSection === null) findings.push(`${ledger.path} has no "## Pinned baselines" section`);
if (pagesSection === null) findings.push(`${ledger.path} has no "## Pages" section`);

/**
* A pin rather than a name: a version, a commit, or a branch. One bullet can
* pin two baselines at once, so every backticked token on a bullet line is a
* candidate and the pins are what gets dropped.
*/
function isPin(token) {
return /^v?\d/.test(token) || /^[0-9a-f]{7,40}$/.test(token) || token === 'main';
}

const baselines = [...(baselinesSection ?? '').matchAll(/^- .*$/gm)]
.flatMap((line) => [...line[0].matchAll(/`([^`]+)`/g)].map((found) => found[1]))
.filter((token) => !isPin(token));

/** The first cell of each table row is the page the row grades. */
const rows = new Map();
for (const found of (pagesSection ?? '').matchAll(/^\| *`([^`]+)` *\|/gm)) {
rows.set(found[1], (rows.get(found[1]) ?? 0) + 1);
}

const graded = [];
for (const tree of GRADED) graded.push(...(await pagesUnder(root, tree)));

for (const page of graded) {
if (page === ledger.path) continue;
if (!rows.has(page)) findings.push(`${page} has no row in ${ledger.path}`);
}

for (const [page, count] of rows) {
if (!graded.includes(page)) findings.push(`${ledger.path} grades a page that does not exist: ${page}`);
if (count > 1) findings.push(`${ledger.path} grades ${page} in ${count} rows`);
}
Comment on lines +83 to +94

const pinned = [];
for (const tree of PINNED) pinned.push(...(await pagesUnder(root, tree)));

for (const page of pinned) {
if (page === ledger.path) continue;
const { values } = await readPage(root, page);

for (const entry of values.get('key-modules') ?? []) {
if (baselines.some((baseline) => entry.includes(baseline))) continue;
findings.push(`${page} key-modules names a baseline ${ledger.path} does not pin: ${entry}`);
}
}

console.log(
`validation-consistency: ${graded.length} graded pages, ${rows.size} ledger rows, ${baselines.length} pinned baselines`,
);

for (const finding of findings) console.error(`error: ${finding}`);

process.exit(findings.length === 0 ? 0 : 1);
25 changes: 25 additions & 0 deletions .github/scripts/install-ripgrep.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Installs ripgrep into a job-owned directory and puts it on the job's PATH.
#
# The runner image does not carry ripgrep, and three checks are ripgrep rules.
# The version and its checksum are pinned here rather than taken from apt,
# because a rule that silently changes engine between runs is a rule nobody can
# reason about: the wallet arm and the casing arm both need PCRE2 lookarounds,
# which this build carries and a distribution build need not.
set -euo pipefail

VERSION='15.2.0'
SHA256='33e15bcf1624b25cdd2a55813a47a2f95dbe126268203e76aa6a585d1e7b149c'
TARGET="ripgrep-${VERSION}-x86_64-unknown-linux-musl"

archive="${RUNNER_TEMP}/${TARGET}.tar.gz"

curl --fail --silent --show-error --location --output "${archive}" \
"https://github.com/BurntSushi/ripgrep/releases/download/${VERSION}/${TARGET}.tar.gz"

echo "${SHA256} ${archive}" | sha256sum --check --status

tar --extract --gzip --file "${archive}" --directory "${RUNNER_TEMP}"

echo "${RUNNER_TEMP}/${TARGET}" >> "${GITHUB_PATH}"
"${RUNNER_TEMP}/${TARGET}/rg" --version | head -1
Loading
Loading