From eb7d1c0754787d8e75b1bff998b9f01c443d6376 Mon Sep 17 00:00:00 2001 From: Enrico Battocchi Date: Fri, 24 Apr 2026 17:07:24 +0200 Subject: [PATCH 1/4] Propose RC docs-sync: daily workflow + agent prompt + area map Adds the plumbing for a daily GitHub Action that polls Yoast product repos for new RC tags, runs a Claude agent against the developer-portal docs, and opens draft PRs where doc updates are warranted. Scope for this first phase: Yoast SEO (free) only. More products added iteratively by extending AGENT_MAP.md and the PRODUCTS dict in the workflow. Architecture: - No GitHub App or PAT required; product repos are public so anonymous cloning works and all writes to this repo use GITHUB_TOKEN. - Never writes to main; state lives in tracking-issue comments, identified by a machine-readable marker embedded in every run-summary comment. - Cloudflare Pages preview deploy on PR is the per-PR validation. - PRs are never auto-merged; branch protection's PR-review rule is the gate. Validated through three manual spikes (narrow positive, negative hotfix, multi-file new feature) plus end-to-end dry-runs of the orchestration prompt. Activation requirements (handled post-merge, see PR body): create a tracking issue, set TRACKING_ISSUE_WORDPRESS_SEO repo variable, set ANTHROPIC_API_KEY secret (coordinated with devops). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/claude-agent/run.md | 158 ++++++++++++++ .github/workflows/rc-docs-sync.yml | 314 +++++++++++++++++++++++++++ AGENT_MAP.md | 332 +++++++++++++++++++++++++++++ 3 files changed, 804 insertions(+) create mode 100644 .github/claude-agent/run.md create mode 100644 .github/workflows/rc-docs-sync.yml create mode 100644 AGENT_MAP.md diff --git a/.github/claude-agent/run.md b/.github/claude-agent/run.md new file mode 100644 index 00000000..8af96dbe --- /dev/null +++ b/.github/claude-agent/run.md @@ -0,0 +1,158 @@ +# .github/claude-agent/run.md +# +# Lives in: Yoast/developer +# Loaded by: the rc-docs-sync workflow (prompt_file). +# Purpose: orchestrate the full RC docs-sync flow in a single agent invocation — +# triage, authoring, one PR per affected area, tracking-issue summary. + +You are the RC docs-sync agent for Yoast's developer portal. A Release Candidate has just been cut in one of Yoast's product repos. Your job: decide whether the developer portal docs (this checkout) need changes, and if so, open one draft-or-ready PR per affected feature area. + +## Context available in this run + +- **`AGENT_MAP.md`** (in this repo root) — the source of truth for feature areas, docs paths, source paths, and symbol namespaces. Read this first. +- **`docs/`** — the current state of the developer portal docs. +- **`$BUNDLE_DIR//rc.diff.filtered`** — noise-filtered diff of the RC against the previous release, one per source repo. (For `ai-brand-insights` there are two.) +- **`$BUNDLE_DIR//rc.diff.full`** — unfiltered diff for cross-check. +- **`$BUNDLE_DIR//rc.diff.stat`** — `git diff --stat` summary. +- **`$BUNDLE_DIR//changelog.source`** — the product's user-facing changelog file. Find the entry for `$RC_TAG` and treat it as the "why" complementing the diff's "what". +- **`$BUNDLE_DIR/symbol-index.txt`** — sorted list of `wpseo_*` / `Yoast\WP\SEO\*` / `duplicate_post_*` symbols *currently documented* anywhere in `docs/`. A symbol that appears in a diff but NOT in this list is a new public surface and likely warrants docs. + +Environment variables (set by the invoking shell or workflow, not hardcoded here): `PRODUCT`, `RC_TAG`, `DISPLAY_NAME` (the human-readable product name from `AGENT_MAP.md`'s product table, e.g. `Yoast SEO`), `BUNDLE_DIR`, `TRACKING_ISSUE` (the issue number where the run summary must be posted), and optionally `DRY_RUN`. Read them before doing anything else so you know which product, RC, bundle, and tracking issue to work with. + +**Runtime note**: you are invoked from a workflow that authenticates with `GITHUB_TOKEN`. You do NOT need to run `yarn build` or `yarn lint` — Cloudflare Pages auto-deploys a preview on every PR push and will fail its check if the Docusaurus build is broken (bad internal links, missing frontmatter, etc.). Just make sure the edits you write are structurally correct; CF Pages is the validator. Link to the workflow run in the PR body for reviewer context. + +## Dry-run mode + +If `$DRY_RUN` is set to `true`, the run is happening outside CI (no working git remote, no `gh` auth, usually no `node_modules`). In that mode: + +- **Skip** all `git checkout -b`, `git commit`, `git push`, `gh pr create`, `gh pr edit`, `gh issue comment`, `yarn build`, and `yarn lint` steps below. +- Edit files **in place** in the working directory. +- Expect two extra files next to `docs/` and `sidebars.js`: `.docs-baseline/` and `.sidebars.baseline` — pristine copies taken before the run started. Do not touch them. +- At the very end (before Step 4's run-summary), emit: + - `$BUNDLE_DIR/proposed-docs.patch` — `diff -ruN .docs-baseline docs > "$BUNDLE_DIR/proposed-docs.patch" || true` + - `$BUNDLE_DIR/proposed-sidebars.patch` — `diff -u .sidebars.baseline sidebars.js > "$BUNDLE_DIR/proposed-sidebars.patch" || true` + - (The `|| true` swallows the nonzero exit `diff` produces when files differ.) +- In the PR body / run-summary, note that `yarn build` and `yarn lint` verification was skipped because the dry-run has no `node_modules`. +- The PR title format and all other authoring rules apply unchanged. + +Everything else in this prompt applies identically in both modes. + +## Flow + +### Step 1 — Triage + +Read `AGENT_MAP.md`. For every hunk in every `rc.diff.filtered`: + +- Map the source path to an area via the area's `source_paths`. +- Match new/removed/renamed symbols against `symbol_namespaces`. +- Compare against `symbol-index.txt` to confirm whether a symbol is already documented. + +Group findings by area. Produce an internal list of PR plans, one per affected area, following the schema from `AGENT_MAP.md`. If zero areas are affected, continue to Step 4 with an empty plan. + +**Rules:** +- Each doc file belongs to exactly one area. No file may appear in two PR plans. +- Maximum 5 PR plans per run. If the triage produces more, consolidate or escalate (comment on the tracking issue explaining what you dropped). +- Never touch `docs/development/**` or `docs/duplicate-post/**` (unless `PRODUCT` is `duplicate-post`). +- If a PR plan creates or renames doc files, it must also include `sidebars.js` with an update describing the navigation entry change. + +### Step 2 — Authoring (only if PR plans exist) + +For each PR plan, in order: + +1. Create a branch: `rc-sync/${PRODUCT}/${RC_TAG}/`. +2. Apply the edits using Edit / Write. Follow the style rules below. +3. `git add` only the files named in this PR plan. `git commit` with a message: `docs(): ` and a body citing `$RC_TAG`. +4. `git push` the branch. +7. Open a PR via `gh pr create` against `main`. Use this **PR title format**: + ``` + <Display name> <base version> — docs(<area>): <short title> + ``` + Where: + - `<Display name>` is the product's Display name column from `AGENT_MAP.md`'s product table (e.g., `Yoast SEO`, `Yoast SEO Premium`, `Yoast Local SEO`). + - `<base version>` is `$RC_TAG` with the `-RC<N>` suffix stripped (e.g., `26.3-RC1` → `26.3`; `27.1.1-RC2` → `27.1.1`). + - `<area>` is the feature-area slug from the PR plan. + - `<short title>` is the PR plan's `title` field. + Example: `Yoast SEO 26.3 — docs(llms-txt): Document new wpseo_llmstxt_link_description filter`. +8. Body template below. +9. Apply labels: `rc-doc-sync`, `product/${PRODUCT}`, `area/<area>`, `rc/${RC_TAG}`. + +### Step 3 — Cross-link sibling PRs + +After all PRs are created, edit each one's body with `gh pr edit` to append a "Sibling PRs from this RC" section linking to the others. This helps the reviewer see the whole RC's doc impact in one glance. + +### Step 4 — Run summary + +Write `$BUNDLE_DIR/run-summary.md`, then post its contents as a comment on the tracking issue via `gh issue comment $TRACKING_ISSUE --body-file "$BUNDLE_DIR/run-summary.md"`. + +The **first line of the comment MUST** be a machine-readable marker that the next scheduled run will use to determine this RC is now processed: + +``` +<!-- rc-docs-sync:v1 product=$PRODUCT rc_tag=$RC_TAG --> +``` + +Literal comment syntax (the `<!-- -->` is an HTML comment that renders invisibly in GitHub's UI but is still readable via the API). Substituting the env vars. No blank line between the marker and the next line. + +The body after the marker should contain: + +- `${DISPLAY_NAME} ${RC_TAG%-RC*}` as a heading (e.g. `Yoast SEO 27.5`). +- RC tag and its **tag-creation date** (use `git -C sources/<repo> log -1 --format=%ad --date=short "$RC_TAG"`, NOT the changelog/readme's release date — that often refers to a later final shipment, not the RC). +- Source repo(s) + previous release. +- Diff size (filtered line count per repo). +- Symbol index size, count of new symbols observed in diff. +- One bullet per PR plan: area, title, PR link. +- If zero PRs: a one-paragraph explanation of what the RC contained and why no doc changes are needed (cite the changelog entry and top-level diff areas). + +**If you fail to post the comment with the marker, the next scheduled run will re-process this RC.** Posting the marker is the acknowledgement of completion. + +## Style rules for any doc edit + +- American English, "we" voice (never "I"). Match existing page tone. +- Semantic heading levels; don't skip from `##` to `####`. +- PHP code fences with `<?php` opener for examples. +- A complete, working `add_filter(...)` example for every new filter documented, with matching priority and argument count (grep the source file to confirm). +- Preserve Docusaurus frontmatter (`id`, `title`, `sidebar_label`) on existing files; include it on any newly created `.md` file. +- Use Docusaurus admonitions (`:::note`, `:::caution`, `:::tip`) where the surrounding file already uses them. +- Use exact symbol names, parameter names, and default values from the diff. Do not guess. If the source has PHPDoc, mirror its `@param` lines. +- **If a filter, API, or command affects observable output (rendered HTML, JSON, file contents, CLI output), include a before/after example of that output when it fits the page's conventions.** Concrete renderings are more illustrative than prose description — e.g., for an llms.txt filter, show the `## Posts` list before and after the filter is applied. + +## Authoring discipline + +- Before writing any filter name in a doc, grep `rc.diff.filtered` for the exact string. If it isn't there, don't write it. +- If a PR plan's `files` list includes a creation, also include `sidebars.js` — forgetting this breaks the Docusaurus build (and hence the Cloudflare Pages preview check). +- Double-check Markdown links on any file you create or edit: internal doc links must resolve to an actual `id:` in another file's frontmatter or to a path that exists. +- If you're uncertain about a subtle behavior change (no symbol rename, just different semantics), include it in the triage PR body under a "Needs human verification" section rather than confidently rewriting docs. + +## PR body template + +``` +## RC docs sync — ${RC_TAG} + +**Product**: ${PRODUCT} +**Area**: <area> +**Source evidence**: +- <file:line or symbol>:<short description> +- … + +## Changes in this PR + +<one-line per file> + +## Verification + +- [x] `yarn build` passes locally in this workflow. +- [x] `yarn lint` clean for files touched by this PR. + +## Reviewer notes + +<anything the agent is uncertain about — behavior-only changes, style judgments, places where the source evidence was ambiguous> + +--- + +_Authored by the RC docs-sync agent. Not auto-merged — requires human review. Run: ${RUN_URL}_ +``` + +## Stop conditions + +- If triage produces zero PR plans, skip Step 2 and 3, still write the run summary in Step 4, and exit successfully. +- If the triage produces more than 5 PR plans, consolidate the smallest areas into sibling plans or escalate with a comment and open only the top 5. +- If `yarn build` fails and you cannot fix it in 3 attempts, close the branch, skip that PR plan, note the failure in the run summary, and continue with the remaining plans. diff --git a/.github/workflows/rc-docs-sync.yml b/.github/workflows/rc-docs-sync.yml new file mode 100644 index 00000000..1c78004f --- /dev/null +++ b/.github/workflows/rc-docs-sync.yml @@ -0,0 +1,314 @@ +# .github/workflows/rc-docs-sync.yml +# +# Lives in: Yoast/developer +# Purpose: once a day, check each opted-in product repo for RC tags we haven't +# processed yet. For each new RC, ask a Claude agent whether the +# developer-portal docs need updates; if so, open one PR per affected +# feature area (per AGENT_MAP.md). +# +# Why no GitHub App or PAT: product repos are public → anonymous cloning works; +# writes to Yoast/developer (branches, PRs, issue comments) use GITHUB_TOKEN. +# The only secret required is ANTHROPIC_API_KEY. +# +# State management: because `main` is protected, this workflow never writes to +# `main`. Instead, the per-product tracking issue's comments serve as state. +# Every run-summary comment starts with a machine-readable marker: +# <!-- rc-docs-sync:v1 product=<slug> rc_tag=<tag> --> +# The workflow scans the tracking issue's comments to find the latest processed +# RC per product, then processes any newer RC tags. +# +# Validation: Cloudflare Pages auto-deploys a preview on every PR push, acting +# as the per-PR check (broken Docusaurus links fail the deploy). The agent +# doesn't re-run `yarn build` locally; it trusts CF Pages for the final word. + +name: RC docs sync + +on: + schedule: + - cron: '0 6 * * *' # daily at 06:00 UTC + workflow_dispatch: + inputs: + product: + description: 'Product slug (must match AGENT_MAP.md; e.g. wordpress-seo). Leave blank to sweep all opted-in products.' + required: false + type: string + rc_tag: + description: 'Specific RC tag to process (e.g. 27.5-RC1). Bypasses state gating for that one product+tag. Useful for backfill.' + required: false + type: string + +concurrency: + group: rc-docs-sync + cancel-in-progress: false + +permissions: + contents: write # push per-RC doc branches (NOT main — main is protected) + pull-requests: write # open and label PRs + issues: write # comment on tracking issue(s) + +jobs: + sync: + runs-on: ubuntu-latest + timeout-minutes: 60 + + env: + # Map product slug → tracking issue repo variable name. Variable names must + # be alphanumeric + underscores, so the slug dashes are normalized to underscores. + # TRACKING_ISSUE_WORDPRESS_SEO (repo var) → issue number in Yoast/developer. + TRACKING_ISSUE_WORDPRESS_SEO: ${{ vars.TRACKING_ISSUE_WORDPRESS_SEO }} + + steps: + - name: Check out Yoast/developer + uses: actions/checkout@v4 + with: + fetch-depth: 1 + path: developer + + # ---- 1. Resolve work queue using issue comments as state ----------- + # For each opted-in product, find the most recent "rc-docs-sync" marker + # comment on its tracking issue. Process RC tags newer than that marker. + + - name: Resolve work queue + id: queue + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INPUT_PRODUCT: ${{ github.event.inputs.product }} + INPUT_RC_TAG: ${{ github.event.inputs.rc_tag }} + GH_REPO: ${{ github.repository }} + run: | + set -euo pipefail + python3 - <<'PY' > queue.json + import json, os, re, subprocess, sys, urllib.request + from urllib.error import HTTPError + + # --- opted-in products for this phase of rollout --- + PRODUCTS = { + "wordpress-seo": { + "display_name": "Yoast SEO", + "repos": ["Yoast/wordpress-seo"], + "tracking_issue_var": "TRACKING_ISSUE_WORDPRESS_SEO", + }, + } + + MARKER_RE = re.compile( + r"<!--\s*rc-docs-sync:v1\s+product=(?P<product>\S+)\s+rc_tag=(?P<rc_tag>\S+)\s*-->" + ) + RC_TAG_RE = re.compile(r"^\d+\.\d+(?:\.\d+)?-RC\d+$") + STABLE_RE = re.compile(r"^\d+\.\d+(?:\.\d+)?$") + + def sort_key(tag): + m = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?(?:-RC(\d+))?$", tag) + if not m: + return (0, 0, 0, 0) + major, minor, patch, rc = m.groups() + # Stable (no RC suffix) should sort AFTER its RCs: use rc=99999 to mark stable. + return (int(major), int(minor), int(patch or 0), int(rc) if rc else 99999) + + def gh_json(args): + cp = subprocess.run(["gh"] + args, check=True, capture_output=True, text=True) + return json.loads(cp.stdout) + + def fetch_comments(issue_number): + # gh issue view --json comments returns {"comments":[{"body":"..","createdAt":"..."},...]} + data = gh_json(["issue", "view", str(issue_number), "--json", "comments"]) + return data.get("comments", []) + + def fetch_latest_marker(issue_number, product_slug): + comments = fetch_comments(issue_number) + # Scan newest first (gh returns in ascending order by default; reverse). + for c in reversed(comments): + for m in MARKER_RE.finditer(c.get("body", "")): + if m.group("product") == product_slug: + return m.group("rc_tag") + return None + + def fetch_tags(repo): + url = f"https://api.github.com/repos/{repo}/tags?per_page=100" + req = urllib.request.Request(url, headers={"Accept": "application/vnd.github+json"}) + with urllib.request.urlopen(req) as r: + tags = json.load(r) + return [t["name"] for t in tags] + + input_product = os.environ.get("INPUT_PRODUCT") or "" + input_rc_tag = os.environ.get("INPUT_RC_TAG") or "" + queue = [] + seed_actions = [] + + products_to_sweep = [input_product] if input_product else list(PRODUCTS.keys()) + for slug in products_to_sweep: + if slug not in PRODUCTS: + print(f"skipping unknown product {slug}", file=sys.stderr) + continue + product = PRODUCTS[slug] + tracking_issue = os.environ.get(product["tracking_issue_var"]) + if not tracking_issue: + print(f"missing repo variable {product['tracking_issue_var']}; cannot process {slug}", file=sys.stderr) + continue + + main_repo = product["repos"][0] + all_tags = fetch_tags(main_repo) + rc_tags = [t for t in all_tags if RC_TAG_RE.match(t)] + stable_tags = [t for t in all_tags if STABLE_RE.match(t)] + + if input_rc_tag and input_product == slug: + if input_rc_tag not in rc_tags: + print(f"{input_rc_tag} not found as RC in {main_repo}", file=sys.stderr) + sys.exit(2) + rcs_to_process = [input_rc_tag] + else: + last = fetch_latest_marker(tracking_issue, slug) + if last is None: + # First-run seeding: mark the current latest RC as "already processed" + # by posting a seed comment, and process nothing historically. + rc_tags_sorted = sorted(rc_tags, key=sort_key) + seed_rc = rc_tags_sorted[-1] if rc_tags_sorted else None + if seed_rc: + seed_actions.append({ + "issue": tracking_issue, + "product": slug, + "rc_tag": seed_rc, + "display_name": product["display_name"], + }) + continue + last_key = sort_key(last) + rcs_to_process = sorted([t for t in rc_tags if sort_key(t) > last_key], key=sort_key) + + for rc_tag in rcs_to_process: + prev_candidates = [t for t in stable_tags if sort_key(t) <= sort_key(rc_tag)] + if not prev_candidates: + print(f"no previous stable for {rc_tag}; skipping", file=sys.stderr) + continue + prev = sorted(prev_candidates, key=sort_key)[-1] + queue.append({ + "product": slug, + "display_name": product["display_name"], + "repos": product["repos"], + "rc_tag": rc_tag, + "prev_release": prev, + "tracking_issue": tracking_issue, + }) + + print(json.dumps({"queue": queue, "seeds": seed_actions})) + PY + cat queue.json + echo "count=$(jq '.queue | length' queue.json)" >> "$GITHUB_OUTPUT" + echo "seed_count=$(jq '.seeds | length' queue.json)" >> "$GITHUB_OUTPUT" + + # ---- 2. Post seed comments for first-run products ------------------ + + - name: Seed first-run tracking issues + if: steps.queue.outputs.seed_count != '0' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + set -euo pipefail + jq -c '.seeds[]' queue.json | while read -r seed; do + issue=$(echo "$seed" | jq -r .issue) + product=$(echo "$seed" | jq -r .product) + rc_tag=$(echo "$seed" | jq -r .rc_tag) + display=$(echo "$seed" | jq -r .display_name) + gh issue comment "$issue" --body "<!-- rc-docs-sync:v1 product=${product} rc_tag=${rc_tag} --> + + **First-run seed for ${display}** — RC tag \`${rc_tag}\` recorded as the baseline. No historical RCs will be processed automatically. To backfill a specific RC, use \`workflow_dispatch\` with \`product=${product}\` and the desired \`rc_tag\`." + done + + # ---- 3. Short-circuit if queue is empty ---------------------------- + + - name: Short-circuit if queue is empty + if: steps.queue.outputs.count == '0' + run: | + echo "No new RC tags to process." + exit 0 + + # ---- 4. Process the queue serially -------------------------------- + # No node setup / yarn install here: the Cloudflare Pages preview deploy + # on the resulting PR is the per-PR validation. Any broken internal links + # or frontmatter errors will fail the preview and surface on the PR. + + - name: Process RC queue + if: steps.queue.outputs.count != '0' + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + set -euo pipefail + mkdir -p sources bundle + + jq -c '.queue[]' queue.json | while read -r item; do + product=$(echo "$item" | jq -r .product) + display_name=$(echo "$item" | jq -r .display_name) + rc_tag=$(echo "$item" | jq -r .rc_tag) + prev=$(echo "$item" | jq -r .prev_release) + tracking_issue=$(echo "$item" | jq -r .tracking_issue) + echo "===== Processing $product @ $rc_tag (prev: $prev) =====" + + # Clone each source repo for this product (anonymous) + for repo in $(echo "$item" | jq -r '.repos[]'); do + name="${repo##*/}" + if [ ! -d "sources/${name}" ]; then + git clone --depth 50 --no-single-branch "https://github.com/${repo}.git" "sources/${name}" + fi + git -C "sources/${name}" fetch --depth 200 origin "refs/tags/${rc_tag}:refs/tags/${rc_tag}" "refs/tags/${prev}:refs/tags/${prev}" || true + done + + # Build the bundle + bundle_dir="bundle/${product}/${rc_tag}" + mkdir -p "$bundle_dir" + for repo in $(echo "$item" | jq -r '.repos[]'); do + name="${repo##*/}" + rb="${bundle_dir}/${name}" + mkdir -p "$rb" + git -C "sources/${name}" diff "${prev}..${rc_tag}" > "${rb}/rc.diff.full" + git -C "sources/${name}" diff "${prev}..${rc_tag}" \ + -- \ + ':(exclude)tests' \ + ':(exclude)**/__tests__' \ + ':(exclude)**/__snapshots__' \ + ':(exclude)**/*.lock' \ + ':(exclude)languages' \ + ':(exclude).github' \ + ':(exclude)composer.lock' \ + ':(exclude)yarn.lock' \ + ':(exclude)package-lock.json' \ + > "${rb}/rc.diff.filtered" + git -C "sources/${name}" diff --stat "${prev}..${rc_tag}" > "${rb}/rc.diff.stat" + for f in readme.txt CHANGELOG.md changelog.md changelog.txt; do + if git -C "sources/${name}" show "${rc_tag}:${f}" > "${rb}/changelog.source" 2>/dev/null; then break; fi + done + done + + # Build symbol index from current docs/ + ( + grep -rohE "'wpseo_[a-zA-Z0-9_]+'" developer/docs/ || true + grep -rohE "'Yoast\\\\WP\\\\SEO\\\\[a-zA-Z0-9_\\\\]+'" developer/docs/ || true + grep -rohE "'duplicate_post_[a-zA-Z0-9_]+'" developer/docs/ || true + ) | sort -u > "${bundle_dir}/symbol-index.txt" + + # Short-circuit if filtered diff is empty + any_content=false + for f in ${bundle_dir}/*/rc.diff.filtered; do + [ -s "$f" ] && any_content=true + done + if [ "$any_content" = "false" ]; then + gh issue comment "$tracking_issue" --body "<!-- rc-docs-sync:v1 product=${product} rc_tag=${rc_tag} --> + + **${display_name} ${rc_tag%-RC*}** (RC \`${rc_tag}\`) — no doc changes needed. + Filtered diff is empty (only tests/translations/lockfiles changed). + Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + continue + fi + + # Invoke the Claude agent. + # (Placeholder — see .github/claude-agent/run.md for the prompt. The action + # reference/version must be pinned when wiring for real, e.g. + # anthropics/claude-code-action@<sha>.) + echo "TODO: invoke agent with BUNDLE_DIR=$(realpath $bundle_dir) PRODUCT=$product RC_TAG=$rc_tag DISPLAY_NAME=$display_name TRACKING_ISSUE=$tracking_issue" + + # Per agent contract, the agent itself is responsible for: + # 1. Creating branches under rc-sync/<product>/<rc_tag>/<area> + # 2. Running `yarn build` / `yarn lint` before each PR + # 3. Opening PRs via `gh pr create` with the required title format and labels + # 4. Posting the run-summary comment with the marker to the tracking issue + done diff --git a/AGENT_MAP.md b/AGENT_MAP.md new file mode 100644 index 00000000..2f67de07 --- /dev/null +++ b/AGENT_MAP.md @@ -0,0 +1,332 @@ +# AGENT_MAP + +Source of truth for the RC docs-sync agent. Tells the agent which doc files belong to which feature area, which source paths feed each area, and which change patterns in a plugin RC should trigger doc updates. + +Each doc file belongs to **exactly one area**. One PR plan per affected area. If an RC touches N areas, the agent opens N PRs (expected: 0–2 per RC most of the time; caps at ~5 for safety). + +## How the agent uses this file + +1. On run start, the agent loads this file plus the full `docs-before/` tree. +2. On every hunk in the RC diff, the agent checks: + - Does the source path match an area's `source_paths`? → candidate for that area. + - Does the hunk introduce/remove/rename a symbol matching an area's `symbol_namespaces`? → candidate for that area. +3. The agent groups candidates by area, produces one PR plan per area with `files` drawn from that area's `docs_paths`. +4. Hunks that don't match any area's triggers are considered doc-irrelevant by default — but the agent must still reason about whether they're a subtle behavior change in a documented feature. + +## How to maintain this file + +- When a new doc directory is added, add a new area entry. +- When a doc file moves, update its area's `docs_paths`. +- When a plugin introduces a new filter namespace, add it to the relevant area's `symbol_namespaces` so the symbol-index generator picks it up. +- If you find the agent repeatedly mis-attributing a change, refine the area boundaries here rather than in the prompt. + +## Product → source repo + +| Product slug | Display name | GitHub repo(s) | +|-------------------------|--------------------------|------------------------------------------------------------------------| +| wordpress-seo | Yoast SEO | `Yoast/wordpress-seo` | +| wordpress-seo-premium | Yoast SEO Premium | `Yoast/wordpress-seo-premium` | +| wordpress-seo-local | Yoast Local SEO | `Yoast/wordpress-seo-local` | +| wpseo-news | Yoast News SEO | `Yoast/wpseo-news` | +| wpseo-video | Yoast Video SEO | `Yoast/wpseo-video` | +| wpseo-woocommerce | Yoast WooCommerce SEO | `Yoast/wpseo-woocommerce` | +| shopify-seo | Yoast SEO for Shopify | `Yoast/shopify-seo` | +| duplicate-post | Yoast Duplicate Post | `Yoast/duplicate-post` | +| ai-brand-insights | Yoast AI Brand Insights | `Yoast/ai-insights-api` + `Yoast/ai-insights-frontend` (split product) | + +Display names are the human-readable product names used in PR titles and tracking issue summaries. They mirror the names used in `docusaurus.config.js` changelog plugin entries. + +`ai-brand-insights` is the only product with more than one source repo — the agent must consider diffs from both when running on its RCs. Product slug matches the changelog id in `docusaurus.config.js`, which differs from the repo name in several cases (`wordpress-seo-local` ↔ `Local SEO`, `wpseo-*` ↔ `News/Video/WooCommerce SEO`, `shopify-seo` ↔ `Yoast SEO for Shopify`, `ai-brand-insights` ↔ `AI Brand Insights`). + +--- + +## Areas + +### `llms-txt` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/llms-txt/**` +- **Source paths** (wordpress-seo): `src/llms-txt/**` +- **Symbol namespaces**: `wpseo_llmstxt_*` +- **Typical triggers**: new/renamed/removed filter in the namespace; changes to the llms.txt file format or default content; changes to what post types are included. +- **Validated in**: Spike A (recall) ✅ + +### `robots-txt` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/robots-txt/**` +- **Source paths** (wordpress-seo): `src/presenters/robots-txt-presenter.php`, `src/helpers/robots-txt-helper.php`, `src/integrations/**/robots-txt*` +- **Symbol namespaces**: `wpseo_robots_*`, `wpseo_*_robots_*` (anchored to robots), `wpseo_disable_robots_*`, `Yoast\WP\SEO\register_robots_rules` +- **Typical triggers**: changes to robots.txt output (new directives, Sitemap/Schemamap lines); new filters that suppress or alter robots output. +- **Validated in**: Spike C bonus catch ✅ + +### `schema-aggregator` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/schema/schema-aggregator/**` +- **Source paths** (wordpress-seo): `src/schema-aggregator/**` +- **Symbol namespaces**: `wpseo_schema_aggregator_*`, `wpseo_article_enhance_*`, `wpseo_person_enhance_*` +- **REST routes**: `yoast/v1/schema-aggregator/**` +- **WP-CLI commands**: `wp yoast aggregate_site_schema`, `wp yoast aggregate_site_schema_clear_cache` +- **Typical triggers**: new filter in the namespace; new REST route; new CLI command; change to caching behavior or default post-type selection. +- **Validated in**: Spike C ✅ + +### `schema-pieces` +- **Products**: wordpress-seo (primary), wordpress-seo-premium, wordpress-seo-local, wpseo-news, wpseo-video, wpseo-woocommerce, shopify-seo +- **Docs paths**: `docs/features/schema/pieces/**` +- **Source paths** (per product): + - **wordpress-seo**: `src/generators/schema/**`, `admin/**/schema*` + - **wordpress-seo-premium**: `src/generated/**/schema*`, `src/integrations/**/schema*` + - **wordpress-seo-local**: `classes/schema/**` (primary schema pieces for this product live under legacy `classes/`, not `src/`) + - **wpseo-news**: `classes/**/schema*`, `classes/**/news-article*` + - **wpseo-video**: `classes/**/schema*`, `classes/**/videoobject*` + - **wpseo-woocommerce**: `classes/**/schema*`, `classes/**/product*` +- **Symbol namespaces**: `wpseo_schema_<piece>` where `<piece>` matches a piece filename (`article`, `person`, `organization`, `webpage`, `website`, `breadcrumb`, `product`, `offer`, `recipe`, `video`, `review`, etc.) +- **Typical triggers**: required/optional property added or removed on a piece class; new piece filter; changed `@type` conditions. +- **When to create vs. update**: create a new file only if a wholly new schema piece is introduced; otherwise update an existing piece file. + +### `schema-api` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/schema/api.md`, `docs/features/schema/integration-guidelines.md` +- **Source paths** (wordpress-seo): `src/integrations/third-party/schema-*`, `src/surfaces/schema*`, `src/helpers/schema/**` +- **Symbol namespaces**: `wpseo_json_ld_output`, `wpseo_schema_graph_pieces`, `wpseo_schema_needs_*`, `yoast_seo_development_mode` +- **Typical triggers**: new top-level schema filter; changes to the graph-piece enable/disable mechanism; new helpers on `YoastSEO()->helpers->schema->*`. + +### `schema-per-plugin` +- **Products**: all plugins that output schema +- **Docs paths**: `docs/features/schema/plugins/**` (one file per plugin: `yoast-seo.md`, `yoast-seo-premium.md`, `local-seo.md`, `news-seo.md`, `video-seo.md`, `woocommerce-seo.md`, `yoast-seo-shopify.md`) +- **Source paths**: see `schema-pieces` above — same per-plugin paths, but this area tracks per-plugin *output aggregates* rather than individual piece properties. +- **Typical triggers**: a plugin starts/stops emitting a piece; a plugin's per-piece properties change in aggregate; the plugin's conditional activation for schema changes. +- **When to touch**: only when a specific plugin's schema output changes in a way that warrants updating its per-plugin summary page; otherwise updates to shared piece docs go to `schema-pieces`. + +### `schema-core` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/schema/background.md`, `docs/features/schema/functional-specification.md`, `docs/features/schema/technology-approach.md` +- **Typical triggers**: rare — fundamental approach changes (e.g., moving from JSON-LD to something else, changing the `@graph` structure). +- **Default**: out of scope for automated PRs unless the diff explicitly indicates a foundational change. Prefer a human-authored PR. + +### `seo-tags-titles` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/seo-tags/titles/**` +- **Source paths** (wordpress-seo): `src/presenters/title*`, `src/builders/title*`, `src/generators/schema/webpage.php` (for `name` field) +- **Symbol namespaces**: `wpseo_title`, `wpseo_title_separator` +- **Typical triggers**: new title filter; change to template replacement vars; change to default template. + +### `seo-tags-descriptions` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/seo-tags/descriptions/**` +- **Source paths** (wordpress-seo): `src/presenters/meta-description*`, `src/builders/meta-description*` +- **Symbol namespaces**: `wpseo_metadesc` +- **Typical triggers**: new description filter; default-generation logic changes. + +### `seo-tags-canonical` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/seo-tags/canonical-urls/**` +- **Source paths** (wordpress-seo): `src/presenters/canonical*`, `src/builders/canonical*`, `src/helpers/canonical*` +- **Symbol namespaces**: `wpseo_canonical` +- **Typical triggers**: canonical-URL filter changes; new `rel="canonical"` emission conditions. + +### `seo-tags-meta-robots` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/seo-tags/meta-robots/**` +- **Source paths** (wordpress-seo): `src/presenters/robots*` (meta tag, not robots.txt), `src/helpers/robots*` +- **Symbol namespaces**: `wpseo_robots`, `wpseo_googlebot`, `wpseo_bingbot` +- **Typical triggers**: new meta robots directive; default index/noindex logic changes. + +### `opengraph` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/opengraph/**` +- **Source paths** (wordpress-seo): `src/presenters/open-graph/**`, `src/generators/open-graph*` +- **Symbol namespaces**: `wpseo_opengraph*`, `wpseo_og_*`, `wpseo_add_opengraph_*` +- **Typical triggers**: new OG tag; new OG filter; default image/locale behavior change. + +### `twitter` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/twitter/**` +- **Source paths** (wordpress-seo): `src/presenters/twitter/**` +- **Symbol namespaces**: `wpseo_twitter_*` +- **Typical triggers**: new X/Twitter card filter; card type changes. + +### `sitemaps` +- **Products**: wordpress-seo, wpseo-news, wpseo-video +- **Docs paths**: `docs/features/xml-sitemaps/**` +- **Source paths**: + - **wordpress-seo**: `inc/sitemaps/**`, `src/integrations/**/sitemap*` + - **wpseo-news**: `classes/**/sitemap*`, `classes/**/news-sitemap*` + - **wpseo-video**: `classes/**/sitemap*`, `xml-video-sitemap.xsl` +- **Symbol namespaces**: `wpseo_sitemap_*`, `wpseo_*_sitemap_*`, `wpseo_news_sitemap_*`, `wpseo_video_sitemap_*` +- **Typical triggers**: new sitemap type; new per-entry filter; lastmod/priority default changes. + +### `indexables` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/indexables/**` +- **Source paths** (wordpress-seo): `src/builders/indexable-*`, `src/repositories/indexable*`, `src/actions/indexing/**` +- **Symbol namespaces**: `wpseo_indexable_*`, `Yoast\WP\SEO\should_index_indexables`, `Yoast\WP\SEO\Exclude_*` +- **Typical triggers**: new excluded-post-type/taxonomy filter; indexing-behavior changes; schema-column additions. + +### `integrations` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/integrations/**` +- **Source paths** (wordpress-seo): `src/integrations/indexnow/**`, `src/integrations/site-connections/**` (and similar well-defined integration folders) +- **Typical triggers**: new integration added (IndexNow, site-connections, Algolia, etc.); change to ping/notify behavior. + +### `ai` +- **Products**: wordpress-seo, wordpress-seo-premium, ai-brand-insights (split across `Yoast/ai-insights-api` + `Yoast/ai-insights-frontend`) +- **Docs paths**: `docs/features/ai/**` +- **Source paths** (wordpress-seo): `src/ai-*/**`, `src/generators/ai*`, `src/integrations/ai*` +- **Source paths** (wordpress-seo-premium): `src/ai/**` +- **Source paths** (ai-insights-api): Laravel app — `app/**` (controllers, models, services, jobs), `routes/**`, `config/**`, `database/migrations/**`, `resources/views/**` (Blade templates) +- **Source paths** (ai-insights-frontend): Vite/React/TypeScript — `src/api/**`, `src/components/**`, `src/pages/**`, `src/hooks/**`, `src/contexts/**`, `src/lib.tsx`, `src/orval/**` (auto-generated API clients — changes here reflect upstream API changes) +- **Split-product workflow rule**: For `ai-brand-insights` RCs the workflow must pull diffs from **both** repos (ai-insights-api and ai-insights-frontend) and pass them to the agent as a single input, clearly labeled by repo. Only the API repo emits user-observable public surface; frontend changes are informational unless they alter documented behavior. +- **Docs-coverage gap note**: At map-drafting time `docs/features/ai/` contained only `ai-errors.md`. There is no feature-spec doc for AI Brand Insights yet. Until such docs exist, RC runs for `ai-brand-insights` will almost always yield `pr_plans: []` — this is correct behavior, not a failure. If AI Brand Insights starts being documented in the portal, expand this area's docs_paths accordingly. +- **Symbol namespaces** (WP side): `wpseo_ai_*` +- **Typical triggers**: new AI error code; new AI feature exposing a filter; change to request/retry behavior documented in `ai-errors.md`; new public REST endpoint in `ai-insights-api`. + +### `alternate-formats` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/alternate-formats/**` +- **Source paths** (wordpress-seo): `src/integrations/**/embed*`, `src/integrations/**/rss*`, `inc/class-rss.php` +- **Typical triggers**: embed/RSS emission changes; filters on embedded content. + +### `controls` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/controls/**` +- **Source paths** (wordpress-seo): `packages/js/src/containers/link-attributes*`, `admin/links/**` +- **Typical triggers**: changes to link-attribute controls; new UI-exposed user controls. + +### `blocks` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/blocks/**` +- **Source paths** (wordpress-seo): `packages/js/src/structured-data-blocks/**`, `src/integrations/blocks/**` +- **Typical triggers**: new Yoast block; breadcrumb block attribute changes. + +### `analysis` +- **Products**: wordpress-seo, shopify-seo +- **Docs paths**: `docs/features/analysis/**` +- **Source paths**: `packages/yoastseo/**`, `packages/js/src/analysis/**` +- **Typical triggers**: new assessment; algorithm changes worth surfacing. + +### `http-headers` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/http-headers/**` +- **Source paths** (wordpress-seo): `src/integrations/front-end/**/header*`, `src/presenters/*header*` +- **Typical triggers**: new header emitted; conditional emission changes. + +### `wp-cli` +- **Products**: wordpress-seo +- **Docs paths**: `docs/features/wp-cli/**` +- **Source paths** (wordpress-seo): `src/commands/**`, `src/cli/**`, any file matching `*-cli-command.php` +- **Symbol namespaces**: `WP_CLI::add_command` registrations. +- **Typical triggers**: new CLI command; new option on an existing command. + +--- + +### `apis` (shared, low-frequency) +- **Products**: wordpress-seo +- **Docs paths**: `docs/customization/apis/**`, `docs/customization/myyoast/apis/**` +- **Source paths** (wordpress-seo): `src/integrations/rest/**`, `src/surfaces/**`, `src/presenters/rest*` +- **Typical triggers**: new REST route; new Surface class/method; Metadata API changes. +- **Scoping note**: the agent should propose targeted file edits (e.g., `rest-api.md` only) rather than blanket updates across all API pages. + +### `customization-free` +- **Products**: wordpress-seo +- **Docs paths**: `docs/customization/yoast-seo/**` +- **Source paths** (wordpress-seo): anywhere that `apply_filters('wpseo_*')` is emitted *and* the symbol is not already mapped to a more specific area (llms-txt, sitemaps, schema, etc.) +- **Symbol namespaces**: `wpseo_*` (fallback; more specific areas take precedence) +- **Typical triggers**: new customization filter; deprecation of an existing filter. +- **Deprecations page**: `docs/customization/yoast-seo/deprecations.md` — update this file whenever a filter/action is removed or renamed. + +### `customization-premium` +- **Products**: wordpress-seo-premium +- **Docs paths**: `docs/customization/yoast-seo-premium/**` +- **Source paths** (wordpress-seo-premium): `src/**` (primary — actions, ai, config, integrations, presenters, routes, surfaces, repositories, introductions, user-meta), `classes/**` (legacy), `cli/**`, `premium.php` +- **Symbol namespaces**: `wpseo_premium_*`, `Yoast\WP\SEO\Premium\*` +- **Deprecations page**: `docs/customization/yoast-seo-premium/deprecations.md` + +### `local-seo` +- **Products**: wordpress-seo-local +- **Docs paths**: `docs/customization/local-seo/**`, `docs/features/schema/plugins/local-seo.md` +- **Source paths** (wordpress-seo-local): `src/**` (builders, integrations, presenters, repositories, posttype, tools, formatters), `classes/**` (legacy — includes `classes/schema/**` which is the primary schema contribution for `schema-per-plugin`), `includes/**`, `widgets/**`, `woocommerce/**`, `local-seo.php` +- **Symbol namespaces**: `wpseo_local_*` + +### `news-seo` +- **Products**: wpseo-news +- **Docs paths**: `docs/customization/news-seo/**`, `docs/features/schema/plugins/news-seo.md` +- **Source paths** (wpseo-news): `classes/**` (primary — this repo is mostly still legacy structure), `src/**` (currently mostly autoloader output), `js/**`, `wpseo-news.php` +- **Symbol namespaces**: `wpseo_news_*` + +### `video-seo` +- **Products**: wpseo-video +- **Docs paths**: `docs/customization/video-seo/**`, `docs/features/schema/plugins/video-seo.md` +- **Source paths** (wpseo-video): `classes/**` (primary — no `src/` tree yet), `detail-retrieval/**`, `post-analysis/**`, `views/**`, `video-seo.php`, `video-seo-api.php`, `xml-video-sitemap.xsl` +- **Symbol namespaces**: `wpseo_video_*` + +### `woocommerce-seo` +- **Products**: wpseo-woocommerce +- **Docs paths**: `docs/features/schema/plugins/woocommerce-seo.md` +- **Source paths** (wpseo-woocommerce): `classes/**` (primary), `src/**` (mostly autoloader output), `js/**`, `wpseo-woocommerce.php` +- **Symbol namespaces**: `wpseo_woo_*`, `wpseo_woocommerce_*` + +### `shopify` +- **Products**: shopify-seo +- **Docs paths**: `docs/shopify/**`, `docs/features/schema/plugins/yoast-seo-shopify.md` +- **Source paths** (shopify-seo): `app/**` (main application code — PHP-based, non-Laravel), `cf-worker/**` (Cloudflare Worker integration), `config/**`, `data/**`, `shopify-seo.php` +- **Non-plugin architecture note**: `shopify-seo` is a Shopify app, not a WordPress plugin. It doesn't follow the `src/`-and-`classes/` convention of the WP plugins; file-path intuition for WP plugins doesn't transfer. Agent should lean heavily on the changelog entry for an RC rather than path heuristics. +- **Typical triggers**: Shopify-specific integration, schema, or internationalization changes. + +### `duplicate-post` +- **Products**: duplicate-post +- **Docs paths**: `docs/duplicate-post/**` +- **Source paths** (duplicate-post): `src/**` (admin, cli, handlers, ui, watchers), `compat/**`, root-level PHP files (`duplicate-post.php`, `admin-functions.php`, `common-functions.php`, `options.php`) +- **Symbol namespaces**: `duplicate_post_*` +- **Off-by-default**: do not run docs-sync on this product until explicitly enabled; its release cadence is independent from the SEO plugins. + +--- + +## Shared / cross-cutting files + +These files may be touched by *any* area's PR when appropriate, but are not themselves anchored to one area: + +- `docs/overview.md` — root landing page. Update when a new top-level section is added or when a linked URL changes. +- `sidebars.js` — Docusaurus navigation (at repo root, not under `docs/`). **Must be updated whenever a doc file is created, deleted, renamed, or moved**, otherwise the new page won't appear in navigation and `yarn build` may fail. +- `_redirects` — update when a doc URL changes (file rename or move). +- `src/css/custom.css` — never touched by the sync agent. + +**Rule**: if a PR plan creates or renames files, its `files` list must also include `sidebars.js` (kind: `update`) with an `edits` description explaining the navigation entry change. Missing this is a failure mode. + +## Out of scope for automated PRs + +These docs are only touched by the sync agent if the RC diff *explicitly* changes their subject matter (which is rare). Default behavior: do not propose changes even if some symbols appear to match. + +- `docs/development/**` — contributor guides, tooling, standards. Changes to Yoast's internal development practices, not to the plugin's public surface. +- `docs/overview.md` when the change is stylistic — only update on genuine information changes. + +## Cross-product ripple rules + +When a public filter or class moves between products (e.g., promoted from Premium to Free, or vice versa), the agent must: + +1. Open one PR plan for the area the symbol is *arriving* in (to add docs). +2. Open a second PR plan for the area the symbol is *leaving* (to add a deprecation/move note on the relevant deprecations page). + +If the move is between namespaces in the same product, do the deprecation-note PR only if the old symbol is already documented. + +--- + +## Noise exclusion (applies to all areas) + +The agent should ignore diff hunks under these paths when deciding what's doc-relevant. These are typically filtered at diff-compute time but the agent should double-check: + +- `tests/**`, `**/__tests__/**`, `**/__snapshots__/**`, `*.test.*`, `*.spec.*`, `*.stories.*` +- `vendor/**`, `node_modules/**`, `build/**` +- `languages/**`, `**/*.pot`, `**/*.po` +- `**/*.lock`, `package-lock.json`, `composer.lock`, `yarn.lock` +- `.github/**` in the source repo +- Version bumps in `wp-seo.php`, `wp-seo-main.php`, `package.json`, and similar bootstraps +- Readme changelog entries (`readme.txt` `== Changelog ==` section) — these are a *signal* that something changed and may cross-reference the diff, but are not themselves doc-relevant changes. + +## Failure modes to monitor in production + +Observed in the spikes or anticipated: + +1. **Over-fanout**: agent splits one feature across N PRs when N=1 is correct. Fix: tighten area boundaries in this file. +2. **Wrong area attribution**: Schema Aggregator placed at top-level instead of nested under `schema/`. Fix: explicit `docs_paths` above prevent this. +3. **Missed cross-cutting effect**: a new feature that touches two areas (e.g., Schema Aggregator affecting robots.txt). Agent handled this correctly in Spike C by producing two PR plans; keep the behavior. +4. **Missed `sidebars.js` update**: agent creates a new file but forgets to wire it into navigation. `yarn build` catches this as a CI failure, but a clean PR should already include it. +5. **Hallucinated symbols**: agent invents a filter name. Defense: always grep the filtered diff for the exact symbol before writing it into the PR; if it isn't there, don't write it. From fd287197dc4ee946cd48764c39040b722c408970 Mon Sep 17 00:00:00 2001 From: Enrico Battocchi <enrico@yoast.com> Date: Fri, 24 Apr 2026 17:18:50 +0200 Subject: [PATCH 2/4] Add coverage-gap self-reporting and drop AI Brand Insights from map Two refinements in response to review: - Agent now detects and reports "AGENT_MAP.md coverage gaps" during every RC run. A coverage gap is a hunk that looks like public surface (new apply_filters / do_action, new REST route, new top-level src/<subsystem>/ file with public classes) whose path or symbol isn't covered by any area's source_paths or symbol_namespaces. Listed in the run-summary comment under a "Coverage gaps observed" section only when present. Informational; does not block the run. Turns every RC into a free audit of the map. - Removed AI Brand Insights from AGENT_MAP.md's Product table and from the `ai` area's product list. Rationale: the developer portal currently has no feature-spec docs for it (only a changelog), so every docs-sync run on the product would reliably produce zero PRs. Keeping it in the map would waste compute and review attention on unambiguously no-op runs. Added a note in the `ai` area describing how to re-add it (product table entry plus source paths for ai-insights-api and ai-insights-frontend, plus a split-product rule) when/if feature docs land. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- .github/claude-agent/run.md | 19 ++++++++++++++++++- AGENT_MAP.md | 38 ++++++++++++++++++------------------- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/.github/claude-agent/run.md b/.github/claude-agent/run.md index 8af96dbe..adc77c44 100644 --- a/.github/claude-agent/run.md +++ b/.github/claude-agent/run.md @@ -11,7 +11,7 @@ You are the RC docs-sync agent for Yoast's developer portal. A Release Candidate - **`AGENT_MAP.md`** (in this repo root) — the source of truth for feature areas, docs paths, source paths, and symbol namespaces. Read this first. - **`docs/`** — the current state of the developer portal docs. -- **`$BUNDLE_DIR/<source-repo>/rc.diff.filtered`** — noise-filtered diff of the RC against the previous release, one per source repo. (For `ai-brand-insights` there are two.) +- **`$BUNDLE_DIR/<source-repo>/rc.diff.filtered`** — noise-filtered diff of the RC against the previous release. One subdirectory per source repo. Most products have a single source repo; if a future product ships a split-repo architecture, there will be multiple and the agent should treat all of them as a single RC unit. - **`$BUNDLE_DIR/<source-repo>/rc.diff.full`** — unfiltered diff for cross-check. - **`$BUNDLE_DIR/<source-repo>/rc.diff.stat`** — `git diff --stat` summary. - **`$BUNDLE_DIR/<source-repo>/changelog.source`** — the product's user-facing changelog file. Find the entry for `$RC_TAG` and treat it as the "why" complementing the diff's "what". @@ -55,6 +55,22 @@ Group findings by area. Produce an internal list of PR plans, one per affected a - Never touch `docs/development/**` or `docs/duplicate-post/**` (unless `PRODUCT` is `duplicate-post`). - If a PR plan creates or renames doc files, it must also include `sidebars.js` with an update describing the navigation entry change. +### Step 1.5 — Coverage-gap detection + +While triaging, additionally collect any **AGENT_MAP.md coverage gaps** you observe. A coverage gap is a hunk that looks like public surface — specifically: + +- A new `apply_filters('<symbol>', …)` or `do_action('<symbol>', …)` call whose source file path does NOT match any area's `source_paths`, AND whose symbol prefix does NOT match any area's `symbol_namespaces`. +- A new file under a top-level `src/<subsystem>/**` path that no area's `source_paths` covers, when the file clearly contains new public classes/interfaces/routes (not internal refactors). +- A new REST route registration (`register_rest_route(...)`) at a path no existing doc references. + +Do NOT flag internal refactors, private helpers, test files, generated code, or symbols whose path IS covered by some area even if you chose not to open a PR for them this run. The goal is surfacing *missing* map entries, not second-guessing triage decisions. + +List each gap as one bullet with: the source path where you observed it, the symbol or route that prompted the flag, and a one-line hypothesis of which area it might belong to (or "new area?" if none fit). + +Emit these in the run summary in Step 4 under a heading **"Coverage gaps observed"**. If there are no gaps, omit the heading entirely — don't emit an empty section. + +The gaps are informational; they do NOT block the run. A maintainer reviewing the tracking issue will decide whether to update `AGENT_MAP.md` in a separate PR. + ### Step 2 — Authoring (only if PR plans exist) For each PR plan, in order: @@ -101,6 +117,7 @@ The body after the marker should contain: - Symbol index size, count of new symbols observed in diff. - One bullet per PR plan: area, title, PR link. - If zero PRs: a one-paragraph explanation of what the RC contained and why no doc changes are needed (cite the changelog entry and top-level diff areas). +- A **"Coverage gaps observed"** section iff you flagged any in Step 1.5. Omit the heading entirely when there are none. **If you fail to post the comment with the marker, the next scheduled run will re-process this RC.** Posting the marker is the acknowledgement of completion. diff --git a/AGENT_MAP.md b/AGENT_MAP.md index 2f67de07..2fb48f66 100644 --- a/AGENT_MAP.md +++ b/AGENT_MAP.md @@ -22,21 +22,24 @@ Each doc file belongs to **exactly one area**. One PR plan per affected area. If ## Product → source repo -| Product slug | Display name | GitHub repo(s) | -|-------------------------|--------------------------|------------------------------------------------------------------------| -| wordpress-seo | Yoast SEO | `Yoast/wordpress-seo` | -| wordpress-seo-premium | Yoast SEO Premium | `Yoast/wordpress-seo-premium` | -| wordpress-seo-local | Yoast Local SEO | `Yoast/wordpress-seo-local` | -| wpseo-news | Yoast News SEO | `Yoast/wpseo-news` | -| wpseo-video | Yoast Video SEO | `Yoast/wpseo-video` | -| wpseo-woocommerce | Yoast WooCommerce SEO | `Yoast/wpseo-woocommerce` | -| shopify-seo | Yoast SEO for Shopify | `Yoast/shopify-seo` | -| duplicate-post | Yoast Duplicate Post | `Yoast/duplicate-post` | -| ai-brand-insights | Yoast AI Brand Insights | `Yoast/ai-insights-api` + `Yoast/ai-insights-frontend` (split product) | +| Product slug | Display name | GitHub repo(s) | +|-------------------------|--------------------------|---------------------------------| +| wordpress-seo | Yoast SEO | `Yoast/wordpress-seo` | +| wordpress-seo-premium | Yoast SEO Premium | `Yoast/wordpress-seo-premium` | +| wordpress-seo-local | Yoast Local SEO | `Yoast/wordpress-seo-local` | +| wpseo-news | Yoast News SEO | `Yoast/wpseo-news` | +| wpseo-video | Yoast Video SEO | `Yoast/wpseo-video` | +| wpseo-woocommerce | Yoast WooCommerce SEO | `Yoast/wpseo-woocommerce` | +| shopify-seo | Yoast SEO for Shopify | `Yoast/shopify-seo` | +| duplicate-post | Yoast Duplicate Post | `Yoast/duplicate-post` | Display names are the human-readable product names used in PR titles and tracking issue summaries. They mirror the names used in `docusaurus.config.js` changelog plugin entries. -`ai-brand-insights` is the only product with more than one source repo — the agent must consider diffs from both when running on its RCs. Product slug matches the changelog id in `docusaurus.config.js`, which differs from the repo name in several cases (`wordpress-seo-local` ↔ `Local SEO`, `wpseo-*` ↔ `News/Video/WooCommerce SEO`, `shopify-seo` ↔ `Yoast SEO for Shopify`, `ai-brand-insights` ↔ `AI Brand Insights`). +Products that have a changelog in this repo but **no feature docs** (e.g. AI Brand Insights) are intentionally excluded from this table. Add them here only when feature-spec docs are introduced in `docs/`; otherwise every RC run on the product would reliably produce zero PRs, wasting compute and review attention. + +Product slug is the stable identifier used throughout this file and the workflow. It does not always match the repo name — see how `wordpress-seo-local` ↔ `Local SEO`, `wpseo-*` ↔ `News/Video/WooCommerce SEO`, `shopify-seo` ↔ `Yoast SEO for Shopify`. Keep the slug consistent with the corresponding changelog plugin id in `docusaurus.config.js` so the two pieces line up. + +No currently-listed product has more than one source repo. If one is ever added (for example a split API+frontend product), the workflow and agent will need an extra rule: clone diffs from both repos, pass them to the agent labeled by repo, and treat the set as a single RC unit. --- @@ -168,16 +171,13 @@ Display names are the human-readable product names used in PR titles and trackin - **Typical triggers**: new integration added (IndexNow, site-connections, Algolia, etc.); change to ping/notify behavior. ### `ai` -- **Products**: wordpress-seo, wordpress-seo-premium, ai-brand-insights (split across `Yoast/ai-insights-api` + `Yoast/ai-insights-frontend`) +- **Products**: wordpress-seo, wordpress-seo-premium - **Docs paths**: `docs/features/ai/**` - **Source paths** (wordpress-seo): `src/ai-*/**`, `src/generators/ai*`, `src/integrations/ai*` - **Source paths** (wordpress-seo-premium): `src/ai/**` -- **Source paths** (ai-insights-api): Laravel app — `app/**` (controllers, models, services, jobs), `routes/**`, `config/**`, `database/migrations/**`, `resources/views/**` (Blade templates) -- **Source paths** (ai-insights-frontend): Vite/React/TypeScript — `src/api/**`, `src/components/**`, `src/pages/**`, `src/hooks/**`, `src/contexts/**`, `src/lib.tsx`, `src/orval/**` (auto-generated API clients — changes here reflect upstream API changes) -- **Split-product workflow rule**: For `ai-brand-insights` RCs the workflow must pull diffs from **both** repos (ai-insights-api and ai-insights-frontend) and pass them to the agent as a single input, clearly labeled by repo. Only the API repo emits user-observable public surface; frontend changes are informational unless they alter documented behavior. -- **Docs-coverage gap note**: At map-drafting time `docs/features/ai/` contained only `ai-errors.md`. There is no feature-spec doc for AI Brand Insights yet. Until such docs exist, RC runs for `ai-brand-insights` will almost always yield `pr_plans: []` — this is correct behavior, not a failure. If AI Brand Insights starts being documented in the portal, expand this area's docs_paths accordingly. -- **Symbol namespaces** (WP side): `wpseo_ai_*` -- **Typical triggers**: new AI error code; new AI feature exposing a filter; change to request/retry behavior documented in `ai-errors.md`; new public REST endpoint in `ai-insights-api`. +- **Symbol namespaces**: `wpseo_ai_*` +- **Typical triggers**: new AI error code; new AI feature exposing a filter; change to request/retry behavior documented in `ai-errors.md`. +- **Note**: AI Brand Insights is an adjacent product with its own changelog in this repo but no feature-spec docs under `docs/features/ai/`. It is deliberately not in this area's product list. When/if feature docs land (e.g. a functional specification for AI Brand Insights), re-add the product to the Product table and expand this area's products + source paths to include `Yoast/ai-insights-api` (Laravel — `app/**`, `routes/**`) and `Yoast/ai-insights-frontend` (Vite/React — `src/**`), plus a split-product workflow rule. ### `alternate-formats` - **Products**: wordpress-seo From 48caa1af078ddd485f556f9cefccce76cc46c5a2 Mon Sep 17 00:00:00 2001 From: Enrico Battocchi <enrico@yoast.com> Date: Fri, 24 Apr 2026 17:22:39 +0200 Subject: [PATCH 3/4] Include Duplicate Post as a regular product in the rollout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the "off-by-default" flag from the duplicate-post area. It has docs in this repo (docs/duplicate-post/**) and an active release cadence in Yoast/duplicate-post, so there's no reason to treat it differently from any other documented product. Simplify the agent's "never touch" rule to docs/development/** only — the per-area docs_paths matching already ensures docs/duplicate-post/ is only touched when PRODUCT=duplicate-post. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- .github/claude-agent/run.md | 2 +- AGENT_MAP.md | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/claude-agent/run.md b/.github/claude-agent/run.md index adc77c44..41b73824 100644 --- a/.github/claude-agent/run.md +++ b/.github/claude-agent/run.md @@ -52,7 +52,7 @@ Group findings by area. Produce an internal list of PR plans, one per affected a **Rules:** - Each doc file belongs to exactly one area. No file may appear in two PR plans. - Maximum 5 PR plans per run. If the triage produces more, consolidate or escalate (comment on the tracking issue explaining what you dropped). -- Never touch `docs/development/**` or `docs/duplicate-post/**` (unless `PRODUCT` is `duplicate-post`). +- Never touch `docs/development/**` — those are contributor/meta docs, not product feature docs, and are out of scope for RC-triggered updates. - If a PR plan creates or renames doc files, it must also include `sidebars.js` with an update describing the navigation entry change. ### Step 1.5 — Coverage-gap detection diff --git a/AGENT_MAP.md b/AGENT_MAP.md index 2fb48f66..c10e80a0 100644 --- a/AGENT_MAP.md +++ b/AGENT_MAP.md @@ -276,7 +276,6 @@ No currently-listed product has more than one source repo. If one is ever added - **Docs paths**: `docs/duplicate-post/**` - **Source paths** (duplicate-post): `src/**` (admin, cli, handlers, ui, watchers), `compat/**`, root-level PHP files (`duplicate-post.php`, `admin-functions.php`, `common-functions.php`, `options.php`) - **Symbol namespaces**: `duplicate_post_*` -- **Off-by-default**: do not run docs-sync on this product until explicitly enabled; its release cadence is independent from the SEO plugins. --- From 8766e1cfd4debd4988c90e96f321779daa15dfc8 Mon Sep 17 00:00:00 2001 From: Enrico Battocchi <enrico@yoast.com> Date: Fri, 24 Apr 2026 17:24:42 +0200 Subject: [PATCH 4/4] Drop all remaining AI Brand Insights mentions from AGENT_MAP The product has no feature docs in this repo and will not gain any, so per-product notes about how to re-integrate it later are noise. Replaced the two AI-Brand-Insights-specific notes (Product table + ai area) with a generic rule stating only products with feature docs belong in the table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- AGENT_MAP.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/AGENT_MAP.md b/AGENT_MAP.md index c10e80a0..7d8f1b40 100644 --- a/AGENT_MAP.md +++ b/AGENT_MAP.md @@ -35,7 +35,7 @@ Each doc file belongs to **exactly one area**. One PR plan per affected area. If Display names are the human-readable product names used in PR titles and tracking issue summaries. They mirror the names used in `docusaurus.config.js` changelog plugin entries. -Products that have a changelog in this repo but **no feature docs** (e.g. AI Brand Insights) are intentionally excluded from this table. Add them here only when feature-spec docs are introduced in `docs/`; otherwise every RC run on the product would reliably produce zero PRs, wasting compute and review attention. +Only products with feature-spec docs under `docs/` belong in this table. A product whose only presence here is a changelog produces zero-PR runs every time, which adds noise to the tracking issue without value — skip it. Product slug is the stable identifier used throughout this file and the workflow. It does not always match the repo name — see how `wordpress-seo-local` ↔ `Local SEO`, `wpseo-*` ↔ `News/Video/WooCommerce SEO`, `shopify-seo` ↔ `Yoast SEO for Shopify`. Keep the slug consistent with the corresponding changelog plugin id in `docusaurus.config.js` so the two pieces line up. @@ -177,7 +177,6 @@ No currently-listed product has more than one source repo. If one is ever added - **Source paths** (wordpress-seo-premium): `src/ai/**` - **Symbol namespaces**: `wpseo_ai_*` - **Typical triggers**: new AI error code; new AI feature exposing a filter; change to request/retry behavior documented in `ai-errors.md`. -- **Note**: AI Brand Insights is an adjacent product with its own changelog in this repo but no feature-spec docs under `docs/features/ai/`. It is deliberately not in this area's product list. When/if feature docs land (e.g. a functional specification for AI Brand Insights), re-add the product to the Product table and expand this area's products + source paths to include `Yoast/ai-insights-api` (Laravel — `app/**`, `routes/**`) and `Yoast/ai-insights-frontend` (Vite/React — `src/**`), plus a split-product workflow rule. ### `alternate-formats` - **Products**: wordpress-seo