# The AI Readiness Index

**Version 2.0 — specification**

**License:** This document and the checks it defines are published under
[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). Copy it, adapt
it, build your own scanner from it — for any purpose, free, forever — as long
as you credit TEN7 as the creator of the AI Readiness Index and license
what you build the same way. The scanner that computes a score from this
document is separate, proprietary software. Full terms at
[air.ten7.com/license](https://air.ten7.com/license).

---

## What this is

This document defines what a website has to do to score 100 on the AI Readiness Index. It is written for two readers at once: the engineer building the scanner in this repository, and the consultant explaining a score to a client.

Every check has an ID, a weight, a detection method, and a scoring rubric. If a check is not described here, the scanner should not score it.

We built this because "is your site ready for AI" is currently an unanswerable question. Vendors assert things. Nobody measures. Our answer is a number you can reproduce, defend line by line, and re-run in six months to show what changed.

The Index measures one thing: **can machines find, read, trust, and use this website?** It does not measure design quality, marketing effectiveness, or whether the content is any good. A beautiful site can score 12. A plain one can score 94.

---

## How to read a score

A result has three parts, and all three matter.

**The score (0–100).** A single weighted number across eight dimensions.

**The gate status.** Two checks are gates. If a site blocks AI crawlers in `robots.txt` or rejects them at the CDN, the total is capped at 40 no matter how good the rest is. A perfectly marked-up site that returns 403 to `ClaudeBot` is not ready — it is invisible. The cap makes that impossible to hide behind a good average.

**The automated coverage.** The percentage of applicable weight the scanner verified on its own. Some checks — log retention, a monthly prompt panel, whether schema is generated from fields or hardcoded in a template — cannot be seen from outside. Those are scored from attestation and flagged. A score of 82 with 91% automated coverage is a much stronger claim than 82 with 64%.

Report all three together. A bare number invites arguments we don't need to have.

### Grade bands

| Score | Grade | What it means |
|---|---|---|
| 90–100 | Exemplary | Machine-legible by design. Remaining work is optional or emerging. |
| 75–89 | Strong | Solid fundamentals with specific, addressable gaps. |
| 60–74 | Adequate | Findable and readable, but leaving significant value unclaimed. |
| 40–59 | Weak | Partially legible. Real content is not reaching answer engines. |
| 0–39 | Not ready | Structural problems. Start with retrievability before anything else. |
| Any | **Gated** | A gate failed. Fix access first; the rest of the score is theoretical. |

When a gate fails, `grade` is `Gated`. The result also carries `ungated_grade` — the band the ungated score would have earned — so the report can say "Gated, and Strong at 81.2 once access is fixed" rather than leaving the consultant to supply the adjective.

---

## The scoring model

### Bands

Every check scores on a 0–4 band. The check's contribution is `weight × (band ÷ 4)`.

Unless a check says otherwise, use the **default coverage rubric** against the sampled page set:

| Band | Condition |
|---|---|
| 4 | Present and correct on ≥95% of applicable sampled pages |
| 3 | Present and correct on 75–94% |
| 2 | Present on 40–74%, or present everywhere but with correctness errors |
| 1 | Present on 1–39% |
| 0 | Absent, or present but broken everywhere |

Coverage percentages are weighted, not raw page counts. Each money page (see Sampling, step 4) counts as **2** in both the numerator and the denominator; every other sampled page counts as 1. Ten money pages in a 250-URL sample therefore score as a 260-page sample. A failure on `/apply` costs twice what the same failure on a 2019 press release costs, which is the reason the client names those URLs in the first place.

Checks that are inherently site-wide and binary (a file exists, a header is set) define their own bands in the check entry.

### Weights

| # | Dimension | Weight | Checks |
|---|---|---|---|
| 1 | Retrievability and rights | 20.0 | 10 |
| 2 | Crawl and index hygiene | 12.0 | 8 |
| 3 | Rendering and extraction | 20.0 | 8 |
| 4 | Alternate representations | 8.0 | 6 |
| 5 | Structured data and the entity graph | 22.0 | 12 |
| 6 | Authorship, provenance, freshness | 10.0 | 8 |
| 7 | Agent interfaces | 5.0 | 9 |
| 8 | Measurement and instrumentation | 3.0 | 7 |
| | **Total** | **100.0** | **68** |

Dimensions 1, 3 and 5 carry 62 of the 100 points. That is deliberate. Access, extraction, and entity identity determine whether anything else matters.

Dimension 8 carries only 3 points despite being the most commercially valuable work we do. Weight here reflects what a scanner can observe from outside a site, not what the work is worth. Track measurement maturity in the engagement, not in this number.

### Applicability and N/A

Each check declares an applicability rule:

- **`always`** — scored on every site.
- **`conditional`** — scored only if a precondition holds. State the precondition in the check.
- **`vertical`** — scored only for a declared client vertical (higher ed, healthcare, nonprofit, government, research).

A conditional check states its precondition in one of two forms, and never in both:

- **A named site fact** — a single named predicate over collected evidence, such as `sample_contains_tables` or `site_has_search`. Predicates are code, not expressions in a config file, so each one is unit-tested and each one names itself in the N/A reason an auditor reads.
- **A dependency on another check** — written as `requires: AR-1.6 ≥ 2`. A dependency is satisfied when the named check scores **band 2 or higher**. Below band 2 the foundation is broken, and measuring what sits on top of it produces a finding nobody can act on: telling a client their malformed `llms.txt` is well cached is noise. A dependency that is itself N/A cascades N/A to everything that depends on it.

Five checks carry a dependency: AR-1.7 → AR-1.6, AR-4.4 → AR-4.3, AR-4.6 → AR-4.1 **or** AR-4.3, AR-7.2 → AR-7.1, AR-8.4 → AR-8.3.

#### Facts the scanner cannot observe

Six checks are conditional on a business fact no crawler can determine: AR-1.6 and AR-1.8 (licensing and access posture), AR-6.3 (YMYL content), AR-6.5 (research and policy publishing), AR-6.6 (original asset production), and AR-7.8 with AR-7.9 (named critical flows). AR-5.5 needs a declared vertical.

These come from an engagement profile supplied per client — never from inference. A fact not declared is N/A with the reason `not_declared_in_engagement_profile`. The scanner does not sniff a site for medical keywords and decide it is YMYL. A guess inside a number we ask a client to defend line by line is worse than a blank row, and a blank row is an easy conversation: *we did not assess this because you have not told us yet.*

A pre-sales scan run before any client contact will therefore leave those seven checks unassessed. The score renormalizes correctly around them.

A check ruled N/A is removed from the denominator. Recompute:

```
score = (Σ earned_points ÷ Σ applicable_weight) × 100
```

Never score an N/A check as zero. Penalizing a museum for having no `Physician` markup makes the Index useless.

Record every N/A decision with its reason in the result file. An auditor must be able to see what we chose not to measure.

### Gates

Two checks are gates: **AR-1.1** and **AR-1.2**.

If either scores 0 or 1, cap the final score at 40 and set `gated: true`. Report the uncapped score alongside it as `ungated_score` and `ungated_grade`, so the client can see what the site would be worth once access is fixed. That contrast is usually the most persuasive number in the whole report.

The cap is a floor-preserving minimum: `total = min(ungated_score, 40)`. A site scoring 22 that also fails a gate stays at 22. Failing a gate never raises a score.

### Verification mode

Every check declares how its band was determined:

- **`auto`** — the scanner determined the band from evidence it collected.
- **`auto-partial`** — the scanner produced a strong signal, but a human must confirm. Heuristics that can be fooled live here.
- **`manual`** — not observable from outside. Scored from a client attestation, or left `null` and excluded.

Automated coverage counts `auto-partial` at half weight, because a strong signal awaiting human confirmation is genuinely half-verified:

```
automated_coverage = (Σ auto_weight + 0.5 × Σ auto_partial_weight) ÷ Σ applicable_weight
```

The half is not arbitrary. Counting `auto-partial` at zero puts a ceiling of 0.83 on every possible run, because the always-applicable `auto-partial` checks are 15.75 points that never leave the denominator. Counting it in full reports 0.96 for a site nobody has confirmed anything about, and erases the distinction the mode exists to draw.

Never let a `manual` check silently default to 4. An unanswered attestation scores `null` and drops out of the denominator, and the result records it as unverified.

#### Attestation against an `auto-partial` check

Four `auto-partial` checks have a top band that asks something no scanner can see. This document says so in each case, and each has a different observable ceiling because their band tables differ:

| Check | Unattested ceiling | What the attestation confirms |
|---|---|---|
| AR-2.8 | 3 | That IndexNow submissions fire on publish, not on a schedule |
| AR-4.1 | 3 | That `llms.txt` is generated from the content model rather than hand-maintained |
| AR-5.1 | 3 | That JSON-LD comes from mapped fields, confirmed with the build team |
| AR-8.7 | 3 | That sitemaps are submitted and alerts reach a named person |

Each of these four now has a band 3 that describes exactly what a scanner can see, so the attestation lifts 3 to 4 rather than jumping a gap. A check whose band table skips a value cannot express *mostly right*, and *mostly right* is the state most real sites are in.

An attestation may **raise the ceiling** on these four. It never sets a band and never lowers one: the scanner scores what it observed, and a supplied attestation permits that observed band to rise as far as the evidence supports. The result records which points came from a claim rather than a probe, and the report prints them as attested.

Without an attestation the check scores at its observable ceiling and the report says what would be needed to go higher. A client who did the work can prove it; a client who says nothing earns nothing.

Every other `auto-partial` check is fully observable. It carries that mode because the heuristic can be fooled — AR-1.8 compares a signed request against an unsigned one, AR-7.3 calls a live tool — not because it needs a client to vouch for it.

#### Checks that need a previous run

Four checks are defined against change over time: AR-2.5 (is `lastmod` a real signal or deploy noise), AR-3.6 (do anchor IDs survive a deploy), AR-6.4 (do dates move only when content moves), and AR-7.9 (do critical-flow selectors churn).

On a first run there is nothing to compare against. Score the observable half, **cap the band at 3**, set `confidence: medium`, and record `stability_unconfirmed` in the evidence. AR-6.4 already words its band 3 this way; the other three follow the same rule.

Supplying a previous run directory as a baseline lifts the cap and permits band 4.

Every ceiling — this one, the attestation ceiling above, and a check's own declaration
that it could not see further — records the reason it applied. A client told a number
was held below the evidence is owed the sentence explaining what would lift it. This makes the re-audit a first-class input to the score rather than a report-writing exercise, and it gives the client a visible, earnable reason the number rises next quarter.

---

## How the audit runs

### Sampling

Most checks score against a sampled page set, not the whole site. Build the sample like this:

1. **Always include:** the homepage, `/robots.txt`, `/sitemap.xml` (and every child sitemap), `/llms.txt`, `/.well-known/` probes, and every URL in the site's primary navigation.
2. **Stratify by content type.** Use the sitemap index, URL patterns, or a declared content-type map. Take up to 10 URLs per type.
3. **Add depth.** Include at least 5 URLs three or more levels deep, to catch templates the homepage never exercises.
4. **Add the money pages.** The client names up to 10 URLs that matter most — programs, services, donate, apply, contact. These are weighted double in coverage math.
5. **Cap the crawl.** Default 250 URLs. Configurable. Record the actual count.

Minimum viable sample is 25 URLs. Mark the run `low_confidence` only when the sample is under 25 **and** more URLs were discovered than were sampled. A twelve-page nonprofit site that was crawled in full is a complete measurement, not a weak one.

#### The representation sub-sample

Four checks want an extra request against every sampled URL: AR-4.3 (`{path}.md`), AR-4.5 (`Accept: text/markdown`), AR-4.6 (cache behavior), and AR-1.9 (`X-Robots-Tag` on non-HTML). At the default 250-URL cap and one request per second, that is over 800 requests and roughly fifteen minutes of politeness delay stacked on top of the base crawl, against infrastructure that often belongs to a hospital.

Those four checks run against a **representation sub-sample**: 25 URLs by default, drawn from the main sample, always including every money page. The size is configurable and the selected URLs are recorded in the run metadata, so the sub-sample is reproducible across audits. Twenty-five pages resolves a rubric whose finest distinction is 75% against 95%.

### Fetching

Fetch every sampled URL twice:

- **Raw** — plain HTTP GET, no JavaScript, with a declared TEN7 audit user-agent.
- **Rendered** — a headless browser with JavaScript enabled.

The difference between those two responses is the entire evidence base for AR-3.1 and AR-3.2, which together carry 9 points. Do not skip the second fetch.

Separately, probe a small set of URLs with each AI crawler user-agent to feed AR-1.2. Identify yourself honestly in a `From` header. We are auditing on the client's behalf, with permission, and we do not pretend to be somebody else's bot.

#### The agent classes

The Index groups the ten agents three ways. AR-1.1 evaluates all ten against `robots.txt`. AR-1.2 probes only the eight that actually issue requests.

| Class | Agents | AR-1.1 | AR-1.2 probe |
|---|---|---|---|
| Answer-serving | `OAI-SearchBot`, `ChatGPT-User`, `PerplexityBot`, `Claude-User` | yes | yes |
| Training and corpus | `GPTBot`, `ClaudeBot`, `CCBot`, `Bytespider` | yes | yes |
| Directive-only | `Google-Extended`, `Applebot-Extended` | yes | **no** |

`Google-Extended` and `Applebot-Extended` are not crawlers. They are opt-out tokens that Google and Apple honor in `robots.txt`; no request ever arrives carrying either string. Blocking them is a rights decision with no fetch behavior attached, which is why they count in AR-1.1 and cannot be probed in AR-1.2. A user-agent matrix that lists them is describing a crawler that does not exist.

Where a band anchor says **major agents**, it means the four answer-serving agents plus `GPTBot` and `ClaudeBot`.

### Politeness

Respect `Crawl-delay`. Default to 1 request per second with 4 concurrent workers. Support an allowlist of client-approved hosts and refuse to run against anything else. Cache every response to disk so re-scoring never re-crawls.

### Reproducibility

Store the raw evidence — headers, HTML, extracted JSON-LD, screenshots — in a run directory keyed by timestamp. A score you cannot reproduce next quarter is an opinion, not a measurement.

---

## Result schema

The scanner writes one `result.json` per run. Everything else (Markdown report, HTML dashboard, remediation backlog) renders from this file.

```json
{
  "schema_version": "1.0",
  "index_version": "1.0",
  "site": {
    "name": "Example University",
    "base_url": "https://example.edu",
    "vertical": "higher-ed",
    "cms": "drupal-10"
  },
  "run": {
    "id": "2026-09-01T20-14-33Z",
    "started_at": "2026-09-01T20:14:33Z",
    "finished_at": "2026-09-01T20:41:02Z",
    "scanner_version": "0.1.0",
    "urls_sampled": 187,
    "representation_sample": 25,
    "low_confidence": false,
    "baseline_run_id": null,
    "extraction_method": "readability-density",
    "extraction_version": "1"
  },
  "score": {
    "total": 61.4,
    "ungated_score": 61.4,
    "gated": false,
    "grade": "Adequate",
    "ungated_grade": "Adequate",
    "applicable_weight": 94.5,
    "earned_points": 58.0,
    "automated_coverage": 0.87
  },
  "dimensions": [
    {
      "id": 3,
      "name": "Rendering and extraction",
      "weight": 20.0,
      "applicable_weight": 20.0,
      "earned_points": 9.5,
      "score_pct": 47.5
    }
  ],
  "checks": [
    {
      "id": "AR-3.1",
      "title": "Primary content is present without JavaScript",
      "dimension": 3,
      "weight": 6.0,
      "applicability": "always",
      "applicable": true,
      "na_reason": null,
      "verification": "auto",
      "band": 1,
      "earned_points": 1.5,
      "confidence": "high",
      "summary": "Raw HTML contains 22% of rendered text on average across 187 URLs.",
      "evidence": [
        {
          "url": "https://example.edu/admissions",
          "raw_text_chars": 812,
          "rendered_text_chars": 9440,
          "coverage": 0.086
        }
      ],
      "remediation": {
        "severity": "critical",
        "effort": "large",
        "summary": "Server-render the decoupled front end.",
        "detail": "Program and admissions templates ship an empty shell..."
      }
    }
  ],
  "remediation_backlog": [
    {
      "rank": 1,
      "check": "AR-3.1",
      "points_available": 4.5,
      "severity": "critical",
      "effort": "large"
    }
  ]
}
```

#### Schema notes

- `site.cms` is best-effort, from the `generator` meta tag and response headers. It is nullable and nothing scores against it.
- A dimension with no applicable checks reports `score_pct: null`, not `0`. Zero means measured and failed.
- **Rounding happens once, at the end.** All arithmetic runs unrounded; the values written to this file are rounded to one decimal for display. The per-check `earned_points` therefore will not sum exactly to `score.earned_points`, and neither is wrong. Do not "fix" this by rounding intermediates — that reintroduces the drift the single rounding step exists to prevent.
- `extraction_method` and `extraction_version` record how main-content text was isolated. When extraction improves, old scores stay interpretable because the method that produced them is on the record.

### Remediation ranking

Sort the backlog by **points available** (`weight − earned_points`), then by effort ascending, then by check ID ascending. The last key is not cosmetic: two runs of the same evidence must produce the same backlog in the same order, or the re-audit comparison is unreadable.

A check appears in the backlog only if it is applicable, has a band, and has points available above zero. N/A checks, unattested `manual` checks, and checks already at band 4 are not remediation items.

Use three severities — `critical`, `major`, `minor` — and three effort sizes — `small`, `medium`, `large`. Effort is a property of the check, defined in this document. Severity is derived: gate failures and any check scoring **0 or 1** with weight ≥3.0 are `critical`; band ≤2 with weight ≥1.5 is `major`; everything else is `minor`.

Band 1 on a heavy check is a critical finding. A site whose program pages ship 22% of their content scores AR-3.1 at band 1, not 0, and calling that `major` alongside a missing caption on a table understates it to the person deciding what to fund.

The client-facing output is not the score. It is the ordered list of what to fix and what each fix is worth.

---

# The checks

Each entry gives: weight, applicability, verification mode, effort, what the check measures, how to detect it, what evidence to keep, and the band rule when it differs from the default coverage rubric.

---

## Dimension 1 — Retrievability and rights

**Weight 20.0**

Can crawlers and agents reach the site, and what has the organization said they may do with what they take? This dimension comes first because nothing downstream matters if the answer to the first half is no.

### AR-1.1 — No blanket robots.txt blocks on AI crawler user-agents

- **Weight:** 5.0 · **Gate** · **Applicability:** always · **Verification:** auto · **Effort:** small
- **Measures:** Whether `robots.txt` disallows the crawlers that feed answer engines.
- **Detect:** Parse `robots.txt`. Evaluate each of `GPTBot`, `OAI-SearchBot`, `ChatGPT-User`, `ClaudeBot`, `Claude-User`, `PerplexityBot`, `Google-Extended`, `Applebot-Extended`, `CCBot`, `Bytespider` against the rule set. Resolve `User-agent: *` fallbacks correctly, including the longest-match group rule.
- **Evidence:** Raw `robots.txt`, the parsed rule tree, and a per-agent allow/deny verdict for `/` and for three sampled content URLs.
- **Bands:** anchored on *which* agents are blocked, not how many.
  - **4** — every listed agent may fetch content paths.
  - **3** — one training or directive-only agent blocked; every answer-serving agent allowed.
  - **2** — two or more training or directive-only agents blocked, up to and including all of them; every answer-serving agent allowed.
  - **1** — exactly one answer-serving agent blocked.
  - **0** — `Disallow: /` for `*`, or two or more answer-serving agents blocked.
- **Why the gate turns on the answer-serving class:** the Index measures whether machines can find, read and *use* a site. An answer engine blocked at `robots.txt` cannot answer a question about the client, which is the failure the gate exists to catch. A training block is a rights posture, and this document declines to judge rights postures — AR-1.4 scores any combination of `yes` and `no` at 4 for exactly that reason. A university that blocks `GPTBot` and welcomes `ChatGPT-User` is reachable, and capping its score at 40 would be measuring our approval rather than machine reach.
- **Consequence:** blocking every training crawler costs 2 of the 5 points and does not gate. Blocking a single answer-serving agent gates.
- **Absent robots.txt:** a 404 scores 4. Nothing is disallowed, so nothing is blocked. This is correct and frequently queried; a missing file is not a failure of this check.
- **Note:** A deliberate, documented block is a business decision, not a defect. Record it, score it honestly, and say in the report that the client chose it. The number should reflect machine reach, not our approval.
- **Fix:** Remove inherited blocks. Keep only directives the client can explain.

### AR-1.2 — CDN and WAF do not silently reject AI crawlers

- **Weight:** 5.0 · **Gate** · **Applicability:** always · **Verification:** auto · **Effort:** medium
- **Measures:** Whether the edge returns real content to AI user-agents, regardless of what `robots.txt` permits.
- **Detect:** Request 10 sampled URLs with each of the eight fetching AI user-agents — the answer-serving and training classes defined under Fetching. `Google-Extended` and `Applebot-Extended` are excluded; they issue no requests. Record status code, response size, and whether the body matches the baseline fetch. Flag 403, 429, 503, interstitial challenge markup, and bodies more than 40% smaller than baseline.
- **Evidence:** A user-agent × URL matrix of status codes, sizes, and challenge detection.
- **Bands:** 4 — all agents get 200 with full content. 3 — one agent rate-limited but eventually served. 2 — one agent blocked or challenged. 1 — several blocked. 0 — most blocked.
- **Fix:** Audit Cloudflare bot settings, Pantheon AGCDN rules, Fastly VCL, and origin WAF. Verify by re-running this probe, not by reading configuration.

### AR-1.3 — No interactive challenges on read-only paths

- **Weight:** 2.0 · **Applicability:** always · **Verification:** auto · **Effort:** small
- **Measures:** Whether anonymous GETs on browse, search, listing, and detail pages return content without a challenge.
- **Detect:** Look for Turnstile, reCAPTCHA, hCaptcha, and JS-challenge signatures in raw responses on non-form URLs. Detect meta-refresh interstitials and challenge cookies.
- **Evidence:** Per-URL challenge detection with the matching signature.
- **Fix:** Scope challenges to POST endpoints and authenticated routes.

### AR-1.4 — Content Signals declared in robots.txt

- **Weight:** 2.0 · **Applicability:** always · **Verification:** auto · **Effort:** small
- **Measures:** Whether the site states a machine-readable position on `search`, `ai-input`, and `ai-train` using Cloudflare's Content Signals Policy.
- **Detect:** Parse `Content-Signal:` lines. Validate syntax (`key=yes|no`, comma-delimited) and check for contradiction with `Disallow` rules.
- **Bands:** 4 — all three signals declared, syntactically valid, consistent with `robots.txt`. 3 — declared but one signal omitted. 2 — declared with a syntax error or an internal contradiction. 1 — a partial or malformed attempt. 0 — absent.
- **Note:** Any combination of `yes`/`no` scores 4. We are measuring that a deliberate position exists, not which position it is.
- **Fix:** Run the policy conversation with the client, then add the line.

### AR-1.5 — Human-readable AI policy comment in robots.txt

- **Weight:** 1.0 · **Applicability:** always · **Verification:** auto-partial · **Effort:** small
- **Measures:** Whether a person reading `robots.txt` finds a plain-language statement of the organization's position.
- **Detect:** Extract `#` comment blocks. Require ≥120 characters of prose and at least two policy terms (`AI`, `training`, `crawl`, `license`, `permission`).
- **Bands:** 4 — substantive block present and consistent with the machine signals. 2 — a comment exists but is thin or boilerplate. 0 — none.
- **Fix:** Write four sentences above the signals. Journalists and counsel read this file.

### AR-1.6 — RSL licensing document published and referenced

- **Weight:** 1.0 · **Applicability:** conditional — the client holds licensable archives (journals, museums, associations, research bodies) or has opted into a licensing posture · **Verification:** auto · **Effort:** medium
- **Measures:** Whether machine-readable licensing terms exist under RSL 1.0 and are discoverable.
- **Detect:** Fetch `/license.xml` and any path referenced from `robots.txt`. Validate against the RSL 1.0 schema. Check for an HTTP `Link:` header on content responses.
- **Bands:** 4 — valid document, referenced from both `robots.txt` and headers. 3 — valid, referenced from one. 2 — present but invalid. 1 — referenced but missing. 0 — absent.
- **Fix:** Decide terms with the client first. The XML is the easy half.

### AR-1.7 — RSL terms propagated to feeds and schema

- **Weight:** 1.0 · **Applicability:** conditional — requires AR-1.6 to pass · **Verification:** auto · **Effort:** small
- **Measures:** Whether licensing travels with the content through every channel, not just the well-known file.
- **Detect:** Check RSS and Atom feeds for licensing elements. Check sampled JSON-LD for a `license` property on licensable types.
- **Fix:** Add the license reference to feed generation and to the schema field mapping.

### AR-1.8 — Web Bot Auth verification configured

- **Weight:** 1.0 · **Applicability:** conditional — the client wants selective agent access rather than open or closed · **Verification:** auto-partial · **Effort:** large
- **Measures:** Whether the edge distinguishes cryptographically verified agents from unsigned scrapers.
- **Detect:** Probe `/.well-known/http-message-signatures-directory` if the site operates its own agents. Otherwise send a signed and an unsigned request and compare treatment.
- **Bands:** 4 — verified agents pass, unsigned agents claiming an AI UA are challenged. 2 — signature headers accepted but not acted on. 0 — no differentiation.
- **Fix:** Configure edge bot rules on `Signature-Agent` rather than user-agent strings.

### AR-1.9 — Correct X-Robots-Tag on non-HTML resources

- **Weight:** 1.0 · **Applicability:** conditional — the site serves PDFs, documents, media, or JSON endpoints · **Verification:** auto · **Effort:** small
- **Measures:** Whether indexing directives reach resources that cannot carry a meta robots tag.
- **Detect:** HEAD every sampled PDF, document, media file, and API endpoint. Record `X-Robots-Tag`. Flag `noindex` on substantive content and absent directives on endpoints that should be excluded.
- **Bands:** 4 — every non-HTML type carries a deliberate, correct value. 2 — mixed or partial. 0 — absent everywhere, or substantive PDFs marked `noindex`.
- **Fix:** Set headers by path pattern at the CDN or web server.

### AR-1.10 — No stray noarchive or nosnippet directives

- **Weight:** 1.0 · **Applicability:** always · **Verification:** auto · **Effort:** small
- **Measures:** Whether the site suppresses the excerpting that answer engines depend on.
- **Detect:** Parse meta robots tags and `X-Robots-Tag` headers across the sample for `noarchive`, `nosnippet`, and `max-snippet:0`.
- **Bands:** 4 — none present. 2 — present on a minority of pages. 0 — present site-wide.
- **Fix:** Remove them. They are almost always left over from a legacy SEO module.

---

## Dimension 2 — Crawl and index hygiene

**Weight 12.0**

AI crawlers visit less often than Googlebot and tolerate less noise. Budget wasted on faceted search never reaches the pages that answer questions.

### AR-2.1 — Missing pages return 404 or 410

- **Weight:** 2.0 · **Applicability:** always · **Verification:** auto · **Effort:** small
- **Measures:** Soft 404s — "not found" content served with a 200 status.
- **Detect:** Request 10 deliberately invalid URLs under real path prefixes. Flag 200 responses. Separately, flag sampled pages whose body matches a not-found template or falls below a minimum content threshold while returning 200.
- **Evidence:** Probe URLs with status codes and body fingerprints.
- **Fix:** Return real status codes. Use 410 for content that is permanently gone.

### AR-2.2 — Redirects resolve in a single hop

- **Weight:** 1.0 · **Applicability:** always · **Verification:** auto · **Effort:** small
- **Measures:** Redirect chain depth and loops.
- **Detect:** Follow redirects without auto-resolution and count hops per sampled URL and per internal link target.
- **Bands:** 4 — no chain exceeds one hop. 3 — a few two-hop chains. 2 — chains of three or more. 1 — widespread chains, meaning both more than 10% of the sample and at least three URLs. 0 — loops present.
- **Fix:** Flatten the redirect table so every source points at its final destination.

### AR-2.3 — Every page emits a valid, self-consistent canonical

- **Weight:** 2.0 · **Applicability:** always · **Verification:** auto · **Effort:** small
- **Measures:** Presence and correctness of `rel=canonical`.
- **Detect:** For each sampled page, extract the canonical and resolve it. Flag: missing, multiple, cross-host, pointing at a redirect, pointing at a non-200, or pointing at the homepage from a deep page.
- **Fix:** Emit a self-referencing canonical by default and correct the exceptions.

### AR-2.4 — Faceted, calendar, and parameterized URLs are controlled

- **Weight:** 2.0 · **Applicability:** conditional — the site has search, filtering, or a calendar · **Verification:** auto-partial · **Effort:** medium
- **Measures:** Whether the crawlable URL space is finite and predictable.
- **Detect:** Crawl listing pages two levels deep with parameters followed. Measure URL growth rate and count distinct parameter combinations. Check whether those URLs are disallowed, `noindex`, or canonicalized to the unfiltered listing.
- **Bands:** 4 — parameter space bounded by robots rules, `noindex`, or canonicals. 3 — mostly controlled with gaps. 2 — partially controlled. 1 — minimal control. 0 — unbounded expansion observed.
- **Fix:** Combine disallow patterns, `noindex` on parameterized views, and canonicals back to the base listing.

### AR-2.5 — Sitemap lastmod reflects real content changes

- **Weight:** 2.0 · **Applicability:** always · **Verification:** auto-partial · **Effort:** medium
- **Measures:** Whether `lastmod` is a freshness signal or deploy noise.
- **Detect:** Parse all `lastmod` values. Flag clustering — more than 60% of URLs sharing a single date, or timestamps identical to the minute across content types. Cross-check against on-page `dateModified` where present. Re-run across two audits to confirm.
- **Bands:** 4 — values are distributed and match on-page dates. 3 — mostly accurate with some clustering. 2 — heavy clustering. 1 — all identical. 0 — absent.
- **First run:** cap at 3 without a baseline run. See *Checks that need a previous run*.
- **Fix:** Wire `lastmod` to the node's changed timestamp, not to cron.

### AR-2.6 — Sitemap index split by type, with media sitemaps

- **Weight:** 1.0 · **Applicability:** always · **Verification:** auto · **Effort:** small
- **Measures:** Whether sitemap structure makes crawl coverage diagnosable.
- **Detect:** Fetch `/sitemap.xml`. Confirm it is an index with child sitemaps. Check for image and video sitemaps where the site has substantial media. Validate against the sitemap schema and the 50k URL / 50MB limits.
- **Fix:** Generate per-content-type children and submit the index to both search consoles.

### AR-2.7 — Paginated series are coherently signaled

- **Weight:** 1.0 · **Applicability:** conditional — the site has paginated listings or multi-page articles · **Verification:** auto · **Effort:** small
- **Measures:** Whether page 2 and beyond are reachable and correctly related to page 1.
- **Detect:** Identify paginated series by URL pattern. Check for `rel=next`/`rel=prev`, a view-all page carrying the canonical, and whether deep pages are reachable by crawl.
- **Fix:** Prefer a view-all canonical where page weight allows.

### AR-2.8 — IndexNow fires on publish and update

- **Weight:** 1.0 · **Applicability:** always · **Verification:** auto-partial · **Effort:** small
- **Measures:** Whether the site pushes change notifications instead of waiting to be crawled.
- **Detect:** Look for an IndexNow key file at the site root (a 32–128 character hex filename returning its own key). Confirm the key resolves. Whether submissions actually fire on publish requires attestation.
- **Bands:** 4 — key file present, valid, and submission confirmed. 3 — key file present and valid, submission unconfirmed. 2 — a key file is referenced but does not resolve or does not contain its own key. 0 — absent or not discoverable.
- **Limitation:** the key file's name *is* the key, and the key is not published. A scanner can only find it where `robots.txt` or a conventional path advertises it, so a zero here means *not discoverable* rather than *absent*. Score it honestly and say which.
- **Fix:** Hook submission into the publish workflow rather than a schedule.

---

## Dimension 3 — Rendering and extraction

**Weight 20.0**

Most AI crawlers do not execute JavaScript. This dimension asks whether the facts exist in the server response, in a structure a parser can follow.

### AR-3.1 — Primary content is present without JavaScript

- **Weight:** 6.0 · **Applicability:** always · **Verification:** auto · **Effort:** large
- **Measures:** The gap between the raw HTTP response and the hydrated DOM. This is the single heaviest check in the Index.
- **Detect:** For every sampled URL, extract main-content text from the raw fetch and from the rendered fetch. Compute `coverage = raw_chars ÷ rendered_chars` after normalizing whitespace and stripping nav, header, and footer regions. Average across the sample and report the per-template distribution.
- **Evidence:** Per-URL character counts, coverage ratio, and the first 500 characters of each version for spot-checking.
- **Bands:** 4 — mean coverage ≥0.95. 3 — 0.80–0.94. 2 — 0.50–0.79. 1 — 0.20–0.49. 0 — below 0.20.
- **Edge cases:** Coverage above 1.0 happens when JavaScript *removes* content that the raw response contained. Clamp the per-URL value to 1.0 for banding and keep the unclamped ratio in the evidence. A URL whose rendered text is empty is excluded from the mean and the exclusion is recorded — it is a fetch problem, not a coverage measurement.
- **Note:** Report the worst-performing template by name. "Your program pages ship 8% of their content" lands harder than a mean.
- **Fix:** Server-render or statically render. For decoupled Drupal this is the largest line item in most remediations.

### AR-3.2 — Progressive disclosure content ships in the initial HTML

- **Weight:** 3.0 · **Applicability:** conditional — the site uses tabs, accordions, load-more, or infinite scroll · **Verification:** auto · **Effort:** medium
- **Measures:** Whether tabbed, collapsed, or lazily paged content exists before interaction.
- **Detect:** Identify disclosure widgets by ARIA roles (`tab`, `tabpanel`, `region` with `aria-expanded`), `<details>`, and common class patterns. For each, check whether the panel body is populated in the raw HTML. Detect infinite scroll by watching for XHR content growth on scroll in the rendered fetch, and check for a paginated fallback.
- **Bands:** 4 — every panel populated in raw HTML; infinite scroll has crawlable pagination. 3 — one pattern incomplete. 2 — panels populated on some templates only. 1 — most content loads on interaction. 0 — all disclosure content is fetched on demand.
- **Fix:** Render all panels and hide with CSS. Give infinite scroll a paginated fallback.

### AR-3.3 — Semantic HTML structure

- **Weight:** 2.0 · **Applicability:** always · **Verification:** auto · **Effort:** medium
- **Measures:** Whether document structure carries meaning or only styling.
- **Detect:** Per page, require exactly one `<main>`, at least one `<article>` or `<section>` wrapping the primary content, and a `<nav>` for primary navigation. Compute a div-to-semantic-element ratio in the content region and flag pages above 12:1.
- **Fix:** Replace structural divs with `article`, `section`, `nav`, `aside`, `figure`, and `dl`.

### AR-3.4 — One h1 and a non-skipping heading hierarchy

- **Weight:** 3.0 · **Applicability:** always · **Verification:** auto · **Effort:** small
- **Measures:** Whether headings segment the page into topics a model can follow.
- **Detect:** Count `h1` elements. Walk the heading tree and flag skipped levels, empty headings, and headings used purely for styling (no following content).
- **Bands:** Default coverage rubric, where a page passes only if it has exactly one non-empty `h1` and no skipped levels.
- **Fix:** Move the site name out of `h1`. Fix skips in the templates, not the content.

### AR-3.5 — Real data tables with header cells

- **Weight:** 2.0 · **Applicability:** conditional — the sample contains tables · **Verification:** auto · **Effort:** medium
- **Measures:** Whether tabular facts — tuition, hours, comparisons, fees — are machine-parseable.
- **Detect:** For each `<table>`, require `<th>` with `scope`, a `<caption>`, and `<thead>`/`<tbody>`. Separately flag layout tables (no `th`, presentational attributes, single row or column).
- **Bands:** 4 — every data table complete, no layout tables. 3 — minor omissions such as missing captions. 2 — header cells present without scope. 1 — tables present with no header cells. 0 — layout tables carrying data.
- **Fix:** Fix the tables that carry facts first. Convert layout tables to CSS.

### AR-3.6 — Stable, human-readable anchor IDs on section headings

- **Weight:** 2.0 · **Applicability:** always · **Verification:** auto-partial · **Effort:** medium
- **Measures:** Fragment-level citability — whether a model can deep-link a passage rather than a page.
- **Detect:** For headings below `h1`, check for an `id`. Classify each as slug-like (derived from heading text) or unstable (`section-3`, `block-a7f2c1`, framework hashes). Compare IDs across two runs to detect churn.
- **Bands:** 4 — ≥95% of subheadings carry slug-like IDs that are stable across runs. 3 — 75–94%. 2 — IDs present but a majority are generated or unstable. 1 — sparse. 0 — none.
- **First run:** cap at 3 without a baseline run. Stability cannot be asserted from one observation. See *Checks that need a previous run*.
- **Fix:** Derive IDs from heading text, not render order.

### AR-3.7 — Alt text on content images, empty alt on decorative

- **Weight:** 1.0 · **Applicability:** always · **Verification:** auto · **Effort:** medium
- **Measures:** Whether images have a textual representation.
- **Detect:** For every `<img>`, record presence of `alt`, its length, and whether the image sits inside a content region or a decorative one. Flag missing `alt`, filename-as-alt, and long alt on obviously decorative images.
- **Fix:** Audit existing content, then make the field required in the media entity so the problem stops recurring.

### AR-3.8 — Key facts exist as HTML text, not only in images or PDFs

- **Weight:** 1.0 · **Applicability:** always · **Verification:** auto-partial · **Effort:** large
- **Measures:** Whether high-value facts are trapped in a scan, an infographic, or a PDF.
- **Detect:** Identify pages whose visible text is thin relative to their images or that link to a PDF as the primary content. Extract PDF text and look for numbers, dates, and prices that appear nowhere in the site's HTML. Requires review before scoring.
- **Bands:** 4 — no high-value facts found only in binaries. 2 — some found only in PDFs. 0 — core facts (tuition, hours, deadlines, contact) exist only in binaries.
- **Fix:** Publish an HTML equivalent. Keep the PDF as a download, not as the source of truth.

---

## Dimension 4 — Alternate representations

**Weight 8.0**

Clean, low-noise versions of the content alongside the HTML.

**Read this before you sell it:** evidence that `llms.txt` is actually consumed by the major models remains thin, and Google has said it does not use it. We include the dimension because it is cheap, it forces good content hygiene, and being early costs nothing. We weight it at 8 points, not 20, for the same reason. Do not let a report imply this is a ranking lever.

### AR-4.1 — /llms.txt exists and is generated from the content model

- **Weight:** 3.0 · **Applicability:** always · **Verification:** auto-partial · **Effort:** medium
- **Measures:** Presence, validity, and freshness of `/llms.txt`.
- **Detect:** Fetch `/llms.txt`. Validate against the llmstxt.org structure: an `# H1` title, an optional blockquote summary, and `##` sections of Markdown links. Resolve a sample of the linked URLs. Compare listed URLs against the sitemap to estimate coverage and staleness. Detect the Drupal `llms_txt` module by response headers or known output patterns.
- **Bands:** 4 — valid, links resolve, coverage tracks the sitemap, generation confirmed. 3 — valid and current but hand-maintained. 2 — present with broken links or clear staleness. 1 — present but malformed. 0 — absent.
- **Fix:** On Drupal, use the `llms_txt` module with token-driven menus. A hand-written file is stale within a quarter.

### AR-4.2 — /llms-full.txt where full-text inclusion is appropriate

- **Weight:** 1.0 · **Applicability:** conditional — the client permits full-text inclusion; N/A if Content Signals declare `ai-train=no` · **Verification:** auto · **Effort:** small
- **Measures:** Presence of the expanded variant.
- **Detect:** Fetch `/llms-full.txt`. Confirm it inlines content rather than linking. Cross-check against AR-1.4 for contradiction.
- **Bands:** 4 — present and consistent with the licensing posture. 2 — present but contradicts declared signals. 0 — absent where appropriate.
- **Fix:** Gate this on the licensing decision, not on convenience.

### AR-4.3 — Markdown companion for every canonical page

- **Weight:** 2.0 · **Applicability:** always · **Verification:** auto · **Effort:** medium
- **Measures:** Whether a clean `.md` representation exists alongside each page.
- **Detect:** For each sampled content URL, request `{path}.md`. Require a 200 with `text/markdown` or `text/plain`, content that materially matches the page body, and no navigation chrome.
- **Fix:** Generate from the same source as the HTML. Strip nav, promos, and cookie banners.

### AR-4.4 — link rel=alternate advertises the Markdown version

- **Weight:** 1.0 · **Applicability:** conditional — requires AR-4.3 to pass · **Verification:** auto · **Effort:** small
- **Measures:** Discoverability of the alternate representation.
- **Detect:** Parse `<link rel="alternate" type="text/markdown">` from the head and resolve the href.
- **Fix:** Add it to the head template. A file nobody can find is not much use.

### AR-4.5 — Accept: text/markdown content negotiation

- **Weight:** 0.5 · **Applicability:** always · **Verification:** auto · **Effort:** medium
- **Measures:** Whether the canonical URL serves Markdown on request, with correct cache headers.
- **Detect:** Request sampled URLs with `Accept: text/markdown`. Confirm the response type and that `Vary: Accept` is set. Re-request through the CDN to confirm the variants are cached separately.
- **Bands:** 4 — negotiation works and `Vary: Accept` is correct. 2 — negotiation works but `Vary` is missing, which risks serving Markdown to browsers. 0 — not supported.
- **Fix:** Getting `Vary` wrong is worse than not doing this at all. Verify at the edge.

### AR-4.6 — Alternate representations are edge-cached

- **Weight:** 0.5 · **Applicability:** conditional — requires AR-4.1 or AR-4.3 · **Verification:** auto · **Effort:** small
- **Measures:** Whether generated representations hit the CDN rather than origin.
- **Detect:** Request `/llms.txt` and sampled `.md` URLs twice. Read cache-status headers and compare response times.
- **Fix:** Set TTLs and wire invalidation to the same cache tags as the source content.

---

## Dimension 5 — Structured data and the entity graph

**Weight 22.0**

The heaviest dimension. An `Organization` block and some FAQ markup is commodity work. A linked, externally anchored entity graph is not.

### AR-5.1 — JSON-LD is generated from mapped fields, not hardcoded

- **Weight:** 2.0 · **Applicability:** always · **Verification:** auto-partial · **Effort:** medium
- **Measures:** Whether schema follows the content or rots on the next content type change.
- **Detect:** Heuristics only. Compare JSON-LD values against visible field values on the same page — a mapped implementation matches. Flag identical literal values repeated across pages of the same type where the visible content differs. Detect Schema.org Metatag output patterns on Drupal. Confirm with the build team.
- **Bands:** 4 — values track content across every sampled page of a type, and the build team confirms the mapping. 3 — values track content on every sampled page, generation unconfirmed. 2 — mixed, with some literals. 0 — clear hardcoding, or values contradict the visible page.
- **Fix:** Map Drupal fields to schema properties through configuration so editors maintain it.

### AR-5.2 — A single linked entity graph with @id references

- **Weight:** 4.0 · **Applicability:** always · **Verification:** auto · **Effort:** medium
- **Measures:** Whether schema forms one coherent graph or dozens of disconnected assertions.
- **Detect:** Extract all JSON-LD across the sample. Build a graph of nodes and `@id` references. Measure: what fraction of **entity** nodes carry an `@id` — value objects such as `ListItem`, `PostalAddress`, `ContactPoint`, `EntryPoint` and `BreadcrumbList` are excluded, because they have no identity and nothing should ever reference them; whether `Organization` and `WebSite` have stable canonical `@id` URIs reused site-wide; whether `WebPage` nodes reference `WebSite` and `WebSite` references `Organization`; and how many duplicate definitions of the same entity exist.
- **Evidence:** The resolved graph, orphan node count, duplicate entity count.
- **Bands:** 4 — canonical `@id`s reused, chain intact, no duplicates. 3 — chain intact with minor duplication. 2 — `@id`s present but inconsistently reused. 1 — `@id`s rare. 0 — isolated blobs with no references.
- **Fix:** Define canonical `@id` URIs once and reference them everywhere.

### AR-5.3 — sameAs links to external authority records

- **Weight:** 3.0 · **Applicability:** always · **Verification:** auto · **Effort:** small
- **Measures:** Entity disambiguation — whether a model can be certain which organization this is.
- **Detect:** Extract `sameAs` from `Organization` and `Person` nodes. Classify each URL by authority: tier 1 (Wikidata, ROR, NPI, IRS EIN, ORCID), tier 2 (Wikipedia, Crunchbase, LinkedIn, Candid), tier 3 (social profiles). Resolve each URL and confirm it returns 200 and references the organization back where possible.
- **Resolution is opt-in.** Those hosts are not the client's, they are not on the allowlist, and requesting them tells a third party which site is being audited. Classification from the markup alone separates a controlled identifier from a Facebook page; without resolution the check stops at band 3 and says why.
- **Bands:** 4 — at least one tier-1 identifier plus two others, all resolving. 3 — tier-1 present, some links stale. 2 — tier-2 and tier-3 only. 1 — social profiles only. 0 — no `sameAs`.
- **Fix:** Claim the Wikidata item. For universities, ROR is free and takes a day.

### AR-5.4 — BreadcrumbList on every page below the homepage

- **Weight:** 2.0 · **Applicability:** always · **Verification:** auto · **Effort:** small
- **Measures:** Whether a page declares where it sits in the information architecture.
- **Detect:** Validate `BreadcrumbList` structure and confirm the trail matches the URL path or menu position. Flag trails that are a single item or that contradict the path.
- **Fix:** Generate from the menu, not by hand.

### AR-5.5 — Vertical-specific schema types

- **Weight:** 4.0 · **Applicability:** vertical — scored against the client's declared vertical only · **Verification:** auto · **Effort:** large
- **Measures:** Whether the site models the things it actually contains, using the right types.
- **Detect:** From the declared vertical, load the expected type set and check coverage against matching content types:
  - **Higher ed:** `Course`, `EducationalOccupationalProgram`, `CollegeOrUniversity`, `EducationEvent`
  - **Healthcare:** `MedicalOrganization`, `Physician`, `MedicalCondition`, `MedicalWebPage`
  - **Nonprofit:** `NGO`, `DonateAction`, `Grant`, `FundingScheme`
  - **Government:** `GovernmentOrganization`, `GovernmentService`, `Legislation`
  - **Research:** `Dataset`, `DataCatalog`, `ScholarlyArticle`
- **Bands:** Fraction of expected types present and valid on the content that warrants them: 4 — ≥90%. 3 — 70–89%. 2 — 40–69%. 1 — 1–39%. 0 — none.
- **Note:** `Dataset` and `DataCatalog` are badly underused. Research organizations routinely hold catalogd data with no machine-readable description at all.
- **Fix:** Pick the applicable set deliberately. Do not implement types the content does not support.

### AR-5.6 — FAQPage and QAPage on genuine question-and-answer content

- **Weight:** 1.0 · **Applicability:** conditional — the site has real Q&A content · **Verification:** auto · **Effort:** small
- **Measures:** Correct markup on genuine Q&A — and the absence of fabricated FAQ blocks.
- **Detect:** Validate `FAQPage` and `QAPage` structure. Cross-check that every marked-up question appears in the visible page text. Flag pages where FAQ markup exists with no corresponding visible content.
- **Bands:** 4 — real Q&A marked up, nothing fabricated. 2 — partial coverage. 0 — absent where warranted. **Cap at 1 if fabricated FAQ blocks are detected**, regardless of coverage.
- **Fix:** Mark up what exists. Do not invent questions to earn the markup — it is a spam pattern and it degrades the page for people.

### AR-5.7 — HowTo on procedural content

- **Weight:** 1.0 · **Applicability:** conditional — the site has procedural content · **Verification:** auto · **Effort:** small
- **Measures:** Whether step sequences are explicit rather than inferred from prose.
- **Detect:** Identify procedural pages by ordered lists with step-like language in headings. Check for `HowTo` with ordered `HowToStep`.
- **Fix:** Application walkthroughs, enrollment steps, and permit processes are exactly what people ask assistants about.

### AR-5.8 — speakable on summaries and ledes

- **Weight:** 1.0 · **Applicability:** always · **Verification:** auto · **Effort:** small
- **Measures:** Whether the page flags its own best short answer.
- **Detect:** Extract `speakable` with `cssSelector` or `xpath`. Resolve the selector against the DOM and confirm it matches 1–3 elements containing 40–400 characters. Flag selectors that match the whole body or nothing.
- **Bands:** 4 — resolves to a genuine summary passage. 2 — present but resolves too broadly. 0 — absent.
- **Fix:** Point it at the lede, not the article.

### AR-5.9 — isAccessibleForFree, about, and mentions with entity references

- **Weight:** 1.0 · **Applicability:** always · **Verification:** auto · **Effort:** medium
- **Measures:** Access status and topical entity linkage.
- **Detect:** Check for `isAccessibleForFree` on substantive content. Check `about` and `mentions`, and classify their values as `@id` references or bare strings.
- **Bands:** 4 — access declared and subject entities referenced by identifier. 2 — properties present with string values only. 0 — absent.
- **Fix:** Entity references are how a model decides a page is *about* a subject rather than merely containing the word.

### AR-5.10 — Product, Offer, and Service with real prices

- **Weight:** 1.0 · **Applicability:** conditional — the site sells, charges, or offers something · **Verification:** auto · **Effort:** medium
- **Measures:** Whether commercial facts are marked up and current.
- **Detect:** Validate `Offer` structure. Compare `price`, `priceCurrency`, and `availability` against the visible page. Flag any mismatch and any `priceValidUntil` in the past.
- **Bands:** 4 — accurate and matching the page. 2 — present but drifting from visible values. 0 — absent, or demonstrably stale.
- **Note:** Stale pricing in schema is worse than none. A model will quote it with confidence.
- **Fix:** Source the schema value from the same field the page renders.

### AR-5.11 — SearchAction on the WebSite node

- **Weight:** 1.0 · **Applicability:** conditional — the site has search · **Verification:** auto · **Effort:** small
- **Measures:** Whether site search is exposed as a callable pattern.
- **Detect:** Extract `potentialAction` / `SearchAction` from the `WebSite` node. Execute the URL template with a test term and confirm real results come back.
- **Bands:** 4 — declared and the template returns results. 2 — declared but the template fails. 0 — absent.
- **Fix:** Small piece of the agent-interface story, and it costs almost nothing.

### AR-5.12 — Schema validation runs in CI

- **Weight:** 1.0 · **Applicability:** always · **Verification:** manual · **Effort:** medium
- **Measures:** Whether broken markup fails the build instead of reaching production.
- **Detect:** Not observable from outside. Attested by the build team, ideally with a link to the pipeline definition.
- **Bands:** 4 — validation gates merges on all covered content types. 2 — validation runs but does not block. 0 — none.
- **Fix:** This is the check that separates an engineering firm from an SEO boutique. Schema rots silently otherwise.

---

## Dimension 6 — Authorship, provenance, freshness

**Weight 10.0**

Trust signals, expressed as structured data rather than as vibes.

### AR-6.1 — Author entity pages with credentials

- **Weight:** 2.0 · **Applicability:** always · **Verification:** auto · **Effort:** medium
- **Measures:** Whether authors resolve to real, credentialed entities.
- **Detect:** Follow `author` references to their target pages. Require `Person` markup with a canonical `@id`, plus at least two of `jobTitle`, `affiliation`, `knowsAbout`, `sameAs`. Resolve `sameAs` (ORCID, LinkedIn, institutional profile).
- **Bands:** 4 — every recurring author has a resolvable entity page with verified identifiers. 3 — most do. 2 — author pages exist without markup or credentials. 1 — author names only. 0 — no author entities.
- **Fix:** A byline string asserts nothing. A resolvable entity with credentials does.

### AR-6.2 — Bylines linked to author entities

- **Weight:** 1.5 · **Applicability:** always · **Verification:** auto · **Effort:** small
- **Measures:** Whether substantive pages are signed, and whether the schema `author` is an `@id` reference rather than a repeated string.
- **Detect:** For each substantive page, check for a visible byline and an `author` property. Classify the value as `@id` reference or literal.
- **Bands:** 4 — visible byline plus `@id` reference on ≥95%. 3 — 75–94%. 2 — `author` present as a bare string. 1 — sparse. 0 — unsigned.
- **Fix:** Where the author is a department, name the department and model it as an entity.

### AR-6.3 — reviewedBy on YMYL content

- **Weight:** 1.0 · **Applicability:** conditional — the site publishes medical, legal, or financial content · **Verification:** auto · **Effort:** medium
- **Measures:** Whether expert review reaches the markup.
- **Detect:** On YMYL pages, check for `reviewedBy` referencing a credentialed `Person`, plus `lastReviewed` where the type supports it, plus a visible reviewer statement.
- **Fix:** Most healthcare and legal clients already run review workflows. The reviewer's identity simply never reaches the page.

### AR-6.4 — datePublished and dateModified are accurate

- **Weight:** 2.5 · **Applicability:** always · **Verification:** auto-partial · **Effort:** medium
- **Measures:** Whether the freshness signal is honest.
- **Detect:** Extract both dates from schema and visible markup. Flag: missing dates, `dateModified` before `datePublished`, future dates, and clustering that suggests a deploy stamp. Compare against sitemap `lastmod` (AR-2.5) and against the previous audit run to confirm dates only move when content moves.
- **Bands:** 4 — dates present, internally consistent, and stable across a deploy with no content change. 3 — present and consistent, stability unconfirmed. 2 — present but clustered or contradicting the sitemap. 1 — one date only. 0 — absent.
- **Fix:** Wire both to the node's own timestamps. A deploy must not move them.

### AR-6.5 — citation markup on primary-source references

- **Weight:** 0.5 · **Applicability:** conditional — the site publishes research or policy content · **Verification:** auto · **Effort:** medium
- **Measures:** Whether sourcing is machine-readable.
- **Detect:** Check for `citation` on research and policy pages. Resolve DOIs where present. Compare against the count of outbound links to primary sources in the body.
- **Fix:** Credibility that a parser cannot see does not count.

### AR-6.6 — C2PA Content Credentials on original assets

- **Weight:** 0.5 · **Applicability:** conditional — the client produces original photography, primary research, or documents where authenticity matters · **Verification:** auto · **Effort:** large
- **Measures:** Whether provenance survives into the delivered file.
- **Detect:** Download sampled original images and PDFs and read C2PA manifests. Confirm the manifest survives the image derivative pipeline — this is where it usually breaks.
- **Bands:** 4 — valid manifests on originals and derivatives. 2 — present on originals, stripped by the pipeline. 0 — absent.
- **Fix:** Scope this deliberately. It needs signing infrastructure.

### AR-6.7 — Editorial policy page referenced via publishingPrinciples

- **Weight:** 0.5 · **Applicability:** always · **Verification:** auto · **Effort:** small
- **Measures:** Whether the site documents how content is produced, reviewed, corrected, and funded.
- **Detect:** Look for `publishingPrinciples` on the `Organization` node and resolve it. For publishers, also check `correctionsPolicy` and `diversityPolicy`.
- **Fix:** Write the page that describes the process the client already follows.

### AR-6.8 — About, Contact, and organizational detail are complete

- **Weight:** 1.5 · **Applicability:** always · **Verification:** auto · **Effort:** medium
- **Measures:** Whether a model can resolve who this organization actually is.
- **Detect:** Locate About and Contact pages. Score presence of: founding date, leadership names, postal address, service area, `contactPoint` entries differentiated by purpose, and public legal identifiers (EIN, ROR, DUNS). Check both the visible page and the `Organization` node.
- **Bands:** Fraction of those seven facts present in both HTML and schema: 4 — ≥6. 3 — 5. 2 — 3–4. 1 — 1–2. 0 — none.
- **Fix:** Thin About pages are a persistent weakness for nonprofits and institutions. Do not leave the key facts in an annual-report PDF.

---

## Dimension 7 — Agent interfaces

**Weight 5.0**

The move from "a site agents can read" to "a site agents can use." Low weight, high differentiation — most sites score zero here, and that is currently fine. Treat this dimension as an opportunity map rather than a deficiency list.

### AR-7.1 — NLWeb endpoint over the site's schema.org data

- **Weight:** 0.5 · **Applicability:** always · **Verification:** auto · **Effort:** large
- **Detect:** Probe known NLWeb paths and `/.well-known/` entries. If found, send a representative question and check for a schema.org-formatted JSON response.
- **Bands:** 4 — endpoint answers correctly and is grounded in site content. 2 — endpoint responds but answers poorly. 0 — absent.

### AR-7.2 — NLWeb exposed as an MCP server with an ask method

- **Weight:** 0.5 · **Applicability:** conditional — requires AR-7.1 · **Verification:** auto · **Effort:** medium
- **Detect:** Attempt an MCP handshake. Confirm the `ask` method is discoverable and returns correct answers.

### AR-7.3 — Domain MCP server over real content APIs

- **Weight:** 1.0 · **Applicability:** always · **Verification:** auto-partial · **Effort:** large
- **Measures:** Whether the client's structured data — course catalog, provider directory, grant database, program finder — is callable as documented tools.
- **Detect:** Discover from `/.well-known/`. Enumerate tools, check each has a description and typed parameters, and call one read-only tool against live data.
- **Bands:** 4 — documented, typed tools returning live data, with auth and rate limiting. 3 — tools present but thinly documented. 2 — a generic query endpoint rather than narrow tools. 0 — none.
- **Note:** The most defensible item in the whole Index. A well-described tool with three parameters beats a generic endpoint.

### AR-7.4 — OpenAPI specification for public APIs

- **Weight:** 0.25 · **Applicability:** conditional — the site has a public or semi-public API · **Verification:** auto · **Effort:** small
- **Detect:** Look for the spec at conventional paths and from `/.well-known/`. Validate it, and confirm it is versioned and linked from the site.

### AR-7.5 — /.well-known/ discovery entries for agent capabilities

- **Weight:** 0.25 · **Applicability:** conditional — the site exposes any agent capability · **Verification:** auto · **Effort:** small
- **Detect:** Enumerate `/.well-known/` for MCP, NLWeb, OpenAPI, and `http-message-signatures-directory`. Validate each returns parseable metadata.

### AR-7.6 — Form fields carry label, name, and autocomplete

- **Weight:** 1.0 · **Applicability:** conditional — the site has public forms · **Verification:** auto · **Effort:** medium
- **Measures:** Whether an agent can tell what each field is for.
- **Detect:** For every public form field: a `<label for>` or equivalent accessible name, a meaningful `name` attribute (reject `field_1`, `input3`), and a valid `autocomplete` token where one applies (`given-name`, `email`, `postal-code`, `tel`). A token *applies* where the field type implies a purpose — `email`, `tel`, `url`, `password`. A search box or a free-text field has no sensible token and is not marked down for lacking one.
- **Bands:** Fraction of fields passing all three: 4 — ≥95%. 3 — 75–94%. 2 — 40–74%. 1 — 1–39%. 0 — none.
- **Fix:** Same change satisfies WCAG. Easy joint justification.

### AR-7.7 — No captchas on browse, search, or filter interactions

- **Weight:** 0.5 · **Applicability:** always · **Verification:** auto · **Effort:** small
- **Detect:** Submit search and filter forms and check for challenge responses. Complements AR-1.3, from the application side rather than the edge.

### AR-7.8 — Critical flows complete without JS-only interactions

- **Weight:** 0.5 · **Applicability:** conditional — the client names critical flows · **Verification:** auto-partial · **Effort:** large
- **Measures:** Whether register, donate, apply, or find-a-clinician can be completed through standard form submissions and real URL transitions.
- **Detect:** For each named flow, walk it with JavaScript disabled. Flag `div`-based controls with click handlers, wizards with no addressable step URLs, and modal-only paths.
- **Bands:** 4 — every flow completes with real URLs at each step. 2 — flows start but cannot complete. 0 — flows are JS-only.
- **Fix:** Run this as a joint engineering and UX audit.

### AR-7.9 — Stable selectors on critical-flow elements

- **Weight:** 0.5 · **Applicability:** conditional — requires AR-7.8 flows to be named · **Verification:** auto · **Effort:** medium
- **Detect:** Collect selectors for the key elements in each flow. Classify as stable (`id`, `data-*`, semantic) or fragile (hashed utility classes, build-generated IDs). Diff across two runs to detect churn.
- **Bands:** 4 — stable and unchanged across runs. 2 — mixed. 0 — hashed classes only.
- **First run:** cap at 2 without a baseline run, since band 4 is defined entirely by cross-run stability. See *Checks that need a previous run*.
- **Fix:** Treat these selectors as a contract that survives redeploys.

---

## Dimension 8 — Measurement and instrumentation

**Weight 3.0**

The proof layer. Commercially, this is the most valuable work in the whole checklist — and almost none of it is visible from outside a site, which is why it carries three points. Do not read a low score here as unimportant. Read it as unmeasurable by scanner.

### AR-8.1 — GA4 channel group for AI assistant referrers

- **Weight:** 0.5 · **Applicability:** always · **Verification:** manual · **Effort:** small
- **Measures:** Whether the client can see traffic from `chatgpt.com`, `perplexity.ai`, `claude.ai`, `copilot.microsoft.com`, and `gemini.google.com`. By default it lands in Direct and disappears.
- **Detect:** Analytics presence is detectable from the page. The channel group is not — attest it, ideally with a screenshot.
- **Bands:** 4 — group live with a saved report. 2 — analytics present, no grouping. 0 — no analytics.

### AR-8.2 — Server-side tagging captures stripped referrers

- **Weight:** 0.5 · **Applicability:** always · **Verification:** manual · **Effort:** medium
- **Detect:** A server-side endpoint may be inferable from network requests. Reconciliation against client-side numbers must be attested.

### AR-8.3 — Access log retention with a queryable store

- **Weight:** 0.5 · **Applicability:** always · **Verification:** manual · **Effort:** medium
- **Measures:** Whether crawler logs survive long enough to show a before-and-after window. Many hosts discard them in days.
- **Bands:** 4 — ≥12 months retained and queryable by user-agent, path, status, date. 2 — retained but not queryable. 0 — discarded.

### AR-8.4 — Scheduled AI crawler activity report

- **Weight:** 0.5 · **Applicability:** conditional — requires AR-8.3 · **Verification:** manual · **Effort:** medium
- **Measures:** Whether crawler behavior is monitored continuously rather than audited once. A crawler that stops appearing usually means somebody reintroduced a block.

### AR-8.5 — Fixed prompt panel run monthly across models

- **Weight:** 0.5 · **Applicability:** always · **Verification:** manual · **Effort:** medium
- **Measures:** Whether answer-share is tracked on a stable panel of brand, category, and comparison prompts.
- **Bands:** 4 — fixed panel, baseline stored, monthly runs comparable. 2 — ad-hoc checks. 0 — none.
- **Note:** The panel has to stay fixed. Tuning prompts between runs destroys the trend line.

### AR-8.6 — Extraction-fidelity baseline captured

- **Weight:** 0.25 · **Applicability:** always · **Verification:** manual · **Effort:** medium
- **Measures:** Whether a before-and-after fidelity score exists — feed the page to a model, ask it the client's key questions, score the accuracy.
- **Note:** The scanner can generate this itself in a later version. Until then, attest it. The before-and-after delta is the best single-slide value demonstration we have.

### AR-8.7 — Search Console and Bing Webmaster Tools verified

- **Weight:** 0.25 · **Applicability:** always · **Verification:** auto-partial · **Effort:** small
- **Detect:** Look for `google-site-verification` and `msvalidate.01` meta tags, or DNS TXT verification records. Whether anyone reads the alerts must be attested.
- **Bands:** 4 — both verified, sitemaps submitted, alerts routed to a person. 3 — both verified; submission and alert routing unconfirmed. 2 — one verified. 0 — neither.
- **Note:** Bing matters more than its search share suggests. Its index feeds several AI products, and clients frequently have Search Console with no Bing property at all.

---

## What 100 means

A site scoring 100 has, at minimum:

- Every AI crawler reaching real content at the edge and in `robots.txt`, with a deliberate, documented licensing position.
- A clean, finite URL space with honest freshness signals.
- Every substantive fact present in the server response, in semantic markup, with stable anchors a model can cite.
- Generated `llms.txt` and Markdown companions, cached at the edge.
- One linked entity graph, anchored to external authorities, validated in CI so it cannot rot.
- Credentialed authors, honest timestamps, machine-readable sourcing.
- Documented agent endpoints and flows an agent can actually complete.
- Instrumentation that proves all of it, running on a schedule.

Almost no site will score 100, and that is the point. The Index is a target to move toward, not a bar to clear. A client who moves from 41 to 72 in one engagement has a number they can take to their board — which is worth considerably more to them than a perfect score they will never reach.

## What the Index deliberately does not measure

We keep this list public because leaving it out would be dishonest.

- **Whether the content is any good.** Structure, not substance.
- **Whether the models actually cite the site.** That is answer-share monitoring (AR-8.5), a different instrument on a different cadence.
- **Traditional SEO performance.** Rankings, backlinks, and Core Web Vitals are real, and they are not this.
- **Accessibility conformance.** Several checks overlap with WCAG, happily. This is not a WCAG audit and must not be sold as one.
- **Security posture.** We probe bot handling at the edge. We do not test the site's defenses.

---

## Versioning and change control

The Index version is independent of the scanner version. Both appear in every result file.

Bump the **minor** version for new checks, clarified detection, or corrected band anchors. Bump the **major** version for any weight change, gate change, or scoring-model change — anything that makes two scores non-comparable.

Never silently re-weight. A client's trend line is the most valuable thing the Index produces, and one quiet weight change destroys it. When a major bump lands, re-score the last run under both versions and show the difference.

### Changelog

#### 2.0 — 2026-09-01

**A major bump, and the versioning rule applies in full.** The one-time exemption
taken at 1.1 is spent. Any site scored under 1.x must be re-scored under 2.0 before
its number is compared to anything.

1. **AR-1.1's bands are anchored on agent class rather than agent count**, and the
   gate now turns on the answer-serving class. Blocking every training crawler costs 2
   of 5 points and does not gate; blocking one answer-serving agent does. Under 1.x the
   table could not tell `PerplexityBot` from `CCBot` — both scored band 3 — which meant
   a site invisible to an answer engine and a site excluded from a corpus were graded
   identically. **This changes which sites are gated, which is why the version is 2.0.**
2. **`critical` severity widens to band 0 or 1** on checks weighing 3.0 or more. A
   6-point check at band 1 is not a `major` finding sitting alongside a missing table
   caption. Ordering and points are unchanged; only the label moves.
3. **AR-2.8, AR-5.1 and AR-8.7 gain a band 3**, and their unattested ceilings rise from
   2 to 3 to match. Their tables previously skipped 3, so with a ceiling at 2 the whole
   observable range was `{0, 2}` and there was no way to say *mostly right*.

#### 1.2 — 2026-09-01

Refinements found while implementing all 68 checks. None changes a weight or a gate.
Two change how a band is reached and are recorded here so a re-score is explicable.

1. **`@id` coverage in AR-5.2 counts entity nodes only.** A `ListItem` inside a
   `BreadcrumbList`, a `PostalAddress` on an `Organization` and a `SearchAction`'s
   `EntryPoint` are value objects: they have no identity and nothing should reference
   them. Counting them penalized a site for marking breadcrumbs up correctly.
2. **AR-5.3 resolution is opt-in.** Band 4 requires `sameAs` links to resolve, which
   means requesting Wikidata, ROR and LinkedIn — hosts the client never allowlisted,
   and hosts that learn which site is being audited. The scanner classifies by
   authority tier from the markup alone and declares an observed ceiling of band 3
   until told to fetch.
3. **AR-7.6 expects an `autocomplete` token only where the field type implies one**
   (`email`, `tel`, `url`, `password`). A search box has no sensible token and must
   not be marked down for lacking one.
4. **"Widespread" in AR-2.2 needs a count as well as a share.** One three-hop chain on
   a two-page site is a finding, not a systemic failure.
5. **Every band ceiling now states its reason** in the result and in the report:
   awaiting a baseline run, awaiting confirmation from the client, limited by the band
   attested, or needing review by a person. A client told a number was held down is
   owed the reason.
6. **AR-2.8's detection method is weaker than this document implied.** The IndexNow key
   file is named after the key, and the key is not published, so a third party can only
   find it where the site advertises it. A negative result is *not discoverable*, which
   is weaker than *absent*, and the check says so.
7. **AR-6.6 is not implemented.** Reading C2PA manifests means parsing image and PDF
   bytes. The check reports that it had no evidence and leaves the denominator rather
   than scoring a site zero for a measurement nobody took.

#### 1.1 — 2026-09-01

Twelve gaps found while building the first scanner against 1.0. Several of these would ordinarily force a major bump, because they change how a score is computed. They are landing as a minor bump because **no site has ever been scored under 1.0** — the scanner did not exist yet, so there is no trend line to break. This exemption applies once. Every change after this one follows the rule above.

1. **Automated coverage** now counts `auto-partial` at half weight. Under the previous wording the metric could not exceed 0.83, while this document's own example result showed 0.87 — arithmetically unreachable.
2. **Attestations may lift an `auto-partial` band ceiling**, never set or lower a band. Four checks (AR-2.8, AR-4.1, AR-5.1, AR-8.7) had a top band this document itself says requires attestation, and no mechanism to supply one.
3. **Cross-run checks cap at band 3 on a first run** (AR-7.9 at 2) and lift with a supplied baseline. AR-2.5, AR-3.6 and AR-7.9 previously required a second audit with no definition of what run one does.
4. **The ten AI agents are sorted into three classes.** AR-1.1's bands referenced categories the document never defined, and AR-1.2 asked us to probe two user-agents that no crawler ever sends.
5. **Money-page double weighting is defined** as counting 2 in both sides of the coverage rubric. Previously asserted and never operationalized.
6. **A gated run grades as `Gated`** and gains an `ungated_grade` field. The bands table offered two labels for the same run.
7. **"Requires AR-X.Y to pass" means band ≥ 2**, with N/A cascading to dependents. Previously undefined against a 0–4 scale.
8. **Business-fact applicability comes from a declared engagement profile**, never inference. Undeclared scores N/A with a reason.
9. **A 25-URL representation sub-sample** bounds AR-1.9, AR-4.3, AR-4.5 and AR-4.6, which together would otherwise triple the request count against a client's origin.
10. `confidence` is an enum: `high`, `medium`, `low`.
11. AR-3.1 coverage clamps to 1.0 and excludes empty renders; a missing `robots.txt` scores AR-1.1 band 4; `score_pct` is `null` where nothing applies; backlog ties break on check ID; `site.cms` is best-effort and nullable.
12. **Rounding happens once**, and the file records that per-check points will not sum exactly to the total.

---

**68 checks · 100.0 points**
