# Changelog All notable changes to `angeo/module-aeo-audit` will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [4.2.3] — 2026-09-16 > Patch release. Build, packaging and code-quality fixes found while > preparing the module for the Mage-OS Extension Directory. No change to > signals, weights or scoring. ### Fixed - **Audit result page could fatal.** `AuditResult\View` called `setAuditResult()` with the nullsafe operator, but `getBlock()` returns `false`, not `null`, when the block is missing. The block type is now checked. - **Unescaped output in admin templates** (Magento coding standard errors): `score_trend.phtml` and `auditresult/view.phtml`. Values were already numbers or fixed colours; they now go through the escaper or an `(int)` cast. - CMS page sampling passes the store ID to `addStoreFilter()` instead of the store object. - Collection filters use `['eq' => …]`. Same SQL. - `HttpCache`: removed a `method_exists()` check that was always true. ### Changed - **PHP 8.1–8.5** (was 8.2–8.4). The code uses no PHP 8.2+ syntax or functions; CI now runs on 8.1 too. - Dev tooling updated to PHPUnit 10.5, PHPStan 2, `bitexpert/phpstan-magento` and `magento/magento-coding-standard` ^40 || ^41. `phpunit.xml` uses the 10.5 `` element. - `phpstan.neon` rewritten to the suite-wide config (level 5 with `bitexpert/phpstan-magento`). The old config ignored every undefined Magento method and every invalid Magento type. The extension generates Magento factory classes during analysis, so no stub files are needed. - CI (`.github/workflows/ci.yml`) now matches the rest of the suite: one job per PHP version 8.1–8.5 running composer validate, lint, Magento coding standard and PHPUnit, plus one PHPStan job on PHP 8.2. The Mage-OS installability job is kept. ### Build - Unit tests run against real Magento classes: Store mocks use `Magento\Store\Model\Store` (for `getBaseUrl()`), generated factories are declared by `Test/Unit/bootstrap.php`, and the agents.md "missing policies" fixture is long enough not to count as a placeholder. ### Documentation - README: one badge row across the suite — CI, Packagist version and downloads, PHP 8.1 – 8.5, supported Magento range, Mage-OS Extension Directory, license. ### Quality - Magento2 coding standard: 0 errors. PHPStan level 5: no errors. ## [4.2.0] — 2026-09-08 > Minor release. Brings the llms.txt signal up to **llmstxt.org v2** > (10 August 2026) and adds two signals for the discovery layer that v2 and > Shopify's rollout turned into an expectation. No breaking changes; upgrading > from 4.1.0 is drop-in (`composer update`, then `bin/magento setup:upgrade && > bin/magento setup:di:compile`). ### Added - **`link_relations` signal — llms.txt v2 link relations (weight 0.7).** v1 said what an llms.txt file should contain; v2 added how an agent finds the markdown version of a page and the llms.txt covering it without guessing URLs. The checker samples a product page and looks for `rel="alternate" type="text/markdown"` and `rel="describedby"` — in the HTML `` or in the HTTP `Link:` header, both of which the spec accepts. It then follows them. A relation that points at a 404 fails the check: declaring a markdown twin that does not exist is worse than declaring nothing, because the agent follows it and fails. It also checks the `Link:` header on the mirror itself — markdown has no `` to carry the relation — and probes both URL forms v2 permits (`page.html.md` and `page.md`), reporting a missing second form as a warning, since serving one is compliant. - **`agents_md` signal — /agents.md (weight 0.7).** Distinct from the `agent_card` signal: `/.well-known/agent-card.json` is a machine-readable A2A capability descriptor, `agents.md` is prose an LLM reads to learn how the shop expects to be dealt with. Shopify rolled it out across its stores and then made it canonical, with llms.txt pointing at it. The check is about consequence, not length: whether an agent about to recommend or transact can find the delivery, returns and privacy terms it must quote. A file with none of them warns — an agent left to summarise policy from memory is how wrong refund windows reach shoppers. Also flags a file served as `text/html` (usually a CMS page shadowing the real file) and a missing `/sitemap_agentic_discovery.xml`. ### Changed - **`llms_txt` signal aligned with v2.** - A bare `> ` is now an issue. An empty blockquote reads to a parser as the spec's summary and carries nothing. Previously it was counted as a valid description. - A heading between the summary and the first H2 is now an issue. v2 allows markdown of any type in that gap **except** headings. - Warns when links point at HTML pages while the store demonstrably serves markdown mirrors — v2 asks that llms.txt links lead to LLM-friendly content. Only raised where a mirror actually resolves, so a store that has not adopted them is never nagged. - The over-size message no longer claims `llms-full.txt` is required by the spec. It is a Mintlify convention. The spec's own answer to a large file is fewer links with more detail behind them. - `## Optional` is neither required nor rewarded: v2 removed its mechanical meaning, so it no longer tells any tool what to drop. ### Notes - Both new signals are module-independent. A store that hand-writes the tags and serves markdown from nginx passes identically to one running `angeo/module-llms-txt`; the fix command is a suggestion shown on failure, not a precondition for passing. - Both are `technical` category, so they run under `bin/magento angeo:aeo:audit -c technical` and add no external API cost. > Minor release. Adds an **A2A Agent Card** signal, refreshes the ACP framing of > the product-feed signal, and teaches the UCP checker the difference between a > superseded revision and an unknown one. No breaking changes; upgrading from > 4.0.0 is drop-in (`composer update`, then `bin/magento setup:upgrade && > bin/magento setup:di:compile`). ### Added - **`agent_card` signal — A2A Agent Card (weight 0.6).** Checks `/.well-known/agent-card.json`. A2A reached 1.0.0 under Linux Foundation governance, and UCP 2026-04-08 lists `a2a` alongside `rest`, `mcp` and `embedded` as a transport a business may advertise — pointing at exactly this path. The signal is **conditional by design**, so it does not penalise the large majority of stores that have never opted into A2A: - UCP profile declares an `a2a` transport → the card is required. Missing, or served only at the legacy path → **FAIL**. - No `a2a` transport declared → the card is optional and its absence is a **PASS** carrying `applicable: false`. The defect it exists to catch is the path migration. Before A2A 0.3 the card lived at `/.well-known/agent.json`, and a large share of published cards are still there — where a spec-compliant 1.0.0 client never looks. A card at the legacy path only is flagged whether or not `a2a` is declared, because it is a live misconfiguration either way. Cards served at *both* paths get a WARN: two copies drift apart. Structural validation covers the fields A2A requires (`name`, `url`, `version`) and the ones that make a card useful rather than merely discoverable (`description`, `capabilities`, `skills`). The UCP transport lookup walks the manifest structurally rather than assuming one shape, since the services block nests differently across UCP revisions. ### Changed - **Product-feed signal re-framed around ACP as a discovery surface.** ACP shipped in September 2025 built around Instant Checkout; OpenAI retired that flow on 24 March 2026 and refocused the protocol on product discovery, with merchants pushing catalog feeds and promotions over ACP while keeping checkout on their own store. The 2026-04-17 revision formalised the shift, adding cart, feed, orders, authentication and MCP transport. The checker's logic is unchanged — the feed was always what it measured — but its documentation no longer describes a checkout integration that no longer exists. - **UCP checker distinguishes superseded revisions from unknown ones.** `2026-01-11` and `2026-01-23` are published UCP revisions. A manifest on one of them previously produced "not in known set — may be newer than this audit knows", which was the opposite of true. Those now warn that the profile is behind `2026-04-08` and name what that revision added; a genuinely unrecognised version string still warns that it may be newer than this release. - Audit user agent bumped to `AngeoAeoAudit/4.1`. ### Notes on scoring The new signal changes composite scores for stores that declare an `a2a` transport without serving a card. Stores that do not use A2A are unaffected: the signal passes for them. --- ## [4.0.0] — 2026-07-02 > Major release: the audit grows an **evidence layer**. Every previous signal > answered "is the door configured to be open?"; v4 adds two signals that > answer "did anyone actually walk through it?" — an edge/WAF reality probe > and observed AI crawler activity from pluggable, GDPR-safe evidence > sources. Grading is rewritten around bot *purpose* (training vs search vs > fetch), CI ships in the repository, and three shipped-broken test classes > plus a security bug in the CrUX checker are fixed. BC breaks listed below. ### Added - **`waf_reality` signal — Edge vs robots.txt consistency (weight 0.9).** Fetches the homepage presenting real AI crawler user agents (OAI-SearchBot, PerplexityBot, GPTBot, ClaudeBot) and compares the edge's behaviour against what robots.txt declares. WAF/CDN rules execute *before* robots.txt is ever read, so a managed challenge rule silently overrides every `Allow` a merchant writes — the classic "why does no AI engine ever cite us?" failure. Block-page fingerprints (Cloudflare, Imperva, PerimeterX, Akamai) are detected even behind HTTP 200. Mismatches WARN rather than FAIL, with an explicit verified-bot caveat: edges that validate crawler source IPs may correctly reject the spoofed probe while admitting genuine bots — cross-reference the activity signal before acting. - **`ai_crawler_activity` signal — observed AI crawler traffic (live_signal, weight 0.5, severity informational).** Merges evidence from pluggable `BotHitSourceInterface` adapters and grades by bot class: search/fetch crawlers seen → PASS; only training crawlers → WARN ("you feed models but earn no citations"); silence → WARN with cross-reference to `waf_reality`. Never FAILs and never fails a CI build by design — evidence coverage is inherently partial. - **Evidence sources (`Angeo\AeoAudit\Api\BotHitSourceInterface`):** - *Built-in instrumentation* (default ON): a frontend-area plugin counts AI bot requests at the PHP layer. Zero configuration, zero filesystem access, works on every hosting model. Honest limitation surfaced in every report: full-page-cache hits never reach PHP and are not counted. - *Webserver access log* (opt-in): tail-reads the last 8 MB of a configured nginx/apache log (combined or JSON lines, auto-detected). Documentation recommends targeted ACLs (`setfacl`) or logrotate copy hooks — never `chmod` on the live log. - Third-party adapters (CDN analytics, Fastly, Cloudflare) register via di.xml on the checker's `sources` argument. - **Privacy by architecture:** only aggregates (bot code, class, store, date, count) are ever persisted to the new `angeo_aeo_bot_hit` table — no IPs, no URLs, no raw user agents, no log lines. Retention (default 90 days) is enforced by the scheduled cron. - **`Angeo\AeoAudit\Service\BotRegistry`** — single purpose-classified catalog of 17 AI agents (training / search / fetcher / opt-out token) with UA and robots.txt tokens, shared by all bot-aware components so no bot is ever classified two different ways in two places. - **GitHub Actions CI** (`.github/workflows/ci.yml`): coding standard, PHPStan, unit tests on PHP 8.2/8.3/8.4, and an installability job against the Mage-OS composer mirror — every green build doubles as a Mage-OS compatibility proof. - **`i18n/en_US.csv`** — base translation dictionary for all admin strings. - **Configurable cron schedule** — `crontab.xml` now reads `angeo_aeo/cron/schedule` (Stores → Configuration → Angeo AEO → Scheduled Audit); default unchanged (`0 3 * * 1`). - **Vendor-neutral fix hints** — suggestions are rephrased ("any module providing the signal works — e.g.") and can be disabled entirely via `General → Show fix suggestions`, a hard requirement for distribution/bundling contexts. - `HttpCache::getAs($url, $userAgent)` — UA-aware cached GET; the cache key includes the UA because the same URL may legitimately answer differently per agent. - `Angeo\AeoAudit\Service\AuditResultPersister` — single owner of the populate → save → prune flow. ### Changed - **`robots_txt` grading rewritten around bot purpose (BC in behaviour).** Blocking a *search-class* crawler (OAI-SearchBot, PerplexityBot, Claude-SearchBot) FAILs — it removes the store from AI answers. Blocking a *training-class* crawler (GPTBot, ClaudeBot, CCBot, …) is now a respected licensing choice: reported in details, never punished. Previously GPTBot was graded "critical" and its block FAILed the audit — that conflated training opt-out with search invisibility. - robots.txt parsing extracted to the shared `Angeo\AeoAudit\Service\RobotsTxtParser` so `robots_txt` and `waf_reality` resolve "is bot X invited?" with identical logic. - CLI, cron and admin RunNow now persist through `AuditResultPersister` (removes 3× duplicated logic). - Audit User-Agent bumped to `AngeoAeoAudit/4.0`. - composer.json support URLs now point at the actual repository. ### Fixed - **Security — CrUX checker (`core_web_vitals`):** the 3.1.0 implementation instantiated a raw `Curl` outside DI with **TLS verification disabled** (`CURLOPT_SSL_VERIFYPEER/HOST = false`), sent the API key **in the URL query string** (leaks into proxy/access logs), and never decrypted the stored value even though the config backend is `Backend\Encrypted` — so a saved key was sent as ciphertext and every call failed. Now: key is decrypted (undecryptable ⇒ treated as unconfigured), sent via the `X-Goog-Api-Key` header, and the request goes through `HttpCache::post` with TLS verification on. The existing unit tests already demanded exactly this behaviour — they simply never ran (see below). - **Four test classes shipped broken in 3.1.0** (`AuditRunnerTest`, `SitemapXmlCheckerTest` — pre-Config constructor signatures; `MerchantPoliciesCheckerTest`, `SitemapXmlCheckerTest::disproportion` — assertions against removed v3.0 behaviour). They never ran because no CI existed; all fixed, full suite green (142 tests). ### BC breaks - `RobotsTxtChecker::__construct()` now requires `BotRegistry` and `RobotsTxtParser` (DI resolves this automatically; only manual instantiation is affected). - `CoreWebVitalsChecker::__construct()` now requires `EncryptorInterface`. - `AeoAuditCommand`, `AuditCron`, `RunNow` constructor signatures changed (persister-based). All DI-resolved. - `robots_txt` scoring changed as described above — stores that block GPTBot will see their score *rise*; stores that block OAI-SearchBot will see it *fall*. Both are corrections, not regressions. - New DB table `angeo_aeo_bot_hit` (created by `setup:upgrade`). ## [3.1.0] — 2026-06-10 > Minor release. Adds per-signal enable/disable configuration, configurable > sitemap placeholder-slug handling, and fixes two false-signal bugs in the > sitemap checker. Fully backward compatible — no interface or DB changes. ### Added - **Per-signal configuration.** Every one of the 15 signals can now be enabled or disabled individually under **Stores → Configuration → Angeo AEO → AEO Audit → Signals (Checks)**. All signals are **enabled by default**, so a fresh install behaves exactly as before. Disabled signals are skipped during the audit and excluded from the score entirely — they neither raise nor lower it (removed from both numerator and denominator). Settings are store-scoped. - **Configurable sitemap placeholder-slug handling.** New group **Angeo AEO → AEO Audit → Sitemap Checker**: - `Placeholder slug handling` — *Affect score* (default) or *Ignore* (report-only, never changes status/score). - `Placeholder slug threshold` — how many placeholder slugs are tolerated before the score is affected (default 1). - New `Angeo\AeoAudit\Model\Config` — a single typed reader for all module settings, so checkers no longer touch `ScopeConfig` directly. - New `Angeo\AeoAudit\Model\Config\Source\SlugMode` option source. - Unit tests: disabled-checker skipping in `AuditRunner`; sitemap foreign-element FAIL; placeholder-slug score/ignore modes; disproportion-false-positive regression. ### Fixed - **Sitemap: false "disproportion" warning.** The v3 check compared sitemap URL count against active **products only**, but a sitemap also lists the homepage, CMS pages and categories — so healthy stores were frequently warned. URL count is now compared against the full indexable surface (products + categories + CMS pages) and reported as **informational context only** (`coverage_ratio`); it never changes the result status. - **Sitemap: false "stale" warning.** Staleness was computed from the **first** `` in the file, so a single old entry (often the homepage or a CMS page) flagged the whole sitemap as stale. A legitimately unchanged product *should* keep an old `` — that is honest metadata, not a defect. The check now inspects the **newest** `` across the file and only warns if nothing at all has changed in 180 days (a sign of a broken generation cron). Individual old entries are informational only. ### Added — sitemap structural integrity - **Sitemap: foreign-element detection.** Non-sitemap elements injected directly into `` (e.g. a stray `