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
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ Source: the repository and pinned tag or commit with the file and line ranges re

Tier: source-read, live-chain, or both.

Row in `validation-log.md` updated: yes, or why the change needs none.
Row in `reference/validation.md` updated: yes, or why the change needs none.
2 changes: 1 addition & 1 deletion .github/scripts/check-frontmatter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { leadingHeading, pagesUnder, readPage, FrontmatterError } from './lib/pa

/** 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'];
const ROOT_PAGES = ['AGENTS.md', 'CLAUDE.md'];

/**
* The trees the docs site renders as routes. The band below is a meta
Expand Down
26 changes: 8 additions & 18 deletions .github/scripts/check-validation-consistency.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,24 @@
*/
import { readFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { pagesUnder, readPage } from './lib/pages.mjs';
import { pagesUnder, readPage, section } 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'];
/** The ledger's own path. It sits in a graded tree and takes no row of its own. */
const LEDGER = '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;
}
try {
return { path: LEDGER, source: await readFile(join(root, LEDGER), 'utf8') };
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}

return null;
Expand All @@ -51,7 +41,7 @@ const ledger = await readLedger();
const findings = [];

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

Expand Down
64 changes: 64 additions & 0 deletions .github/scripts/compose-release-notes.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env node
/**
* Composes the body of a GitHub Release from the ledger's pinned baselines.
*
* A `YYYY.MM.PATCH` tag says when the corpus moved and nothing else, because a
* knowledge corpus has no API surface to break. What a reader can act on is the
* set of releases the pages were read against: a fact here is a fact about one
* pinned contract, indexer, or library version. The ledger is where those pins
* live, so the Release copies them rather than keeping a second list that drifts.
*
* Usage: node .github/scripts/compose-release-notes.mjs <tag> [root]
*/
import { readFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { section } from './lib/pages.mjs';

const LEDGER = 'reference/validation.md';

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

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

let source;
try {
source = await readFile(join(resolve(root), LEDGER), 'utf8');
} catch (error) {
console.error(`error: cannot read ${LEDGER}: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
const baselines = section(source, 'Pinned baselines');

if (baselines === null) {
console.error(`error: ${LEDGER} has no "## Pinned baselines" section`);
process.exit(1);
}

const pins = [...baselines.matchAll(/^- .*$/gm)].map((found) => found[0]);

/**
* A Release body naming no baseline is the failure this script exists to
* prevent, so it fails the run rather than publishing an empty list.
*/
if (pins.length === 0) {
console.error(`error: ${LEDGER} lists no baseline under "## Pinned baselines"`);
process.exit(1);
}

const repository = process.env.GITHUB_REPOSITORY ?? 'atomicassets/atomic-knowledge';
const blob = `https://github.com/${repository}/blob/${tag}`;

process.stdout.write(
[
'The pages at this tag are read against these baselines. Each page names the ones it draws from in its own `key-modules` line.',
'',
...pins,
'',
`What changed: [CHANGELOG.md](${blob}/CHANGELOG.md).`,
`How each page was checked: [the provenance ledger](${blob}/${LEDGER}).`,
'',
].join('\n'),
);
22 changes: 20 additions & 2 deletions .github/scripts/lib/pages.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Reads the frontmatter block every page in this corpus carries, and walks the
* page trees the checks run over.
* Reads the frontmatter block every page in this corpus carries, walks the page
* trees the checks run over, and cuts a named section out of a page.
*
* The reader covers the shapes `.github/frontmatter.schema.json` admits and
* nothing else: a scalar, a flow sequence, and a block sequence, each of
Expand Down Expand Up @@ -122,6 +122,24 @@ export function leadingHeading(body) {
return null;
}

/**
* The body of one `## ` section, by its exact heading text, or null when the
* page has no such heading. The section runs to the next `## ` line or to the
* end of the page, which is what lets a caller read the last section of a file
* as well as one in the middle.
*/
export function section(source, heading) {
const lines = source.split('\n');
const start = lines.findIndex((line) => line.trimEnd() === `## ${heading}`);

if (start === -1) return null;

const rest = lines.slice(start + 1);
const end = rest.findIndex((line) => line.startsWith('## '));

return (end === -1 ? rest : rest.slice(0, end)).join('\n');
}

/** Every markdown page under `directory`, repository-relative, sorted. */
export async function pagesUnder(root, directory, found = []) {
let entries;
Expand Down
105 changes: 105 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Cuts a tag and a Release for what just merged. Tags are `YYYY.MM.PATCH`, one
# per merged pull request: a corpus has no API surface to break, so a semantic
# major and minor carry nothing a reader can act on, while recency and the
# baselines behind the pages are exactly what a reader wants. The Release body
# copies those baselines from the ledger.
#
# The trigger is a branch push, so a tag push never reaches this workflow and
# the tag it creates cannot start a second run. A run whose commit already
# carries a tag stops before the Release, which is what makes a re-run of an
# older commit harmless.
#
# Actions are pinned by commit sha with the version in the trailing comment, the
# same as the checks workflow.
name: release

on:
push:
branches: [main]

# The tag and the Release are the only writes this workflow makes.
permissions:
contents: write

concurrency:
# Two merges landing together would read the same tag list and compute the
# same patch number, so they queue rather than race. A queued run is never
# cancelled: the merge it belongs to is already on the default branch.
group: release
cancel-in-progress: false

jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# The existing tags are the input to the patch number, and a
# shallow checkout carries none of them, which would restart
# every month at zero and collide with a tag that exists.
fetch-depth: 0

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'

- name: The next tag for this month
id: next
run: |
set -euo pipefail

if [ -n "$(git tag --points-at HEAD)" ]; then
echo "::notice::$(git rev-parse --short HEAD) already carries a tag, so this run cuts nothing"
echo "tag=" >> "${GITHUB_OUTPUT}"
exit 0
fi

# The merge commit's own date in UTC rather than the runner
# clock, so a run that starts either side of midnight tags the
# month the merge landed in. Git formats it, so nothing here
# needs a date library.
prefix="$(TZ=UTC git show --no-patch --date=format-local:%Y.%m --format=%cd HEAD)"

# The first release of a month is patch zero, and each one
# after it is the highest suffix already used that month plus
# one. The comparison is numeric, so .10 follows .9 rather than
# sorting between .1 and .2, and a tag whose suffix is not a
# number is not a release of this scheme and is passed over.
patch=0
for existing in $(git tag --list "${prefix}.*"); do
suffix="${existing##*.}"
case "${suffix}" in '' | *[!0-9]*) continue ;; esac
if [ "${suffix}" -ge "${patch}" ]; then patch=$((suffix + 1)); fi
done

tag="${prefix}.${patch}"

# Shape-checked before the write, because everything below
# takes this value on trust: it names the tag the API creates
# and titles the Release.
if [[ ! "${tag}" =~ ^[0-9]{4}\.[0-9]{2}\.[0-9]+$ ]]; then
echo "::error::computed tag is not YYYY.MM.PATCH: ${tag}"
exit 1
fi

echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
echo "cutting ${tag}"

- name: The Release, naming the baselines the corpus was read against
if: steps.next.outputs.tag != ''
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.next.outputs.tag }}
run: |
set -euo pipefail

node .github/scripts/compose-release-notes.mjs "${TAG}" > "${RUNNER_TEMP}/release-notes.md"

# One call creates the tag and the Release together, so a
# failure cannot leave a tag standing with no Release behind
# it, and the next run reads a tag list that means what it says.
gh release create "${TAG}" \
--target "${GITHUB_SHA}" \
--title "${TAG}" \
--notes-file "${RUNNER_TEMP}/release-notes.md"
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Start here. Find the outcome you are working toward below, read the file on that

Read the routed file in full rather than searching it. The facts there encode behavior no method signature carries: which account is billed for a row, which read answers with a null instead of an error, which flag lets a transaction commit while delivering nothing.

Take each section at face value and do not extrapolate past what it states. A page says what was checked, and what it does not say was not checked. `validation-log.md` records how every page was validated and against what. It grades `reference/` and `guides/` only: a tutorial's claim is that its steps run, and a concepts page restates facts the pages it links already carry, so neither takes a row there.
Take each section at face value and do not extrapolate past what it states. A page says what was checked, and what it does not say was not checked. `reference/validation.md` records how every page was validated and against what. It grades `reference/` and `guides/` only: a tutorial's claim is that its steps run, and a concepts page restates facts the pages it links already carry, so neither takes a row there.

Re-check any fact that names a version when that dependency moves. The two SDK pages are pinned to `@atomichub/atomicassets` 2.1.1, read at tag `v2.1.1`, and `@atomichub/atomicmarket` 2.4.1, read at tag `v2.4.1`; the client-library page is pinned to `@wharfkit/antelope` 1.1.1, and the AtomicAssets and AtomicMarket contract pages to `v2.0.0-rc4` and `v2.0.0-rc2`. A fact read at one of those pins is a fact about that release, not about the package name.

Expand Down
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Changelog

What each release of this corpus changed, one release per tag. `Corrected` comes first in every release, because a fact that was wrong is what a returning reader has to see before anything else. The other sections are `Added`, `Revalidated`, and `Removed`, in that order, and a section with nothing in it is left out.

## 2026.08.0

### Corrected

- `reference/sdk/atomicassets.md` no longer says that constructing an `ExplorerApi` fires a `/v1/config` request: construction starts no request, and the action surface is a getter that resolves on first use.
- `reference/sdk/atomicassets.md` no longer says that `transfer` remaps its account parameters. The signature takes `from` and `to`, so there is nothing to remap.
- `reference/sdk/atomicmarket.md` no longer says the action layer covers the royalty configuration alone. It covers the sale, auction, buyoffer, template-buyoffer, marketplace, balance, royalty, and RAM families, with five composers over them.
- `reference/sdk/atomicmarket.md` and `reference/api.md` say the mainnet royalty route answers 416 rather than 404, which inverts the guard advice: `getRoyaltyConfig` maps 416 to `null`, so a caller matching on `ApiError` sees a silent `null` instead.
- `reference/api.md` names both sales list routes the hosted deployment answers on, because the served OpenAPI document describes only the newer one and a generated client disagrees with a hand-written one about which exists.
- `reference/sdk/atomicmarket.md` samples `getConfig` from a deployment that runs V2, because the mainnet value it carried belongs to a chain with no royalty layer at all.
- `reference/atomicassets/structure.md` describes an asset as the individually owned item at the bottom of the four-level model, which reserves the fungible vocabulary for the balances the contract holds.
- `reference/atomicassets/structure.md` and `reference/atomicmarket/fees-and-royalties.md` point their three dead cross-references at something a reader can reach.
- `guides/deposits.md`, `guides/links.md`, `guides/sales.md`, and `reference/atomicassets/backing-tokens.md` write `account` when the holder of an asset or a balance is an account, rather than naming the signing software in front of it.
- Thirty-one `scope` lines across `reference/` and `guides/` moved into the 140 to 160 character band the site renders as a meta description, and twenty more write the product name as a proper noun or mark the account name as code.
- `guides/asset-lifecycle.md` is titled with the query it answers rather than with its category, and three headings across `guides/links.md`, `reference/sdk/atomicassets.md`, and `reference/sdk/atomicmarket.md` write the product name the way the product spells it.
- `reference/validation.md` lists `@wharfkit/antelope` 1.1.1 among the pinned baselines, a source its own row for `reference/wharfkit.md` already named.

### Added

- `tutorials/first-collection.md` walks an empty WAX testnet account to a minted asset, with a checkpoint after every step and an appendix for each way a step fails.
- `tutorials/starters.md` names each starter, what it does, and whether it needs a key.
- `concepts/` explains why the protocol is shaped the way it is, one page each for the four-level model, ownership as chain state, the single order book, royalties as settlement math, choosing a read path, and the comparison against the single-token standard on an EVM chain.
- `starters/` carries five directories a reader clones and runs whole. `read-assets` and `storefront-read` need no key; `create-collection`, `mint-asset`, and `list-a-sale` sign on WAX testnet.
- `guides/signing.md` builds the session every write snippet in this corpus opens against, with one chain id per chain read from a running node.
- `guides/asset-lifecycle.md` shows the same mint built through the SDK builder beside the hand-written payload, and names the numeric guards that throw at the call instead of on chain.
- `guides/sales.md`, `guides/auctions.md`, and `guides/buyoffers.md` name the composer behind each multi-action flow, its memo literals, the two bundle opt-outs, and what a settlement quantity has to be.
- `guides/querying-the-api.md` raises percent-encoding and the testnet host pair out of its preamble into sections of their own.
- `reference/sdk/atomicassets.md` and `reference/sdk/atomicmarket.md` carry their read and action surfaces as tables, with the path-segment guard, the numeric ABI-type guards, the settlement helpers, and the royalty payout ledger the shipped releases added.
- `guides/auctions.md`, `guides/sales.md`, `reference/atomicassets/structure.md`, and `reference/atomicmarket/fees-and-royalties.md` open on a diagram of the sequence their prose states worst.
- `reference/validation.md` carries the provenance ledger, which moved out of the repository root so it renders beside the pages it grades.
- `AGENTS.md` routes by the outcome an agent arrives with, one row per outcome, and names the pins a version-sensitive fact has to be re-checked against.
- `skills/atomic-integration/SKILL.md` carries the mint procedure, the market composers, and the network choice instead of one indirection, and `skills/report/SKILL.md` opens a report with the fields the issue forms accept.
- `.github/frontmatter.schema.json` states the three frontmatter keys a page may carry, and `.github/workflows/checks.yml` gates a merge on ten checks a reviewer cannot run by eye, among them a fragment with no matching heading, a description outside the band the site renders it into, and a ledger row for a page that no longer exists.
- `reference/atomicassets/tables.md` and `reference/atomicassets/actions.md` state that no action decrements a template's `issued_supply`, `burnasset` included, so the field counts lifetime mints and a circulating supply has to subtract burns.
- `reference/atomicmarket/fees-and-royalties.md` states that no per-listing guard caps a seller's exposure to the execution-time collection fee, because `assertsale` takes no fee parameter.
- `reference/atomicassets/actions.md` and `reference/atomicmarket/ram.md` pin the two RAM byte costs a caller pays: 112 bytes for the table scope a first transfer creates, and 121 + 16N bytes for a market balances row holding N token symbols, both computed from a pinned `AntelopeIO/leap` baseline and observed on WAX mainnet.

### Revalidated

- `guides/asset-lifecycle.md`, `guides/auctions.md`, `guides/buyoffers.md`, and `guides/sales.md` were re-read at `@atomichub/atomicassets` 2.1.1 and `@atomichub/atomicmarket` 2.4.1. Every builder and helper they cite is byte-identical to the tag before it, so only the pins moved.

### Removed

- `CLAUDE.md` drops the six lines that restated `AGENTS.md` and keeps the pointer to it.
Loading
Loading