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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 2.1.0 (2026-06-03)

### Added
- **Stable finding codes.** Every `AuditFinding` now carries a `code` namespaced as `<factor-id>.<check>[.<variant>]` (e.g. `technical-seo.h1.multiple`, `schema-validity.singleton.duplicate`), so agents and integrations key on a stable machine identifier instead of regex-matching the human `message` (which can change between releases). 212 codes across all 19 analyzers; the full registry is in [docs/finding-codes.md](docs/finding-codes.md). Codes follow a documented convention and are unique across the tool (enforced by a test). `AuditFinding.code` is required, so the compiler guarantees no finding ships without one.
- `hasMissingMetaDescription` (the `--require-meta` gate) now keys on `technical-seo.meta-description.missing` rather than a message prefix — the first consumer migrated to codes.

### Changed
- **`schemaVersion` bumped to `1.1`** (additive: findings gained the `code` field). Report shapes are otherwise unchanged.

## 2.0.0 (2026-06-03)

### Breaking
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
- Audit **built HTML offline** in CI: a `next export` / `dist` / `out` directory, no network. [Static output](docs/cli.md#static-output-mode)
- Detect the **platform / CMS / framework**: WordPress, Webflow, Shopify, Next.js, Vercel. [Platform detection](docs/cli.md#platform-detection)
- Opt in to **Lighthouse, geographic, and agent-skill** factors. [Optional factors](docs/scoring.md#optional-factors)
- `text`, `json`, and `markdown` output with **CI-friendly exit codes**. [CLI reference](docs/cli.md)
- `text`, `json`, `markdown`, and `agent` output with **CI-friendly exit codes**. [CLI reference](docs/cli.md)
- **Agent-native output**: a versioned `schemaVersion`, a slim `--format agent` decision, ranked structured fixes, and stable [finding codes](docs/finding-codes.md) so integrations key on codes, not prose. [API](docs/api.md#machine-readable-output-for-ai-agents)
- Use as a **library** ([API](docs/api.md)) or from Claude Code via the **`/aeo` skill** ([skill](docs/skill.md)).

Website: [canonry.ai](https://canonry.ai)
Expand Down
4 changes: 2 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const report = await runSitemapAudit('https://example.com', {
factors: ['schema-validity', 'structured-data'], // Optional subset
})

console.log(report.schemaVersion) // '1.0', JSON shape version (see "Machine-readable output")
console.log(report.schemaVersion) // '1.1', JSON shape version (see "Machine-readable output")
console.log(report.aggregateGrade) // 'B+'
console.log(report.pagesAudited) // 22
console.log(report.criticalDefects) // Binary per-page defects (multiple/missing H1, missing title/meta), grouped by defect
Expand All @@ -51,7 +51,7 @@ Each entry in `crossCuttingIssues[].topIssues` carries a `recommendation` plus t

- **`schemaVersion`** (on `AuditReport` and `SitemapAuditReport`, exported as `SCHEMA_VERSION`) versions the JSON shape independently of the npm version. Pin to it and treat a major bump as breaking; treat its absence as a pre-2.0 report.
- **`prioritizedFixes: PrioritizedFix[]`** is the ranked, pre-computed to-do list, so an agent need not average factor scores and re-rank. Each fix carries a stable `id` (a defect id like `"multiple-h1"` or a factor id like `"technical-seo"`), `kind`, an optional `severity`, the complete `affectedPages` array (never truncated), `affectsHomepage`, `prevalencePct`, and a human `summary`.
- **Stable identifiers** on the decision surface (`criticalDefects[].id`, `prioritizedFixes[].id` / `kind`) let integrations key on codes, not on matching message strings.
- **Stable identifiers** everywhere: the decision surface (`criticalDefects[].id`, `prioritizedFixes[].id` / `kind`) and every individual factor finding (`factors[].findings[].code`, e.g. `technical-seo.h1.multiple`) carry stable codes, so integrations key on codes, not on matching message strings. The full code registry is in [finding-codes.md](finding-codes.md).

## Static output (offline, from disk)

Expand Down
278 changes: 278 additions & 0 deletions docs/finding-codes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
# Finding codes

Every `AuditFinding` carries a stable `code` so integrations can key on a machine identifier instead of matching the human `message` string (which may change between releases).

## Convention

`<factor-id>.<check>[.<variant>]` — lowercase kebab-case, dot-separated. `<check>` names the sub-check (e.g. `h1`, `meta-description`); `<variant>` distinguishes the outcomes of one check (e.g. `missing`, `multiple`, `single`). All branches of one check share the `<check>` segment. Codes are stable across releases and unique across the tool.

## Registry

### Structured Data (JSON-LD)

- `structured-data.json-ld.found`
- `structured-data.json-ld.missing`
- `structured-data.schema.found`
- `structured-data.schema.missing`
- `structured-data.schema-depth.strong`
- `structured-data.schema-depth.moderate`
- `structured-data.schema-depth.low`

### Content Depth

- `content-depth.word-count.strong`
- `content-depth.word-count.moderate`
- `content-depth.word-count.low`
- `content-depth.h1.single`
- `content-depth.h1.multiple`
- `content-depth.h1.missing`
- `content-depth.headings.strong`
- `content-depth.headings.moderate`
- `content-depth.headings.low`
- `content-depth.paragraphs.strong`
- `content-depth.paragraphs.moderate`
- `content-depth.paragraphs.low`
- `content-depth.lists.present`
- `content-depth.lists.none`

### AI-Readable Content

- `ai-readable-content.content-negotiation.found`
- `ai-readable-content.aux-resource.missing`
- `ai-readable-content.aux-resource.timeout`
- `ai-readable-content.aux-resource.unreachable`
- `ai-readable-content.aux-resource.not-html`
- `ai-readable-content.aux-resource.found`
- `ai-readable-content.llms-txt.strong`
- `ai-readable-content.llms-txt.short`
- `ai-readable-content.llms-full-txt.strong`
- `ai-readable-content.llms-full-txt.short`
- `ai-readable-content.robots-txt.found`
- `ai-readable-content.robots-txt.unreachable`
- `ai-readable-content.robots-txt.missing`
- `ai-readable-content.sitemap.found`
- `ai-readable-content.sitemap.unreachable`
- `ai-readable-content.sitemap.missing`
- `ai-readable-content.llms-txt-link.found`
- `ai-readable-content.llms-txt-link.missing`
- `ai-readable-content.markdown-endpoint.found`
- `ai-readable-content.markdown-endpoint.missing`

### E-E-A-T Signals

- `eeat-signals.author.credentialed`
- `eeat-signals.author.no-credentials`
- `eeat-signals.author.missing`
- `eeat-signals.author-meta.found`
- `eeat-signals.author-meta.missing`
- `eeat-signals.review.found`
- `eeat-signals.review.missing`
- `eeat-signals.trust-links.strong`
- `eeat-signals.trust-links.partial`
- `eeat-signals.trust-links.missing`
- `eeat-signals.organization.with-people`
- `eeat-signals.organization.no-people`
- `eeat-signals.organization.missing`

### FAQ Content

- `faq-content.faqpage.present`
- `faq-content.faqpage.missing`
- `faq-content.details.multiple`
- `faq-content.details.single`
- `faq-content.details.none`
- `faq-content.headings.multiple`
- `faq-content.headings.low`
- `faq-content.headings.missing`
- `faq-content.qa-pairs.multiple`
- `faq-content.qa-pairs.low`
- `faq-content.qa-pairs.none`

### Citations & Authority Signals

- `citations.external-links.strong`
- `citations.external-links.moderate`
- `citations.external-links.low`
- `citations.authoritative-domains.found`
- `citations.authoritative-domains.none`
- `citations.sameas.strong`
- `citations.sameas.moderate`
- `citations.sameas.missing`
- `citations.anchor-text.strong`
- `citations.anchor-text.moderate`
- `citations.anchor-text.low`

### Schema Completeness

- `schema-completeness.schema.none`
- `schema-completeness.local-business.strong`
- `schema-completeness.local-business.partial`
- `schema-completeness.local-business.low`
- `schema-completeness.faqpage.strong`
- `schema-completeness.faqpage.partial`
- `schema-completeness.faqpage.low`
- `schema-completeness.howto.strong`
- `schema-completeness.howto.partial`
- `schema-completeness.organization.strong`
- `schema-completeness.organization.partial`
- `schema-completeness.organization.low`
- `schema-completeness.schema-depth.moderate`
- `schema-completeness.schema-depth.low`

### Schema Validity

- `schema-validity.json-ld.none`
- `schema-validity.block.empty`
- `schema-validity.block.invalid`
- `schema-validity.singleton.duplicate`
- `schema-validity.block.valid`

### Entity Consistency

- `entity-consistency.name.missing`
- `entity-consistency.name.single`
- `entity-consistency.name.moderate`
- `entity-consistency.name.multiple`
- `entity-consistency.title.ok`
- `entity-consistency.title.long`
- `entity-consistency.canonical.present`
- `entity-consistency.canonical.missing`
- `entity-consistency.contact.ok`
- `entity-consistency.contact.partial`
- `entity-consistency.contact.missing`

### Content Freshness

- `content-freshness.date-modified.recent`
- `content-freshness.date-modified.moderate`
- `content-freshness.date-modified.stale`
- `content-freshness.date-modified.missing`
- `content-freshness.last-modified.recent`
- `content-freshness.last-modified.older`
- `content-freshness.last-modified.missing`
- `content-freshness.sitemap.recent`
- `content-freshness.sitemap.stale`
- `content-freshness.sitemap.no-match`
- `content-freshness.sitemap.timeout`
- `content-freshness.sitemap.unreachable`
- `content-freshness.sitemap.missing`
- `content-freshness.copyright.recent`
- `content-freshness.copyright.older`
- `content-freshness.copyright.missing`

### Content Extractability

- `content-extractability.content-ratio.strong`
- `content-extractability.content-ratio.moderate`
- `content-extractability.content-ratio.low`
- `content-extractability.citable-blocks.strong`
- `content-extractability.citable-blocks.moderate`
- `content-extractability.citable-blocks.missing`
- `content-extractability.paywall.found`
- `content-extractability.paywall.none`
- `content-extractability.ad-density.high`
- `content-extractability.ad-density.low`
- `content-extractability.ad-density.none`
- `content-extractability.direct-answer.strong`
- `content-extractability.direct-answer.moderate`
- `content-extractability.direct-answer.none`

### Definition Blocks

- `definition-blocks.headings.multiple`
- `definition-blocks.headings.single`
- `definition-blocks.headings.missing`
- `definition-blocks.lists.found`
- `definition-blocks.lists.none`
- `definition-blocks.schema.found`
- `definition-blocks.schema.missing`
- `definition-blocks.dl.found`
- `definition-blocks.dl.none`

### AI Crawler Access

- `ai-crawler-access.robots-txt.missing`
- `ai-crawler-access.robots-txt.unreachable`
- `ai-crawler-access.crawler.allowed`
- `ai-crawler-access.crawler.blocked`
- `ai-crawler-access.sitemap.found`
- `ai-crawler-access.content-signal.found`

### Named Entities

- `named-entities.brand-name.strong`
- `named-entities.brand-name.low`
- `named-entities.brand-name.missing`
- `named-entities.entity-name.missing`
- `named-entities.knows-about.present`
- `named-entities.knows-about.missing`
- `named-entities.proper-noun-density.strong`
- `named-entities.proper-noun-density.moderate`
- `named-entities.proper-noun-density.low`

### Technical SEO

- `technical-seo.h1.single`
- `technical-seo.h1.missing`
- `technical-seo.h1.multiple`
- `technical-seo.alt-text.none`
- `technical-seo.alt-text.ok`
- `technical-seo.alt-text.missing`
- `technical-seo.alt-text.empty`
- `technical-seo.meta-description.missing`
- `technical-seo.meta-description.short`
- `technical-seo.meta-description.long`
- `technical-seo.meta-description.present`
- `technical-seo.canonical.missing`
- `technical-seo.canonical.present`

### Snippet Eligibility

- `snippet-eligibility.directives.none`
- `snippet-eligibility.noindex.present`
- `snippet-eligibility.nosnippet.present`
- `snippet-eligibility.max-snippet.zero`
- `snippet-eligibility.max-snippet.low`
- `snippet-eligibility.noarchive.present`
- `snippet-eligibility.noimageindex.present`
- `snippet-eligibility.directives.not-restrictive`

### Geographic Signals (optional)

- `geographic-signals.localbusiness-schema.found`
- `geographic-signals.localbusiness-schema.missing`
- `geographic-signals.geo-coordinates.found`
- `geographic-signals.geo-coordinates.missing`
- `geographic-signals.postal-address.found`
- `geographic-signals.postal-address.missing`
- `geographic-signals.area-served.found`
- `geographic-signals.area-served.missing`
- `geographic-signals.geo-meta.found`
- `geographic-signals.geo-meta.missing`
- `geographic-signals.visible-location.found`
- `geographic-signals.visible-location.missing`

### Agent Skill Exposure (optional)

- `agent-skill-exposure.schema-action.well-formed`
- `agent-skill-exposure.schema-action.partial`
- `agent-skill-exposure.schema-action.missing`
- `agent-skill-exposure.mcp-discovery.found`
- `agent-skill-exposure.mcp-discovery.missing`
- `agent-skill-exposure.a2a-agent-card.found`
- `agent-skill-exposure.a2a-agent-card.missing`
- `agent-skill-exposure.openapi.found`
- `agent-skill-exposure.openapi.missing`
- `agent-skill-exposure.microdata.found`
- `agent-skill-exposure.microdata.missing`
- `agent-skill-exposure.forms.none`
- `agent-skill-exposure.forms.strong`
- `agent-skill-exposure.forms.partial`
- `agent-skill-exposure.forms.weak`

### Lighthouse (optional)

- `lighthouse.psi.unreachable`
- `lighthouse.category.missing`
- `lighthouse.category.score`
- `lighthouse.category.none`
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ainyc/aeo-audit",
"version": "2.0.0",
"version": "2.1.0",
"description": "The most comprehensive open-source Answer Engine Optimization (AEO) audit tool. Scores websites across 16 ranking factors that determine AI citation.",
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion skills/aeo/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Returns:
Use `--format json` for the full report, or **`--format agent`** for just the decision: `{ schemaVersion, tool, mode, url, score, grade, pass, criticalDefectCount, issues }`, where `issues` is the ranked `prioritizedFixes` and the per-factor/per-page detail is omitted. Prefer `--format agent` when you only need to decide and act. Key fields for acting on the result without parsing prose:
- `schemaVersion` (on every audit report) versions the JSON shape independently of the package version — pin to it and treat a major bump as breaking; absence means a pre-2.0 report.
- `prioritizedFixes` is a ranked array of objects, each with a stable `id`, `kind`, optional `severity`, the complete `affectedPages` list (never truncated), `affectsHomepage`, `prevalencePct`, and a human `summary`. It's the pre-computed to-do list — no need to re-rank factor scores yourself.
- Stable identifiers (`criticalDefects[].id`, `prioritizedFixes[].id`) let integrations key on codes rather than message strings.
- Stable identifiers everywhere — `criticalDefects[].id`, `prioritizedFixes[].id`, and every factor finding's `code` (e.g. `technical-seo.h1.multiple`) — let integrations key on codes rather than message strings.

#### Auxiliary File Diagnostics

Expand Down
Loading