From 0ec5de0a70279682e3c80a8a8d16b8e67a40a538 Mon Sep 17 00:00:00 2001 From: Arber Xhindoli <14798762+arberx@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:13:57 -0400 Subject: [PATCH] =?UTF-8?q?feat!:=20remove=20letter=20grades=20and=20facto?= =?UTF-8?q?r=20status,=20pure=200=E2=80=93100=20score=20(3.0.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the A+–F letter grade and pass/partial/fail factor status from the audit output, leaving a pure 0–100 score per factor plus a weighted overall score. Removes the grade-family JSON fields (overallGrade, aggregateGrade, per-page overallGrade, ScoredFactor.grade/status, CrossCuttingIssue.avgGrade, AgentSummary.grade) and the scoreToGrade/scoreToStatus exports; PrioritizedFix.avgGrade becomes avgScore. Bumps schemaVersion to 2.0 and the package to 3.0.0, with formatters, tests, and docs updated to match. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 10 ++++++++ CLAUDE.md | 2 +- README.md | 6 ++--- docs/api.md | 11 ++++----- docs/cli.md | 4 +-- docs/scoring.md | 23 +++++++++--------- docs/skill.md | 2 +- package.json | 2 +- skills/aeo/SKILL.md | 14 +++++------ src/agent-summary.ts | 2 -- src/formatters/markdown.ts | 32 ++++++++++++------------ src/formatters/text.ts | 39 +++++++++++++++--------------- src/index.ts | 11 ++++----- src/schema.ts | 2 +- src/scoring.ts | 36 +++------------------------ src/sitemap.ts | 9 ++----- src/static-audit.ts | 5 +--- src/types.ts | 22 +++++++---------- test/agent-summary.test.ts | 10 ++------ test/analyzers/legacy.test.ts | 8 +++--- test/cli-require-meta.test.ts | 4 --- test/critical-defects.test.ts | 8 ++---- test/e2e/cli.test.ts | 39 ++++++++++++++---------------- test/sitemap-cross-cutting.test.ts | 3 --- test/sitemap-options.test.ts | 1 - test/sitemap-rewrite.test.ts | 1 - test/static-audit.test.ts | 2 +- 27 files changed, 125 insertions(+), 183 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a26b22e..7cab329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 3.0.0 (2026-06-08) + +### Breaking +- **Letter grades removed — the audit is now a pure 0–100 score.** The `grade`-family fields are gone from the JSON: `AuditReport.overallGrade`, `SitemapAuditReport.aggregateGrade`, `SitemapPageResult.overallGrade`, `ScoredFactor.grade`, `CrossCuttingIssue.avgGrade`, and `AgentSummary.grade` (so `--format agent` now emits `{ schemaVersion, tool, mode, url, score, pass, criticalDefectCount, issues }`). `PrioritizedFix.avgGrade` (a letter) is replaced by **`PrioritizedFix.avgScore`** (a 0–100 number, cross-cutting fixes only). The `scoreToGrade()` export is removed from `@ainyc/aeo-audit/scoring`. Migrate by reading the 0–100 `overallScore` / `aggregateScore` / per-factor `score` / `avgScore` and thresholding to your own bands. +- **Per-factor `status` band removed.** `ScoredFactor.status` (`'pass' | 'partial' | 'fail'`) and the `scoreToStatus()` export are gone. `ScoredFactor` is now structurally identical to `RawFactorResult` (`id, name, weight, score, findings, recommendations`). Derive any banding from `score` directly. (`SitemapPageResult.status` — `'success' | 'error'` — is unrelated and unchanged, as is the `AgentSummary.pass` ≥ 70 gate and all CLI exit codes.) +- **`schemaVersion` bumped to `2.0`** to mark the removed fields. Parsers pinned to `1.x` should expect `grade`/`status` to be absent. + +### Changed +- The single-page report `summary` now reads `Overall score /100. …` instead of `Overall grade . …`. Text and markdown reports show the numeric score (and a score-derived color/icon) wherever a letter grade previously appeared; the markdown factor table drops its `Grade` and `Status` columns, and the per-page table drops `Grade` (keeping the `success`/`error` `Status`). + ## 2.1.0 (2026-06-03) ### Added diff --git a/CLAUDE.md b/CLAUDE.md index c08571e..1f6c905 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,7 +32,7 @@ pnpm lint # Run linter ``` src/ index.ts # Main entry: runAeoAudit(url, options) - scoring.ts # Factor definitions, weights, grade calculation + scoring.ts # Factor definitions, weights, score calculation fetch-page.ts # URL fetching with SSRF protection errors.ts # AeoAuditError class cli.ts # CLI argument parsing diff --git a/README.md b/README.md index 947eba4..26f5db8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **The most comprehensive open-source technical AEO (Answer Engine Optimization) audit tool.** Scores any website across 16 ranking factors that decide whether AI answer engines (ChatGPT, Perplexity, Gemini, Claude) will cite your content. -- Grade any URL across **16 AEO factors**: structured data, `llms.txt`, E-E-A-T, extractability, snippet eligibility, and more. [Scoring](docs/scoring.md) +- Score any URL across **16 AEO factors**: structured data, `llms.txt`, E-E-A-T, extractability, snippet eligibility, and more. [Scoring](docs/scoring.md) - Audit a **whole site** from its sitemap; per-page findings roll up into ranked fixes. [Sitemap mode](docs/cli.md#sitemap-mode) - 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) @@ -21,7 +21,7 @@ Website: [canonry.ai](https://canonry.ai) npx @ainyc/aeo-audit https://example.com ``` -Prints a graded report. Common variations: +Prints a scored report. Common variations: ```bash # Every page in the sitemap, site-wide issues only @@ -51,7 +51,7 @@ Modes: audit, fix, schema, `llms.txt`, monitor. See the [skill guide](docs/skill | Doc | What's in it | |---|---| | [CLI reference](docs/cli.md) | Every flag, mode, and exit code | -| [Scoring](docs/scoring.md) | The 16 factors, weights, grading scale | +| [Scoring](docs/scoring.md) | The 16 factors, weights, score bands | | [Programmatic API](docs/api.md) | `runAeoAudit`, `runSitemapAudit`, `runStaticAudit` | | [Skill](docs/skill.md) | `/aeo` modes and install | | [Changelog](CHANGELOG.md) | Release history | diff --git a/docs/api.md b/docs/api.md index 54e5979..dc6b447 100644 --- a/docs/api.md +++ b/docs/api.md @@ -18,7 +18,6 @@ const report = await runAeoAudit('https://example.com/specific-page', { // Scoped to that exact host; redirects/sitemap entries to other private hosts stay blocked. }) -console.log(report.overallGrade) // 'A+' console.log(report.overallScore) // 98 console.log(report.factors) // Array of factor results with scores, findings, recommendations ``` @@ -33,8 +32,8 @@ const report = await runSitemapAudit('https://example.com', { factors: ['schema-validity', 'structured-data'], // Optional subset }) -console.log(report.schemaVersion) // '1.1', JSON shape version (see "Machine-readable output") -console.log(report.aggregateGrade) // 'B+' +console.log(report.schemaVersion) // '2.0', JSON shape version (see "Machine-readable output") +console.log(report.aggregateScore) // 84 console.log(report.pagesAudited) // 22 console.log(report.criticalDefects) // Binary per-page defects (multiple/missing H1, missing title/meta), grouped by defect console.log(report.crossCuttingIssues) // Per-factor rollup with affectedUrls for every recommendation @@ -43,7 +42,7 @@ console.log(report.prioritizedFixes) // Ranked PrioritizedFix[]: critical defe Each entry in `crossCuttingIssues[].topIssues` carries a `recommendation` plus the exact `affectedUrls` so you can attribute each problem to specific pages, e.g. "FAQPage duplicate" pointing at every blog post that has it. -`criticalDefects` surfaces **binary structural defects by impact, not prevalence**. The cross-cutting rollup ranks by how many pages a factor affects, so an unambiguous one-line-fix defect on a single important page (a homepage split across four `

`s, or a `/contact-us` page with none) would otherwise be averaged into a passing factor grade and excluded from `prioritizedFixes`. Each group names the offending pages (homepage and high sitemap-`priority` pages first), and the critical-severity ones lead `prioritizedFixes`. +`criticalDefects` surfaces **binary structural defects by impact, not prevalence**. The cross-cutting rollup ranks by how many pages a factor affects, so an unambiguous one-line-fix defect on a single important page (a homepage split across four `

`s, or a `/contact-us` page with none) would otherwise be averaged into a passing factor score and excluded from `prioritizedFixes`. Each group names the offending pages (homepage and high sitemap-`priority` pages first), and the critical-severity ones lead `prioritizedFixes`. ### Machine-readable output (for AI agents) @@ -64,9 +63,9 @@ const result = await runStaticAudit('./out', { }) if (result.kind === 'single') { - console.log(result.report.overallGrade) // single .html file → AuditReport + console.log(result.report.overallScore) // single .html file → AuditReport } else { - console.log(result.report.aggregateGrade) // directory → SitemapAuditReport shape + console.log(result.report.aggregateScore) // directory → SitemapAuditReport shape console.log(result.report.criticalDefects) console.log(result.report.crossCuttingIssues) } diff --git a/docs/cli.md b/docs/cli.md index d99594e..135b255 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -26,7 +26,7 @@ npx @ainyc/aeo-audit https://example.com --sitemap --format agent `--format json` is the contract for programmatic and agent consumers: every report carries a `schemaVersion` (so a parser can detect breaking shape drift) and sitemap reports expose a `criticalDefects` rollup plus a ranked `prioritizedFixes` array of structured objects. See [api.md](api.md#machine-readable-output-for-ai-agents) for the field shapes. -`--format agent` returns just the decision, not the report: `{ schemaVersion, tool, mode, url, score, grade, pass, criticalDefectCount, issues }`, where `issues` is the ranked `PrioritizedFix[]` (critical defects first, then cross-cutting by prevalence). It omits the per-factor and per-page detail so an agent can act without averaging and re-ranking scores itself. Works for single-URL, sitemap, and static-output audits; in `--detect-platform` mode it falls back to the structured JSON. +`--format agent` returns just the decision, not the report: `{ schemaVersion, tool, mode, url, score, pass, criticalDefectCount, issues }`, where `issues` is the ranked `PrioritizedFix[]` (critical defects first, then cross-cutting by prevalence). It omits the per-factor and per-page detail so an agent can act without averaging and re-ranking scores itself. Works for single-URL, sitemap, and static-output audits; in `--detect-platform` mode it falls back to the structured JSON. ## Running a subset of factors @@ -99,7 +99,7 @@ Auto-discovery checks `/sitemap.xml` → `/sitemap-index.xml` → `Sitemap:` dir When the sitemap has more URLs than `--limit`, the run audits the highest-priority pages and prints a notice to stderr listing how many were skipped and how to audit them all. -A **Critical Defects** section lists binary, one-line-fix structural defects (an `

` count other than one, a missing ``, a missing meta description) surfaced **regardless of how few pages they affect**, with the offending pages named (homepage and high sitemap-`priority` pages first). These would otherwise be averaged into a passing factor grade and excluded from the prevalence-ranked fixes; the critical-severity ones also lead the prioritized fix list. The section is shown even with `--top-issues`. See the machine-readable shapes in [api.md](api.md#machine-readable-output-for-ai-agents). +A **Critical Defects** section lists binary, one-line-fix structural defects (an `<h1>` count other than one, a missing `<title>`, a missing meta description) surfaced **regardless of how few pages they affect**, with the offending pages named (homepage and high sitemap-`priority` pages first). These would otherwise be averaged into a passing factor score and excluded from the prevalence-ranked fixes; the critical-severity ones also lead the prioritized fix list. The section is shown even with `--top-issues`. See the machine-readable shapes in [api.md](api.md#machine-readable-output-for-ai-agents). The optional in-process factors are honored per page: pass `--include-geo` and/or `--include-agent-skills` to add them to every audited page. `--lighthouse` is the exception: it cannot be combined with `--sitemap` because each PageSpeed Insights call takes 15-30s. diff --git a/docs/scoring.md b/docs/scoring.md index d6b66f2..80700f6 100644 --- a/docs/scoring.md +++ b/docs/scoring.md @@ -44,15 +44,14 @@ These are excluded by default; when included, the weights renormalize. > **Note on Google's guidance.** Google's [AI features and your website][google-aeo] guide says `llms.txt` and heavy structured data aren't required for AI Overviews or AI Mode. We still score them: Google is one engine; ChatGPT, Perplexity, and Claude do rely on them. Snippet eligibility is the one hard gate Google enforces: a page must be indexable and snippet-eligible to appear in AI features. -## Grading scale - -| Grade | Score | Meaning | -|-------|-------|---------| -| A+ | 97-100 | Exceptional AEO readiness | -| A / A- | 90-96 | Strong foundation | -| B+/B/B- | 80-89 | Good with clear gaps | -| C+/C/C- | 70-79 | Moderate, needs work | -| D+/D/D- | 60-69 | Weak | -| F | <60 | Critical | - -The CLI exits `0` for a score ≥ 70 and `1` below; see [Exit codes](cli.md#exit-codes). +## Score bands + +The audit emits a **0–100 score** for every factor and a weighted **overall score** (0–100) — there are no letter grades or pass/partial/fail labels. Threshold the raw score to whatever bands suit your use. For reference, the CLI's own conventions: + +| Score | Meaning | +|-------|---------| +| 70–100 | Strong — meets the CLI's pass gate | +| 40–69 | Moderate — clear gaps remain | +| 0–39 | Weak — major work needed | + +The CLI exits `0` for an overall score ≥ 70 and `1` below; see [Exit codes](cli.md#exit-codes). diff --git a/docs/skill.md b/docs/skill.md index 234725e..eec694f 100644 --- a/docs/skill.md +++ b/docs/skill.md @@ -12,7 +12,7 @@ ClawHub package: [arberx/aeo](https://clawhub.ai/arberx/aeo) ## Modes -- `audit`: grading and diagnosis +- `audit`: scoring and diagnosis - `fix`: code changes after an audit - `schema`: JSON-LD validation - `llms`: generate `llms.txt` and `llms-full.txt` diff --git a/package.json b/package.json index cc3c13a..4bea2d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ainyc/aeo-audit", - "version": "2.1.0", + "version": "3.0.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", diff --git a/skills/aeo/SKILL.md b/skills/aeo/SKILL.md index 3b43817..f0445f1 100644 --- a/skills/aeo/SKILL.md +++ b/skills/aeo/SKILL.md @@ -37,7 +37,7 @@ npx @ainyc/aeo-audit@1 "<url>" [flags] --format json ## Modes -- `audit`: grade and diagnose a site +- `audit`: score and diagnose a site - `fix`: apply code changes after an audit - `schema`: validate JSON-LD and entity consistency - `llms`: create or improve `llms.txt` and `llms-full.txt` @@ -84,7 +84,7 @@ Use for broad requests such as "audit this site" or "why am I not being cited?" npx @ainyc/aeo-audit@1 "<url>" [flags] --format json ``` 2. Return: - - Overall grade and score + - Overall score - Short summary - Factor breakdown - Top strengths @@ -117,15 +117,15 @@ Flags: Pages are audited with bounded concurrency (5 in flight) to avoid hammering the target origin. Returns: -- Per-page scores and grades -- **Critical defects** — binary, one-line-fix structural defects (an `<h1>` count other than one, a missing `<title>`, a missing meta description) surfaced **regardless of how few pages they affect**, with the offending pages named (homepage and high sitemap-`priority` pages first). These would otherwise be averaged into a passing factor grade; the JSON field is `criticalDefects` and critical-severity ones are also promoted to the top of `prioritizedFixes`. Shown even with `--top-issues`. +- Per-page scores +- **Critical defects** — binary, one-line-fix structural defects (an `<h1>` count other than one, a missing `<title>`, a missing meta description) surfaced **regardless of how few pages they affect**, with the offending pages named (homepage and high sitemap-`priority` pages first). These would otherwise be averaged into a passing factor score; the JSON field is `criticalDefects` and critical-severity ones are also promoted to the top of `prioritizedFixes`. Shown even with `--top-issues`. - Cross-cutting issues (factors failing across multiple pages) -- Aggregate score and grade +- Aggregate score - Prioritized fixes (critical defects first, then ranked by site-wide impact) #### Machine-readable output (for agents) -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: +Use `--format json` for the full report, or **`--format agent`** for just the decision: `{ schemaVersion, tool, mode, url, score, 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 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. @@ -220,7 +220,7 @@ Use when the user wants code changes applied after the audit. ```bash npx @ainyc/aeo-audit@1 "<url>" [flags] --format json ``` -2. Find factors with status `partial` or `fail`. +2. Find factors scoring below 70 (lowest first). 3. Apply targeted fixes in the current codebase. 4. Prioritize: - Structured data and schema completeness diff --git a/src/agent-summary.ts b/src/agent-summary.ts index 0b70708..62cb391 100644 --- a/src/agent-summary.ts +++ b/src/agent-summary.ts @@ -25,7 +25,6 @@ export function agentSummaryFromAudit(report: AuditReport): AgentSummary { mode: 'single', url: report.finalUrl, score: report.overallScore, - grade: report.overallGrade, pass: report.overallScore >= PASS_THRESHOLD, criticalDefectCount: criticalDefects.filter((g) => g.severity === 'critical').length, issues, @@ -40,7 +39,6 @@ export function agentSummaryFromSitemap(report: SitemapAuditReport): AgentSummar mode: 'sitemap', url: report.sitemapUrl, score: report.aggregateScore, - grade: report.aggregateGrade, pass: report.aggregateScore >= PASS_THRESHOLD, criticalDefectCount: report.criticalDefects.filter((g) => g.severity === 'critical').length, issues: report.prioritizedFixes, diff --git a/src/formatters/markdown.ts b/src/formatters/markdown.ts index 6e80f2e..a9a8104 100644 --- a/src/formatters/markdown.ts +++ b/src/formatters/markdown.ts @@ -15,7 +15,7 @@ export function formatMarkdown(report: AuditReport): string { lines.push(`# AEO Audit Report`) lines.push(``) lines.push(`**URL:** ${report.finalUrl}`) - lines.push(`**Overall Grade:** ${report.overallGrade} (${report.overallScore}/100)`) + lines.push(`**Overall Score:** ${report.overallScore}/100`) lines.push(`**Audited:** ${report.auditedAt}`) lines.push(``) lines.push(`## Summary`) @@ -24,11 +24,11 @@ export function formatMarkdown(report: AuditReport): string { lines.push(``) lines.push(`## Factor Breakdown`) lines.push(``) - lines.push(`| Factor | Weight | Score | Grade | Status |`) - lines.push(`|--------|--------|-------|-------|--------|`) + lines.push(`| Factor | Weight | Score |`) + lines.push(`|--------|--------|-------|`) for (const factor of report.factors) { - lines.push(`| ${factor.name} | ${factor.weight}% | ${factor.score} | ${factor.grade} | ${factor.status} |`) + lines.push(`| ${factor.name} | ${factor.weight}% | ${factor.score} |`) } lines.push(``) @@ -40,7 +40,7 @@ export function formatMarkdown(report: AuditReport): string { lines.push(`## Strengths`) lines.push(``) for (const factor of strengths) { - lines.push(`- **${factor.name}** (${factor.grade}): ${factor.findings.filter((f) => f.type === 'found').map((f) => f.message).join(' ')}`) + lines.push(`- **${factor.name}** (${factor.score}/100): ${factor.findings.filter((f) => f.type === 'found').map((f) => f.message).join(' ')}`) } lines.push(``) @@ -48,7 +48,7 @@ export function formatMarkdown(report: AuditReport): string { lines.push(``) for (const factor of opportunities) { const recs = factor.recommendations.slice(0, 2) - lines.push(`- **${factor.name}** (${factor.grade}): ${recs.join(' ')}`) + lines.push(`- **${factor.name}** (${factor.score}/100): ${recs.join(' ')}`) } lines.push(``) @@ -71,7 +71,7 @@ export function formatSitemapMarkdown(report: SitemapAuditReport, topIssuesOnly lines.push(`# AEO Sitemap Audit Report`) lines.push(``) lines.push(`**Sitemap:** ${report.sitemapUrl}`) - lines.push(`**Aggregate Grade:** ${report.aggregateGrade} (${report.aggregateScore}/100)`) + lines.push(`**Aggregate Score:** ${report.aggregateScore}/100`) lines.push(`**Pages:** ${report.pagesAudited} audited of ${report.pagesDiscovered} discovered (${report.pagesFiltered} filtered as non-HTML, ${report.pagesTruncated} truncated by --limit ${report.effectiveLimit})`) if (report.pagesTruncated > 0) { lines.push(``) @@ -83,15 +83,15 @@ export function formatSitemapMarkdown(report: SitemapAuditReport, topIssuesOnly if (!topIssuesOnly) { lines.push(`## Per-Page Scores`) lines.push(``) - lines.push(`| URL | Score | Grade | Status |`) - lines.push(`|-----|-------|-------|--------|`) + lines.push(`| URL | Score | Status |`) + lines.push(`|-----|-------|--------|`) for (const page of report.pages) { const url = page.url.length > 60 ? page.url.slice(0, 57) + '...' : page.url if (page.status === 'error') { - lines.push(`| ${url} | - | - | error: ${page.error} |`) + lines.push(`| ${url} | - | error: ${page.error} |`) } else { - lines.push(`| ${url} | ${page.overallScore} | ${page.overallGrade} | ${page.status} |`) + lines.push(`| ${url} | ${page.overallScore} | ${page.status} |`) } } @@ -122,12 +122,12 @@ export function formatSitemapMarkdown(report: SitemapAuditReport, topIssuesOnly if (report.crossCuttingIssues.length > 0) { lines.push(`## Cross-Cutting Issues`) lines.push(``) - lines.push(`| Factor | Avg Score | Avg Grade | Affected Pages |`) - lines.push(`|--------|-----------|-----------|----------------|`) + lines.push(`| Factor | Avg Score | Affected Pages |`) + lines.push(`|--------|-----------|----------------|`) for (const issue of report.crossCuttingIssues) { const pct = Math.round((issue.affectedPages / issue.totalPages) * 100) - lines.push(`| ${issue.factorName} | ${issue.avgScore} | ${issue.avgGrade} | ${issue.affectedPages}/${issue.totalPages} (${pct}%) |`) + lines.push(`| ${issue.factorName} | ${issue.avgScore} | ${issue.affectedPages}/${issue.totalPages} (${pct}%) |`) } lines.push(``) @@ -157,8 +157,8 @@ export function formatSitemapMarkdown(report: SitemapAuditReport, topIssuesOnly for (let i = 0; i < report.prioritizedFixes.length; i++) { const fix = report.prioritizedFixes[i] const tag = fix.severity ? `**[${fix.severity}]** ` : '' - const grade = fix.avgGrade ? ` (avg ${fix.avgGrade})` : '' - lines.push(`${i + 1}. ${tag}**${fix.title}**${grade} _(${fix.prevalencePct}% of pages)_ — ${fix.recommendation}`) + const avg = fix.avgScore !== undefined ? ` (avg ${fix.avgScore}/100)` : '' + lines.push(`${i + 1}. ${tag}**${fix.title}**${avg} _(${fix.prevalencePct}% of pages)_ — ${fix.recommendation}`) // Spell out every affected page — agents and humans both need the full set. for (const url of fix.affectedPages) { const home = isHomepageUrl(url) ? ' **(homepage)**' : '' diff --git a/src/formatters/text.ts b/src/formatters/text.ts index 603576a..4c49733 100644 --- a/src/formatters/text.ts +++ b/src/formatters/text.ts @@ -15,19 +15,18 @@ import type { PlatformCategory, PlatformConfidence, PlatformDetectionReport, - ScoredFactor, SitemapAuditReport, } from '../types.js' -function gradeColor(grade: string): string { - if (grade.startsWith('A')) return GREEN - if (grade.startsWith('B')) return YELLOW +function scoreColor(score: number): string { + if (score >= 70) return GREEN + if (score >= 40) return YELLOW return RED } -function statusIcon(status: ScoredFactor['status']): string { - if (status === 'pass') return `${GREEN}✓${RESET}` - if (status === 'partial') return `${YELLOW}~${RESET}` +function scoreIcon(score: number): string { + if (score >= 70) return `${GREEN}✓${RESET}` + if (score >= 40) return `${YELLOW}~${RESET}` return `${RED}✗${RESET}` } @@ -40,12 +39,12 @@ function bar(score: number, width = 20): string { export function formatText(report: AuditReport): string { const lines = [] - const gc = gradeColor(report.overallGrade) + const sc = scoreColor(report.overallScore) lines.push(``) lines.push(`${BOLD}AEO Audit Report${RESET}`) lines.push(`${DIM}${report.finalUrl}${RESET}`) lines.push(``) - lines.push(` ${BOLD}Grade:${RESET} ${gc}${BOLD}${report.overallGrade}${RESET} ${bar(report.overallScore, 30)} ${report.overallScore}/100`) + lines.push(` ${BOLD}Score:${RESET} ${sc}${BOLD}${report.overallScore}/100${RESET} ${bar(report.overallScore, 30)}`) lines.push(``) lines.push(`${BOLD}Factors${RESET}`) lines.push(`${'─'.repeat(70)}`) @@ -53,10 +52,10 @@ export function formatText(report: AuditReport): string { const sorted = [...report.factors].sort((a, b) => b.score - a.score) for (const factor of sorted) { - const icon = statusIcon(factor.status) - const fc = gradeColor(factor.grade) + const icon = scoreIcon(factor.score) + const fc = scoreColor(factor.score) const name = factor.name.padEnd(30) - lines.push(` ${icon} ${name} ${bar(factor.score)} ${fc}${factor.grade.padEnd(3)}${RESET} ${DIM}(${factor.weight}%)${RESET}`) + lines.push(` ${icon} ${name} ${bar(factor.score)} ${fc}${String(factor.score).padStart(3)}${RESET} ${DIM}(${factor.weight}%)${RESET}`) } lines.push(`${'─'.repeat(70)}`) @@ -87,12 +86,12 @@ export function formatText(report: AuditReport): string { export function formatSitemapText(report: SitemapAuditReport, topIssuesOnly = false): string { const lines = [] - const gc = gradeColor(report.aggregateGrade) + const sc = scoreColor(report.aggregateScore) lines.push(``) lines.push(`${BOLD}AEO Sitemap Audit Report${RESET}`) lines.push(`${DIM}${report.sitemapUrl}${RESET}`) lines.push(``) - lines.push(` ${BOLD}Aggregate Grade:${RESET} ${gc}${BOLD}${report.aggregateGrade}${RESET} ${bar(report.aggregateScore, 30)} ${report.aggregateScore}/100`) + lines.push(` ${BOLD}Aggregate Score:${RESET} ${sc}${BOLD}${report.aggregateScore}/100${RESET} ${bar(report.aggregateScore, 30)}`) lines.push(` ${DIM}${report.pagesAudited} pages audited of ${report.pagesDiscovered} discovered (${report.pagesFiltered} filtered, ${report.pagesTruncated} truncated by --limit ${report.effectiveLimit})${RESET}`) if (report.pagesTruncated > 0) { lines.push(` ${DIM}Note: ${report.pagesTruncated} additional pages skipped by --limit. Pass --limit ${Math.max(report.pagesDiscovered, 9999)} to audit them all.${RESET}`) @@ -110,8 +109,8 @@ export function formatSitemapText(report: SitemapAuditReport, topIssuesOnly = fa lines.push(` ${RED}✗${RESET} ${url.padEnd(50)} ${RED}error${RESET}`) } else { const url = page.url.length > 50 ? page.url.slice(0, 47) + '...' : page.url - const pgc = gradeColor(page.overallGrade) - lines.push(` ${statusIcon(page.overallScore >= 70 ? 'pass' : page.overallScore >= 40 ? 'partial' : 'fail')} ${url.padEnd(50)} ${bar(page.overallScore, 15)} ${pgc}${page.overallGrade.padEnd(3)}${RESET}`) + const pgc = scoreColor(page.overallScore) + lines.push(` ${scoreIcon(page.overallScore)} ${url.padEnd(50)} ${bar(page.overallScore, 15)} ${pgc}${String(page.overallScore).padStart(3)}${RESET}`) } } @@ -145,8 +144,8 @@ export function formatSitemapText(report: SitemapAuditReport, topIssuesOnly = fa for (const issue of report.crossCuttingIssues) { const pct = Math.round((issue.affectedPages / issue.totalPages) * 100) - const igc = gradeColor(issue.avgGrade) - lines.push(` ${igc}${issue.avgGrade.padEnd(3)}${RESET} ${issue.factorName.padEnd(32)} ${DIM}avg ${issue.avgScore}/100, affects ${pct}% of pages${RESET}`) + const igc = scoreColor(issue.avgScore) + lines.push(` ${igc}${issue.factorName.padEnd(32)}${RESET} ${DIM}avg ${issue.avgScore}/100, affects ${pct}% of pages${RESET}`) for (const detail of issue.topIssues) { lines.push(` ${DIM}• ${detail.recommendation}${RESET} ${DIM}(${detail.affectedUrls.length}/${issue.totalPages} pages)${RESET}`) @@ -165,8 +164,8 @@ export function formatSitemapText(report: SitemapAuditReport, topIssuesOnly = fa for (let i = 0; i < report.prioritizedFixes.length; i++) { const fix = report.prioritizedFixes[i] const tag = fix.severity ? `[${fix.severity === 'critical' ? RED : YELLOW}${fix.severity}${RESET}] ` : '' - const grade = fix.avgGrade ? `${DIM} avg ${fix.avgGrade}${RESET}` : '' - lines.push(` ${CYAN}${i + 1}.${RESET} ${tag}${BOLD}${fix.title}${RESET}${grade} ${DIM}(${fix.prevalencePct}% of pages)${RESET}`) + const avg = fix.avgScore !== undefined ? `${DIM} avg ${fix.avgScore}/100${RESET}` : '' + lines.push(` ${CYAN}${i + 1}.${RESET} ${tag}${BOLD}${fix.title}${RESET}${avg} ${DIM}(${fix.prevalencePct}% of pages)${RESET}`) lines.push(` ${DIM}→ ${fix.recommendation}${RESET}`) // Spell out every affected page — agents and humans both need the full set. for (const url of fix.affectedPages) { diff --git a/src/index.ts b/src/index.ts index 32652be..e35d5d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -89,16 +89,16 @@ const ALL_FACTOR_IDS = new Set([ ...OPTIONAL_FACTOR_DEFINITIONS.map((d) => d.id), ]) -function buildSummary(factors: ScoredFactor[], overallGrade: string): string { +function buildSummary(factors: ScoredFactor[], overallScore: number): string { if (!factors.length) { - return `Overall grade ${overallGrade}. No factors evaluated.` + return `Overall score ${overallScore}/100. No factors evaluated.` } const ranked = [...factors].sort((a, b) => b.score - a.score) const strengths = ranked.slice(0, 2).map((factor) => factor.name) const weaknesses = ranked.slice(-2).map((factor) => factor.name) - return `Overall grade ${overallGrade}. Strongest signals: ${strengths.join(', ')}. Biggest opportunities: ${weaknesses.join(', ')}.` + return `Overall score ${overallScore}/100. Strongest signals: ${strengths.join(', ')}. Biggest opportunities: ${weaknesses.join(', ')}.` } function assertValidFactorIds(selectedFactors: string[]): void { @@ -186,7 +186,7 @@ export async function auditHtmlPage(page: AuditHtmlPageInput, options: RunAeoAud }), ) - const { overallScore, overallGrade, factors } = scoreFactors(rawFactorResults) + const { overallScore, factors } = scoreFactors(rawFactorResults) return { schemaVersion: SCHEMA_VERSION, @@ -194,8 +194,7 @@ export async function auditHtmlPage(page: AuditHtmlPageInput, options: RunAeoAud finalUrl: page.finalUrl, auditedAt: new Date().toISOString(), overallScore, - overallGrade, - summary: buildSummary(factors, overallGrade), + summary: buildSummary(factors, overallScore), factors, criticalDefects: detectCriticalDefects(context), metadata: { diff --git a/src/schema.ts b/src/schema.ts index 1d31ce3..8e9fd02 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -6,4 +6,4 @@ * Lives in its own module (not `index.ts`) so report builders can read it without * importing the audit entry points — which test suites routinely mock. */ -export const SCHEMA_VERSION = '1.1' +export const SCHEMA_VERSION = '2.0' diff --git a/src/scoring.ts b/src/scoring.ts index b21c2ae..e2eead2 100644 --- a/src/scoring.ts +++ b/src/scoring.ts @@ -26,38 +26,11 @@ export const OPTIONAL_FACTOR_DEFINITIONS: FactorDefinition[] = [ { id: 'lighthouse', name: 'Lighthouse (Performance/A11y/Best Practices)', weight: 8 }, ] -export function scoreToGrade(score: number): string { - if (score >= 97) return 'A+' - if (score >= 93) return 'A' - if (score >= 90) return 'A-' - if (score >= 87) return 'B+' - if (score >= 83) return 'B' - if (score >= 80) return 'B-' - if (score >= 77) return 'C+' - if (score >= 73) return 'C' - if (score >= 70) return 'C-' - if (score >= 67) return 'D+' - if (score >= 63) return 'D' - if (score >= 60) return 'D-' - return 'F' -} - -export function scoreToStatus(score: number): 'pass' | 'partial' | 'fail' { - if (score >= 70) return 'pass' - if (score >= 40) return 'partial' - return 'fail' -} - export function scoreFactors(rawFactorResults: RawFactorResult[]): ScoredFactorSummary { - const factors = rawFactorResults.map((factor) => { - const score = clampScore(factor.score) - return { - ...factor, - score, - grade: scoreToGrade(score), - status: scoreToStatus(score), - } - }) + const factors = rawFactorResults.map((factor) => ({ + ...factor, + score: clampScore(factor.score), + })) const totalWeight = factors.reduce((sum, factor) => sum + factor.weight, 0) @@ -69,7 +42,6 @@ export function scoreFactors(rawFactorResults: RawFactorResult[]): ScoredFactorS return { overallScore, - overallGrade: scoreToGrade(overallScore), factors, } } diff --git a/src/sitemap.ts b/src/sitemap.ts index 98ba5a2..5940738 100644 --- a/src/sitemap.ts +++ b/src/sitemap.ts @@ -3,7 +3,6 @@ import { buildCriticalDefects, isHomepageUrl } from './critical-defects.js' import { normalizeTargetUrl } from './fetch-page.js' import { runAeoAudit } from './index.js' import { SCHEMA_VERSION } from './schema.js' -import { scoreToGrade } from './scoring.js' import type { AuditReport, CriticalDefectGroup, @@ -312,7 +311,6 @@ function buildCrossCuttingIssues(successPages: AuditReport[]): CrossCuttingIssue factorId, factorName: entry.name, avgScore, - avgGrade: scoreToGrade(avgScore), affectedPages, totalPages: successPages.length, topRecommendations: sortedIssues.slice(0, 3).map((i) => i.recommendation), @@ -379,8 +377,8 @@ function buildPrioritizedFixes( affectedPages, affectsHomepage, prevalencePct: pct(count), - avgGrade: issue.avgGrade, - summary: `${issue.factorName} (avg ${issue.avgGrade}) — ${count} page${count === 1 ? '' : 's'}: ${recommendation}`, + avgScore: issue.avgScore, + summary: `${issue.factorName} (avg ${issue.avgScore}/100) — ${count} page${count === 1 ? '' : 's'}: ${recommendation}`, } }) @@ -481,7 +479,6 @@ export async function runSitemapAudit(rawUrl: string, options: SitemapAuditOptio pageResult: { url: report.finalUrl, overallScore: report.overallScore, - overallGrade: report.overallGrade, status: 'success', factors: report.factors, metadata: report.metadata, @@ -495,7 +492,6 @@ export async function runSitemapAudit(rawUrl: string, options: SitemapAuditOptio pageResult: { url: entry.loc, overallScore: 0, - overallGrade: 'F', status: 'error', error: message, priority: entry.priority, @@ -539,7 +535,6 @@ export async function runSitemapAudit(rawUrl: string, options: SitemapAuditOptio pagesTruncated: truncated, effectiveLimit, aggregateScore, - aggregateGrade: scoreToGrade(aggregateScore), pages: pageResults, criticalDefects, crossCuttingIssues, diff --git a/src/static-audit.ts b/src/static-audit.ts index 13ad777..12b7c1d 100644 --- a/src/static-audit.ts +++ b/src/static-audit.ts @@ -6,7 +6,6 @@ import { auditHtmlPage } from './index.js' import { buildCriticalDefects } from './critical-defects.js' import { SCHEMA_VERSION } from './schema.js' import { buildCrossCuttingIssues, buildPrioritizedFixes, mapWithConcurrency } from './sitemap.js' -import { scoreToGrade } from './scoring.js' import type { AuditReport, AuxiliaryResources, @@ -247,7 +246,6 @@ export async function runStaticAudit(targetPath: string, options: StaticAuditOpt pageResult: { url: report.finalUrl, overallScore: report.overallScore, - overallGrade: report.overallGrade, status: 'success', factors: report.factors, metadata: report.metadata, @@ -257,7 +255,7 @@ export async function runStaticAudit(targetPath: string, options: StaticAuditOpt } catch (error) { const message = error instanceof Error ? error.message : String(error) return { - pageResult: { url, overallScore: 0, overallGrade: 'F', status: 'error', error: message }, + pageResult: { url, overallScore: 0, status: 'error', error: message }, report: null, } } @@ -291,7 +289,6 @@ export async function runStaticAudit(targetPath: string, options: StaticAuditOpt pagesTruncated: truncated, effectiveLimit, aggregateScore, - aggregateGrade: scoreToGrade(aggregateScore), pages: pageResults, criticalDefects, crossCuttingIssues, diff --git a/src/types.ts b/src/types.ts index 01be217..44e84dd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -112,10 +112,12 @@ export interface RawFactorResult extends AnalysisResult { weight: number } -export interface ScoredFactor extends RawFactorResult { - grade: string - status: 'pass' | 'partial' | 'fail' -} +/** + * A factor with its score finalized (clamped to 0–100). Structurally identical to + * `RawFactorResult`: the audit reports a raw 0–100 score per factor, with no + * derived letter grade or pass/partial/fail band. + */ +export type ScoredFactor = RawFactorResult export interface AuditMetadata { fetchTimeMs: number @@ -143,7 +145,7 @@ export type CriticalDefectSeverity = 'critical' | 'warning' * scores — which bundle many sub-checks and can average a single bad signal away — * these are detected directly from the DOM and are simply present or not. They are * surfaced separately so a high-impact defect on one important page (e.g. a - * homepage with four `<h1>`s) is never hidden by low prevalence or a passing grade. + * homepage with four `<h1>`s) is never hidden by low prevalence or a passing score. */ export interface CriticalDefect { id: CriticalDefectId @@ -164,7 +166,6 @@ export interface AuditReport { finalUrl: string auditedAt: string overallScore: number - overallGrade: string summary: string factors: ScoredFactor[] /** Binary structural defects on this page, detected independently of scoring. */ @@ -180,7 +181,6 @@ export interface FactorDefinition { export interface ScoredFactorSummary { overallScore: number - overallGrade: string factors: ScoredFactor[] } @@ -205,7 +205,6 @@ export type Analyzer = (context: AuditContext) => AnalysisResult | Promise<Analy export interface SitemapPageResult { url: string overallScore: number - overallGrade: string status: 'success' | 'error' error?: string factors?: ScoredFactor[] @@ -263,8 +262,8 @@ export interface PrioritizedFix { affectsHomepage: boolean /** Share of audited pages this fix applies to (0–100). */ prevalencePct: number - /** Average grade across audited pages for the factor (cross-cutting only). */ - avgGrade?: string + /** Average factor score (0–100) across audited pages (cross-cutting only). */ + avgScore?: number /** Ready-to-display one-line headline (does not inline the page list). */ summary: string } @@ -285,7 +284,6 @@ export interface AgentSummary { /** The audited page URL (single) or the sitemap/root URL (multi). */ url: string score: number - grade: string /** True when the score meets the >= 70 gate (the default exit-0 threshold). */ pass: boolean /** Number of critical-severity binary defects (e.g. a missing or duplicated H1). */ @@ -303,7 +301,6 @@ export interface CrossCuttingIssue { factorId: string factorName: string avgScore: number - avgGrade: string affectedPages: number totalPages: number topRecommendations: string[] @@ -322,7 +319,6 @@ export interface SitemapAuditReport { pagesTruncated: number effectiveLimit: number aggregateScore: number - aggregateGrade: string pages: SitemapPageResult[] /** * High-impact binary defects surfaced regardless of prevalence (issue #42). diff --git a/test/agent-summary.test.ts b/test/agent-summary.test.ts index 903d7f7..21d0dd7 100644 --- a/test/agent-summary.test.ts +++ b/test/agent-summary.test.ts @@ -17,8 +17,6 @@ function factor(overrides: Partial<ScoredFactor> & { id: string; name: string }) name: overrides.name, weight: 8, score: overrides.score ?? 40, - grade: overrides.grade ?? 'F', - status: overrides.status ?? 'fail', findings: overrides.findings ?? [], recommendations: overrides.recommendations ?? [], } @@ -31,7 +29,6 @@ function auditReport(overrides: Partial<AuditReport> = {}): AuditReport { finalUrl: 'https://example.com/', auditedAt: '2026-04-18T00:00:00.000Z', overallScore: 60, - overallGrade: 'D-', summary: '', factors: [], criticalDefects: [], @@ -74,8 +71,7 @@ describe('agentSummaryFromAudit', () => { it('reports pass=true and no issues for a clean, passing page', () => { const report = auditReport({ overallScore: 92, - overallGrade: 'A', - factors: [factor({ id: 'structured-data', name: 'Structured Data', score: 95, status: 'pass', grade: 'A', recommendations: [] })], + factors: [factor({ id: 'structured-data', name: 'Structured Data', score: 95, recommendations: [] })], }) const summary = agentSummaryFromAudit(report) @@ -101,7 +97,6 @@ describe('agentSummaryFromSitemap', () => { pagesTruncated: 0, effectiveLimit: 200, aggregateScore: 64, - aggregateGrade: 'D', pages: [], criticalDefects, crossCuttingIssues: [], @@ -143,7 +138,7 @@ describe('formatAgent / formatSitemapAgent', () => { it('emits valid JSON with the decision keys and none of the heavy detail', () => { const parsed = JSON.parse(formatAgent(auditReport({ factors: [factor({ id: 'x', name: 'X' })] }))) expect(Object.keys(parsed).sort()).toEqual( - ['criticalDefectCount', 'grade', 'issues', 'mode', 'pass', 'schemaVersion', 'score', 'tool', 'url'].sort(), + ['criticalDefectCount', 'issues', 'mode', 'pass', 'schemaVersion', 'score', 'tool', 'url'].sort(), ) // The point of agent mode: no 27 pages of factor/page detail. expect(parsed.factors).toBeUndefined() @@ -163,7 +158,6 @@ describe('formatAgent / formatSitemapAgent', () => { pagesTruncated: 0, effectiveLimit: 200, aggregateScore: 80, - aggregateGrade: 'B-', pages: [], criticalDefects: [], crossCuttingIssues: [], diff --git a/test/analyzers/legacy.test.ts b/test/analyzers/legacy.test.ts index 7a15a11..786db7c 100644 --- a/test/analyzers/legacy.test.ts +++ b/test/analyzers/legacy.test.ts @@ -134,7 +134,7 @@ test('structured data analyzer detects nested HowTo schema', () => { expect(howToFinding?.type).toBe('found') }) -test('scoring engine computes grades and statuses', () => { +test('scoring engine clamps factor scores and computes a weighted overall', () => { const scored = scoreFactors([ { id: 'structured-data', @@ -154,7 +154,7 @@ test('scoring engine computes grades and statuses', () => { }, ]) - expect(scored.factors[0].status).toBe('pass') - expect(scored.factors[1].status).toBe('fail') - expect(typeof scored.overallGrade).toBe('string') + expect(scored.factors[0].score).toBe(80) + expect(scored.factors[1].score).toBe(20) + expect(typeof scored.overallScore).toBe('number') }) diff --git a/test/cli-require-meta.test.ts b/test/cli-require-meta.test.ts index 2152da2..f399e5a 100644 --- a/test/cli-require-meta.test.ts +++ b/test/cli-require-meta.test.ts @@ -11,8 +11,6 @@ function technicalSeoFactor( name: 'Technical SEO', weight: 5, score: 50, - grade: 'D', - status: 'partial', findings, recommendations: [], } @@ -31,8 +29,6 @@ describe('hasMissingMetaDescription', () => { name: 'Structured Data', weight: 10, score: 80, - grade: 'B', - status: 'pass', findings: [], recommendations: [], }, diff --git a/test/critical-defects.test.ts b/test/critical-defects.test.ts index f17a267..b1e1dc2 100644 --- a/test/critical-defects.test.ts +++ b/test/critical-defects.test.ts @@ -49,7 +49,6 @@ function report(url: string, criticalDefects: CriticalDefect[]): AuditReport { finalUrl: url, auditedAt: '2026-04-18T00:00:00.000Z', overallScore: 75, - overallGrade: 'C', summary: '', factors: [], criticalDefects, @@ -222,7 +221,6 @@ describe('buildPrioritizedFixes with critical defects', () => { factorId: factorName.toLowerCase().replace(/\s+/g, '-'), factorName, avgScore: 40, - avgGrade: 'F', affectedPages, totalPages: 25, topRecommendations: [rec], @@ -245,7 +243,7 @@ describe('buildPrioritizedFixes with critical defects', () => { kind: 'cross-cutting', id: 'technical-seo', title: 'Technical SEO', - avgGrade: 'F', + avgScore: 40, }) expect(typeof fixes[0].summary).toBe('string') expect(typeof fixes[0].prevalencePct).toBe('number') @@ -303,7 +301,6 @@ describe('formatters list every affected page (no truncation)', () => { pagesTruncated: 0, effectiveLimit: 200, aggregateScore: 50, - aggregateGrade: 'F', pages: [], criticalDefects, crossCuttingIssues: [], @@ -345,8 +342,7 @@ describe('formatters list every affected page (no truncation)', () => { affectedPages: manyPages.map((p) => p.url), affectsHomepage: false, prevalencePct: 100, - avgGrade: 'F', - summary: 'Technical SEO (avg F) — 14 pages: Add a meta description.', + summary: 'Technical SEO (avg 40/100) — 14 pages: Add a meta description.', } it('spells out every page of each prioritized fix in text output', () => { diff --git a/test/e2e/cli.test.ts b/test/e2e/cli.test.ts index d845949..7482b43 100644 --- a/test/e2e/cli.test.ts +++ b/test/e2e/cli.test.ts @@ -179,10 +179,9 @@ test('compiled CLI returns the expected JSON report for the fixture site', async assert.equal(report.finalUrl, FIXTURE_ORIGIN) assert.equal(report.auditedAt, FIXED_NOW) assert.equal(report.overallScore, 76) - assert.equal(report.overallGrade, 'C') assert.equal( report.summary, - 'Overall grade C. Strongest signals: AI-Readable Content, Schema Validity. Biggest opportunities: Schema Completeness, E-E-A-T Signals.', + 'Overall score 76/100. Strongest signals: AI-Readable Content, Schema Validity. Biggest opportunities: Schema Completeness, E-E-A-T Signals.', ) assert.deepEqual(report.metadata, { fetchTimeMs: 0, @@ -200,27 +199,25 @@ test('compiled CLI returns the expected JSON report for the fixture site', async report.factors.map((factor) => ({ id: factor.id, score: factor.score, - grade: factor.grade, - status: factor.status, })), [ - { id: 'structured-data', score: 78, grade: 'C+', status: 'pass' }, - { id: 'content-depth', score: 59, grade: 'F', status: 'partial' }, - { id: 'ai-readable-content', score: 100, grade: 'A+', status: 'pass' }, - { id: 'eeat-signals', score: 25, grade: 'F', status: 'fail' }, - { id: 'faq-content', score: 82, grade: 'B-', status: 'pass' }, - { id: 'citations', score: 66, grade: 'D', status: 'partial' }, - { id: 'schema-completeness', score: 45, grade: 'F', status: 'partial' }, - { id: 'schema-validity', score: 100, grade: 'A+', status: 'pass' }, - { id: 'entity-consistency', score: 94, grade: 'A', status: 'pass' }, - { id: 'content-freshness', score: 82, grade: 'B-', status: 'pass' }, - { id: 'content-extractability', score: 48, grade: 'F', status: 'partial' }, - { id: 'definition-blocks', score: 100, grade: 'A+', status: 'pass' }, - { id: 'ai-crawler-access', score: 100, grade: 'A+', status: 'pass' }, - { id: 'named-entities', score: 84, grade: 'B', status: 'pass' }, - { id: 'technical-seo', score: 80, grade: 'B-', status: 'pass' }, - { id: 'snippet-eligibility', score: 100, grade: 'A+', status: 'pass' }, - { id: 'geographic-signals', score: 94, grade: 'A', status: 'pass' }, + { id: 'structured-data', score: 78 }, + { id: 'content-depth', score: 59 }, + { id: 'ai-readable-content', score: 100 }, + { id: 'eeat-signals', score: 25 }, + { id: 'faq-content', score: 82 }, + { id: 'citations', score: 66 }, + { id: 'schema-completeness', score: 45 }, + { id: 'schema-validity', score: 100 }, + { id: 'entity-consistency', score: 94 }, + { id: 'content-freshness', score: 82 }, + { id: 'content-extractability', score: 48 }, + { id: 'definition-blocks', score: 100 }, + { id: 'ai-crawler-access', score: 100 }, + { id: 'named-entities', score: 84 }, + { id: 'technical-seo', score: 80 }, + { id: 'snippet-eligibility', score: 100 }, + { id: 'geographic-signals', score: 94 }, ], ) }) diff --git a/test/sitemap-cross-cutting.test.ts b/test/sitemap-cross-cutting.test.ts index 102cd1b..56d8dca 100644 --- a/test/sitemap-cross-cutting.test.ts +++ b/test/sitemap-cross-cutting.test.ts @@ -8,8 +8,6 @@ function factor(overrides: Partial<ScoredFactor> & { id: string; name: string }) id: overrides.id, name: overrides.name, weight: 5, - grade: overrides.grade ?? 'C', - status: overrides.status ?? 'partial', score: overrides.score ?? 60, findings: overrides.findings ?? [], recommendations: overrides.recommendations ?? [], @@ -23,7 +21,6 @@ function report(url: string, factors: ScoredFactor[]): AuditReport { finalUrl: url, auditedAt: '2026-04-18T00:00:00.000Z', overallScore: 60, - overallGrade: 'C', summary: '', factors, criticalDefects: [], diff --git a/test/sitemap-options.test.ts b/test/sitemap-options.test.ts index 289b8b7..bfe3085 100644 --- a/test/sitemap-options.test.ts +++ b/test/sitemap-options.test.ts @@ -26,7 +26,6 @@ function fakeReport(url: string) { finalUrl: url, auditedAt: '2026-01-01T00:00:00.000Z', overallScore: 90, - overallGrade: 'A-', summary: '', factors: [], metadata: { diff --git a/test/sitemap-rewrite.test.ts b/test/sitemap-rewrite.test.ts index dbd4376..90a1162 100644 --- a/test/sitemap-rewrite.test.ts +++ b/test/sitemap-rewrite.test.ts @@ -25,7 +25,6 @@ function fakeReport(url: string) { finalUrl: url, auditedAt: '2026-01-01T00:00:00.000Z', overallScore: 90, - overallGrade: 'A-', summary: '', factors: [], metadata: { diff --git a/test/static-audit.test.ts b/test/static-audit.test.ts index cd1ba23..3c6285a 100644 --- a/test/static-audit.test.ts +++ b/test/static-audit.test.ts @@ -134,6 +134,6 @@ describe('runStaticAudit critical defects (issue #42)', () => { expect(topFix.affectedPages).toContain('https://example.com/') // The report carries a schema version so agent parsers can detect shape drift. - expect(result.report.schemaVersion).toBe('1.1') + expect(result.report.schemaVersion).toBe('2.0') }) })