diff --git a/agent_ready/DOCS-spec-notes.md b/agent_ready/DOCS-spec-notes.md
new file mode 100644
index 0000000..796ae33
--- /dev/null
+++ b/agent_ready/DOCS-spec-notes.md
@@ -0,0 +1,97 @@
+# AgentReady spec notes
+
+These notes record what the checker implements and where each rule comes from.
+Fetched 2026-10-22.
+
+## Sources
+
+- AgentReady v2.0 (October 2026, MIT):
+ {_TEXT} No page at this address.` carries `language-*`, `lang-*` or `data-language`. Every fence in a markdown mirror has an info string. |
+| AR-READ-06 | SHOULD | "Discovery files (`llms.txt`, `.well-known/*`, `openapi.json`, `AGENTS.md`) **SHOULD** be linked from content agents already visit" | `/llms.txt` and the catalog (plus `/openapi.json` with `--act`) exist and are linked from the homepage, by `` or ``. |
+| AR-READ-07 | SHOULD | "The site **SHOULD** ship an `llms.txt`: a lean, described index of its key pages." | 200 `text/plain` or `text/markdown`, not HTML. It needs an H1 first, a `>` summary, and an `## H2` with a `- [name](url)` list; other prose is allowed. Every on-site link resolves. |
+| AR-READ-08 | SHOULD | "Key pages **SHOULD** carry JSON-LD — the homepage above all" | The homepage has a parseable `application/ld+json` block with `@context`. |
+| AR-READ-09 | MAY | "Pages **MAY** advertise a markdown mirror via `` or an equivalent HTTP `Link:` header." | Every checked page advertises a mirror. The mirror is 200 `text/markdown` or `text/plain`, and is not HTML. |
+| AR-READ-10 | n/a | "`AR-READ-10` is retired." | not emitted |
+| AR-READ-11 | SHOULD | "Pricing **SHOULD** be published on a first-party page" | SKIP (n/a: no paid plans) |
+| AR-ACT-01 | SHOULD | auth completable by an agent; "OAuth 2.0 with discoverable metadata under `/.well-known`" | `--act`: either `oauth-protected-resource` has `resource`, or `oauth-authorization-server` has `issuer`. |
+| AR-ACT-02 | SHOULD | "A public API **SHOULD** be described by a machine-readable OpenAPI contract." | `--act`: `/openapi.json` has `openapi` and `paths`, and every operation has an `operationId`. |
+| AR-ACT-03 | SHOULD | "...expose an MCP server over Streamable HTTP at `/mcp`." | `--act`, url mode: a POST `initialize` gets a JSON-RPC result, or a 401 with `WWW-Authenticate`. |
+| AR-ACT-04 | MAY | "An MCP server **MAY** ship a server card" | `--act`: the catalog has an `application/mcp-server-card+json` entry. |
+| AR-ACT-05/06/08/09 | MAY/SHOULD | MCP Apps, SDKs+CLI, payments, NLWeb `/ask` | `--act`: SKIP (manual) |
+| AR-ACT-07 | SHOULD | "...register WebMCP tools on `document.modelContext`." | `--act`: `modelContext` appears in the homepage HTML or in its same-site scripts. |
+| AR-ACT-10 | MUST | "...**MUST** expose at least one interface an agent can act through: an MCP server (`AR-ACT-03`) or a CLI (`AR-ACT-06`)." | `--act`: same verdict as the `/mcp` probe. |
+
+## kuhy's choices that are stricter than the spec
+
+- Every in-scope item is required, whatever its strength. That includes the
+ MAY items (FIND-04, READ-09).
+- The spec says a site "**MAY** block a training crawler". kuhy chose to allow
+ training crawlers too, so a blanket `Disallow` for GPTBot, ClaudeBot or any
+ other agent is a FAIL.
+- The act stage is off by default. Turn it on with `--act` only for sites with
+ user actions. The spec allows this: "a purely informational site conforms on
+ `AR-FIND-*` and `AR-READ-*` alone".
+
+## AI Catalog schema (ai-catalog.md, specVersion 1.0)
+
+- The media type is `application/ai-catalog+json`. When fetched by link
+ discovery, plain `application/json` is also acceptable.
+- At the top level, the object MUST have:
+ - `specVersion`: a string in "Major.Minor" form, for example `"1.0"`.
+ - `entries`: an array, which may be empty.
+- The top level MAY have `host`, `extensions` and `signature`.
+- If `host` is present, it MUST have `displayName`. It MAY have
+ `identifier`, `documentationUrl` and `logoUrl`.
+- Each entry MUST have:
+ - `identifier`: a URI or URN. The recommended form is
+ `urn:air:{publisher}:{namespace}:{name}`.
+ - `type`: a media type, for example `application/mcp-server-card+json`.
+ - **exactly one** of `url` or `data`.
+- `displayName` is optional on entries (ai-catalog ADR-0016).
+- Link discovery uses `Link: <...>; rel="ai-catalog"` or
+ ``.
+
+Minimal valid example:
+
+```json
+{"specVersion": "1.0",
+ "host": {"displayName": "kuhy"},
+ "entries": [{"identifier": "urn:air:kuhy.duckdns.org:docs:llms",
+ "type": "text/plain", "url": "https://kuhy.duckdns.org/llms.txt"}]}
+```
+
+Agentic Resource Discovery v0.91 ()")
+ ctx.report.verdict(
+ "AR-READ-01",
+ ["answer not in initial HTML: " + summarize(no_text)] if no_text else [],
+ f"{len(pages)} pages carry >= {ctx.min_text} chars of text without JS",
+ )
+ md_untagged = _check_mirrors(ctx, pages)
+ untagged += md_untagged
+ ctx.report.verdict(
+ "AR-READ-05",
+ ["code without a language tag: " + summarize(untagged)] if untagged else [],
+ f"{blocks} HTML code blocks (plus mirror fences) all language-tagged",
+ )
+
+
+def _check_mirrors(ctx: Ctx, pages: list[str]) -> list[str]:
+ """AR-READ-09 for every page; returns mirrors with untagged fences (READ-05)."""
+ bad: list[str] = []
+ untagged: list[str] = []
+ targets: dict[str, str] = {}
+ for path in pages:
+ if ctx.site.get(path).status != 200:
+ continue
+ href = _mirror_href(ctx, path)
+ target = ctx.site.to_path(href, path) if href else None
+ if target is None:
+ bad.append(f"{path} (no text/markdown alternate link)")
+ else:
+ targets[path] = target
+ ctx.site.prefetch(list(targets.values()))
+ for path, target in targets.items():
+ resp = ctx.site.get(target)
+ if resp.status != 200:
+ bad.append(f"{target} (HTTP {resp.status})")
+ elif (
+ ctx.type_problem(resp, TEXT_TYPES, target)
+ or formats.looks_like_html(resp.text)
+ or not resp.text.strip()
+ ):
+ bad.append(f"{target} (not markdown: {resp.ctype or 'empty'})")
+ elif n := formats.untagged_fences(resp.text):
+ untagged.append(f"{target} ({n} untagged fences)")
+ ctx.report.verdict(
+ "AR-READ-09",
+ ["markdown mirror missing/broken: " + summarize(bad)] if bad else [],
+ f"{len(targets)} pages advertise a working markdown mirror",
+ )
+ return untagged
+
+
+def check_status_codes(ctx: Ctx) -> None:
+ """AR-READ-02: unknown paths are a real 404 (url) / a 404.html exists (dist)."""
+ if not ctx.live:
+ resp = ctx.site.get(f"/agentready-probe-{secrets.token_hex(6)}")
+ text = scan(resp.text).text if resp.body else ""
+ ok = resp.status == 404 and len(text) >= 10
+ ctx.report.verdict(
+ "AR-READ-02",
+ [] if ok else ["no 404.html with real text in the build output"],
+ "404.html exists for unknown paths",
+ )
+ return
+ token = secrets.token_hex(6)
+ probes = [f"/agentready-probe-{token}", f"/agentready-probe-{token}/page.html"]
+ ctx.site.prefetch(probes)
+ bad = [
+ f"{p} -> HTTP {s}"
+ for p in probes
+ if (s := ctx.site.get(p).status) not in (404, 410)
+ ]
+ ctx.report.verdict(
+ "AR-READ-02",
+ ["soft 404, unknown paths must return 404: " + ", ".join(bad)] if bad else [],
+ "unknown paths return 404",
+ )
+
+
+def check_homepage(ctx: Ctx) -> None:
+ """AR-READ-03 real links on the homepage, AR-READ-08 JSON-LD on it."""
+ resp, page = ctx.home()
+ links = {
+ p
+ for a in page.anchors
+ if not a.startswith(("#", "javascript:", "mailto:"))
+ and (p := ctx.site.to_path(a))
+ and p != HOME
+ }
+ ok = resp.status == 200 and bool(links)
+ ctx.report.verdict(
+ "AR-READ-03",
+ [] if ok else ["homepage HTML has no links to other pages"],
+ f"homepage links {len(links)} pages in raw HTML",
+ )
+ problems = []
+ if not page.jsonld_ok:
+ problems.append(
+ 'homepage has no ',
+ ]
+ return (
+ '' + "".join(head) + ""
+ f"{title}
'
+ ''
+ ""
+ )
+
+
+def mirror(title: str) -> str:
+ """Return a markdown mirror with a language-tagged fence."""
+ return f"# {title}\n\n{_TEXT}\n\n```python\nprint('d20')\n```\n"
+
+
+def write(root: Path, rel: str, text: str) -> None:
+ """Write ``text`` to ``root/rel``, creating parent dirs."""
+ path = root / rel
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(text, encoding="utf-8")
+
+
+def build_good(root: Path) -> Path:
+ """Create a fully compliant build output under ``root``."""
+ write(root, "index.html", page("Dice", "/index.md", home=True))
+ write(root, "index.md", mirror("Dice"))
+ write(root, "about/index.html", page("About", "/about/index.md"))
+ write(root, "about/index.md", mirror("About"))
+ write(
+ root,
+ "404.html",
+ "print("d20")Not found
{LONG}