Changelog
Release history
Every shipping pseolint package and its version, read straight from the workspace at build time — so what you see here is exactly what ships, never a hand-maintained copy that drifts.
@pseolint/core
npm · engine
pseolint
npm · CLI
@pseolint/mcp
npm · MCP server
@pseolint/action
GitHub Action
@pseolint/extension
Chrome Web Store
@pseolint/web
hosted app
@pseolint/core
- v0.7.5
- content-effort is now a recall driver, not just a ±1 nudge. When the AI content-effort score is very low (≤3 — below the lowest reputable site in the calibration corpus), the verdict escalates two tiers, so a verbose AI content farm (which defeats the thin-content/unique-value/near-duplicate rules by writing rich, entity-distinct prose) gets flagged on the effort signal alone. Effort 4–5 keeps the conservative ±1 escalation and ≥25 keeps the −1 soften, so reputable low-content directories are unaffected. Raw
riskis untouched (verdict-only moderation), so CI gates stay deterministic. On the calibration corpus this lifts addressable recall 67%→80% with no new false positives. Only active when--content-effortis enabled. - Internal: split the cheerio-dependent soft-404 probe (
evaluateProbe) into its own module (rules/tech/soft-404-probe.ts) sosoft404Ruleand the./rules/tech/soft-404subpath stay parser-free — letting the browser extension import the rule without pulling cheerio into the service-worker bundle. Behaviour-neutral (audit output unchanged);evaluateProbeis still called internally by the auditor and remains covered by tests. The only externally-visible change is thatevaluateProbeis no longer re-exported from the package root (it had no external consumers).
- content-effort is now a recall driver, not just a ±1 nudge. When the AI content-effort score is very low (≤3 — below the lowest reputable site in the calibration corpus), the verdict escalates two tiers, so a verbose AI content farm (which defeats the thin-content/unique-value/near-duplicate rules by writing rich, entity-distinct prose) gets flagged on the effort signal alone. Effort 4–5 keeps the conservative ±1 escalation and ≥25 keeps the −1 soften, so reputable low-content directories are unaffected. Raw
- v0.7.4
- v0.7.4 — pluggable cache backend + internal refinements.
- Pluggable HTTP cache backend (`@pseolint/core`). The audit cache's storage now sits behind a
CacheBackend { get, set }interface. The defaultFilesystemCacheBackendpreserves the existing dir-based behaviour byte-for-byte (the CLI is unchanged), so a host can supply its own store — e.g. an R2-backed cache on ephemeral-filesystem serverless — viaAuditOptions.cache.backend. New exports:CacheBackend,FilesystemCacheBackend,AnyCacheEntry,RedirectPointerEntry,CACHE_ENTRY_SCHEMA_VERSION. All fetch revalidation (ETag/304), redirect-pointer, and negative-cache logic stays backend-agnostic, and every backend call is fail-safe — a backend error degrades to a cache miss (read) or a logged no-op (write), never aborting an audit. - `./rules/scope` subpath export (`@pseolint/core`). A dependency-light entry exposing
SCORED_RULE_COUNT,RULE_SCOPE, andisRuleAllowedInDiffwithout pulling the full engine barrel — for consumers (browser/edge bundles) that only need rule-scope metadata. - MCP JSON char-cap is now read per call (`@pseolint/mcp`).
PSEOLINT_MCP_JSON_CHAR_CAPis resolved at request time instead of frozen at module load, so the oversized-payload envelope threshold can be tuned/tested without a restart. No protocol change.
- v0.7.3
- v0.7.3 — render-aware checks, AI content-effort, and bring-your-own inputs. Verdict moderators never change the raw
risknumber, so CI gates keyed off--ci-thresholdstay stable. - Bring-your-own authority. New
--authority-score <0-100>CLI flag andauthorityScoreconfig key, alongside the existing MCPauthorityScoreparam and a per-domain setting in the Pro web dashboard.>= 80shifts the verdict one tier lenient,<= 30one tier stricter. The engine stays authority-blind by design. - AI content-effort signal. New
--content-effort(+--content-effort-model) CLI flag,AuditOptions.contentEffort, and an optional MCPcontentEffortparam. An LLM judges a 0-100 content originality/effort score from sampled page text (≤10 pages, content-hash cached) that moderates the verdict ±1 tier. Opt-in, needsANTHROPIC_API_KEY, no-ops safely without one (~$0.003/page on claude-sonnet-4-6). Resolved score is written tosummary.contentEffort.score. Runs automatically for Pro audits in the web app. - Render-aware crawl checks.
--render(Playwright, Node-only) now feeds two render-diff rules:tech/csr-bailoutflags pages whose substantive content/interactivity exists only after client-side JS (invisible to crawlers + the first indexing pass), andtech/soft-404probes one synthetic nonexistent URL per template cluster (an HTTP 200 means the directory will index unbounded junk). Both no-op without--render/ outside programmatic directories. - MCP rule knowledge as resources. The MCP server now exposes the pSEO rule catalog as resources (
pseolint://rulesindex +pseolint://rules/<ruleId>per-rule Markdown) so assistants can explain findings without guessing. The open knowledge bundle is also served at/okfand linked fromllms.txt.
- v0.7.3 — render-aware checks, AI content-effort, and bring-your-own inputs. Verdict moderators never change the raw
- v0.7.2
- fix(core): schema/consistency no longer false-positives on pages with multiple JSON-LD blocks. The v0.7.1 per-cluster rewrite compared the UNION of @types across a cluster, so a template where every page legitimately emits several blocks (e.g. TechArticle + FAQPage + Organization) read as "mixed types" and fired on every cluster (6 FPs on pseolint.dev's own audit). Now it compares each page's @type SET signature and fires only when pages in the same template cluster genuinely disagree.
- fix(core): v0.7.2 rule-design batch — graded thresholds + presence-quality. Follow-up to the v0.7.1 FP-elimination batch, addressing the two deferred root causes (C: binary/absolute thresholds, D: presence-not-quality). Verified against the 24-fixture calibration corpus: zero new false positives vs the prior metrics, and the crawl-size verdict flips are gone. C — binary-threshold redesigns:
- spam/boilerplate-ratio: continuous document-frequency weighting replaces the floor(N\*0.8)+1 skeleton cliff; 2-band severity. Verdict no longer flips when one more sibling page is crawled.
- spam/template-diversity: log-bucketed coarsening of the structureSignature so single-template sites with minor chrome variation are no longer read as diverse (the exact-count signature shared with near-duplicate/doorway-pattern is untouched); confidence band.
- content/value-add: continuous categoriesPresent/4 E-E-A-T sub-score replacing the 3-step hard-threshold value; 2-band severity (drops "critical").
- content/wikipedia-paraphrase: min-length guard + threshold 0.40→0.55 above the legal/medical topic-overlap baseline; advisory language, stays low-confidence. D — presence-quality (validate the value, not just its presence):
- schema/required-fields: empty arrays / whitespace / nameless author objects count as missing.
- schema/json-ld-valid: @type accepts string OR all-string array (["Article","NewsArticle"] no longer false-positives).
- tech/og-completeness: whitespace values count as missing; severity graded (title/description warning, image-only info).
- content/eeat-signals: transparency signal reads contentText not raw html; about-link must be same-host.
- v0.7.1
- content/unique-value now scores originality as a rarity density (normalized-IDF average over a page's distinct tokens) instead of an absolute count of exactly-page-exclusive words. This fixes margin instability (the flagged set no longer "shuffles" when content is added) and false positives on large, tightly- themed sites — validated against the reputable-pSEO fixtures: doorway/entity-swap spam fires at density ~0.09 while reputable corpora (incl. paperforge.dev) clear at ≥0.28, with floors
passBelow 0.20/errorBelow 0.12in the gap. Config knob renamed:rules.uniqueValueMinWords: number→rules.uniqueValueDensity: { passBelow, errorBelow }. The rule signature is nowuniqueValueRule(pages, { passBelow, errorBelow }). Borderline pages fireinforather thanerrorso a near-miss no longer reads as a ship-blocker. - v0.7.1 — rule false-positive elimination batch (post unique-value design review). Stops the engine flagging healthy sites without weakening real detection. Each fix is TDD'd and validated against the reputable-pSEO fixtures.
- links/orphan-pages, links/cluster-connectivity — suppressed on sampled crawls (the linking/target page is often un-fetched; reliable only on a full crawl).
- tech/canonical-consistency — collapse "canonicalizes outside crawl scope" to one site-level note when all pages point at the same alternate host (staging/ preview/localhost), instead of one finding per page; dedup HTTP-vs-HTML.
- tech/sitemap-completeness — normalize sitemap URLs before the set-diff (kills trailing-slash/query false "missing"); demote the missing aggregate to warning.
- schema/consistency — flag @type variance per template cluster (structureSignature), not site-wide (was a guaranteed FP on any multi-template site).
- aeo/crawler-access — honor robots
Allowdirectives per RFC 9309 (allow-all no longer reported as fully blocked). - Severity/confidence bands — error/critical demoted to warning on weak or forecast signals: thin-content medium band, summary-bait, translation-no-op, entity-swap (low mask coverage), soft-404 (OR-weighted confidence model). Note: bundled as a patch (0.x) despite a behavior/scoring shift and the
rules.uniqueValueMinWords→rules.uniqueValueDensityconfig rename.
- content/unique-value now scores originality as a rarity density (normalized-IDF average over a page's distinct tokens) instead of an absolute count of exactly-page-exclusive words. This fixes margin instability (the flagged set no longer "shuffles" when content is added) and false positives on large, tightly- themed sites — validated against the reputable-pSEO fixtures: doorway/entity-swap spam fires at density ~0.09 while reputable corpora (incl. paperforge.dev) clear at ≥0.28, with floors
- v0.7.0
- Add
checkOriginHealth()— a pre-flight origin probe that runs before an audit crawls.BackpressureMonitoronly protects an origin _during_ a crawl, after dozens of requests have already landed on a struggling server (the paperforge/Neon incident, where each fetch fanned out into uncached DB queries that exhausted the egress quota).checkOriginHealth()fires a handful of concurrent probes at the entry URL first — concurrent, so it observes the origin the way the real crawl hits it (parallel fan-out), not a rosier one-request-at-a-time picture — and returns anok/unreachable/degradedverdict. - SSRF-safe — every probe and redirect hop is re-validated against private/loopback ranges (overridable
validateHopfor tests). - Conservative: a single transient timeout never trips it.
unreachablerequires _every_ probe to fail;degradedrequires a 5xx majority or sustained latency past the same 8s ceilingBackpressureMonitoruses. 4xx is not treated as degradation. - Concurrent probes keep the wall-clock cost to ~one request, so wiring it onto a request path doesn't add N× latency.
- Fail-open: never throws, so a bug in the check can't block a legitimate audit. In the hosted app, the public
POST /api/auditsroute blocks onlyunreachableorigins (nothing to audit), and therun-auditworker — which every audit path runs through, including the monitoring cron — pre-flights the origin and drops adegraded/unreachableone to gentle (low-concurrency) mode instead of finishing off a struggling server. - v0.7.0 — Calibration & authority foundations
- Two-sided calibration harness + score-vs-outcome instrument. New
calibrationMetrics()reports how well the risk score tracks real winning/penalized outcomes against a labeled corpus — threshold-free AUC, class-separation gap, per-band empirical penalty rate, and the over-flag / recall-leak confusion-zone sites. Adetectabilitycorpus field separates the engine's addressable ceiling from structurally-undetectable (off-page) cases. - Corpus-derived entity auto-masking (
deriveEntityPatterns): clusters pages by URL template and masks tokens that vary across siblings, lifting policy-violating recall (44% → 56% on the calibration corpus) and fixing the reputable-vs-spam risk inversion. - Domain-authority moderation scaffolding: a pluggable
AuthorityProvider(CompositeAuthorityProvidermax-combine,OpenPageRankProvider,CommonCrawlProvider) feeds the existing verdict-shift. Fail-safe no-op until an authority source is configured — no behaviour change by default.
- Add
- v0.6.6
- Sitemap coverage guardrail + axis-aware unique-value guidance (fixes #3, #4) #4 — declared-vs-discovered coverage guardrail. When a sitemap is found at discovery, two independent under-coverage signals now flag the run
truncatedwithtruncatedKind: "coverage"(distinct from the backpressure"backpressure"kind), reusing the existing partial-coverage surface (CLI/Action/MCP/web): - (A) unreachable child sitemaps.
collectUrlsFromSitemapnow reports child total/failed counts, so a sitemap index whose children 404 / aren't valid sitemaps / exceed the depth cap is flagged — the case a URL-count comparison can never see, and the original false-negative class. - (B) fetch shortfall. Far fewer pages were FETCHED than the sitemap declares. Compared against pages actually fetched (pre-filter, pre-sample) and bounded by every deliberate limit (explicit
--sample-size, crawl cap, declared total) — so noindex / non-HTML pages, intentional sampling, and a small crawl cap do not false-fire (the two false positives the first cut shipped with). AddsAuditSummary.truncatedKind: "backpressure" | "coverage"(+ JSON schema) so consumers and CI can branch on the cause rather than overloading one boolean. #3 — `content/unique-value` guidance. The fix string is now axis-aware (warns that content shared across same-axis sibling pages — boilerplate, per-axis data — does NOT count), the message surfaces the shared-vs-unique word split, and tokenization strips surrounding punctuation so"word"/"word."count as one token (removing false precision in the surfaced counts). Also routed a remainingsplice(0, n, ...big)spread through the iterativepushAllhelper (same V8 argument-cap class as the earlier crash fix). - A truncated run can no longer present as a confident clean verdict Found by re-running
pseolint https://paperforge.devagainst the live site: the backpressure watchdog (correctly) aborts the cold-start origin after ~11 fetches, but the 1-page salvage was then run through the normal classify → score → verdict pipeline and emerged assmall-marketing+ suppressed pSEO rules + `READY ✓` — reproducing the original case-study false-negative via the watchdog rather than via discovery (which works). Two fixes: - Classification: when a run is truncated BEFORE classification (a backpressure abort salvaged only a fragment), the site type is forced to
unclear(confidence 0, no rule suppression). Classifying a salvaged fragment as a confidentsmall-marketingsite — and suppressing the pSEO rules off it — is what produced the false green. - Verdict: any truncated run's verdict is floored to at least
caution— it can never readready, so the headline matches the partial-coverage banner. The watchdog itself is unchanged: it exists to protect exactly this origin (its crawl fans out into uncached DB queries), so it should keep aborting — the fix is that the salvaged report is now honest about being incomplete.
- Sitemap coverage guardrail + axis-aware unique-value guidance (fixes #3, #4) #4 — declared-vs-discovered coverage guardrail. When a sitemap is found at discovery, two independent under-coverage signals now flag the run
- v0.6.5
- Harden the public JSON output contract. Bump
SCHEMA_VERSIONfrom2026-04-v0.4to2026-06-v0.6so it reflects the current v0.6 shape (templates[],truncated/truncatedReason) — the constant had drifted and was never bumped when those landed. Publish a draft 2020-12 JSON Schema atschemas/audit-summary.schema.json(now shipped via the packagefiles/exports) and add a schema-contract test that validates the JSON formatter output against it so the schema and types can't silently drift. Document the contract in the README, including thatissuesis severity-bucketed (blockers/shouldFix/informational) rather than a flat array or category-keyed map, thatschemaVersionbumps on every breaking or additive-public output change, and thattruncated: truemeans counts/verdict are lower bounds for CI gates. - flush a partial
truncated:truereport on watchdog abort + warn on localhost concurrency override When the backpressure watchdog aborted a crawl mid-flight (the real user run at--concurrency 5against a single-process dev server), theOriginDegradedErrorpropagated out ofauditSource()and every downstream phase (dedup, rules, enrichment, scoring, report assembly) was skipped — the CLI caught the error, printed "aborted — origin looks degraded", and exited 1 with zero output. Protecting the origin is correct; throwing away everything collected is not.loadPagesFromSource()now fills a caller-owned salvage sink incrementally, so a mid-crawl abort no longer loses the pages already fetched.auditSource()catches the watchdog abort at the page-loading boundary (and at the next abort checkpoint, for fetch implementations that ignore the abort signal), recovers the partial page set, runs the rest of the pipeline over it, and returns a normalAuditSummarywithtruncated: trueandtruncatedReasonset to the origin-degraded message. A zero-page abort still returns a valid truncated summary instead of crashing. External aborts (ctrl-C / parent timeout) and--no-backpressureare unchanged. The CLI now prints a clear⚠ PARTIAL REPORTbanner to stderr for a truncated run, still writes/emits the report (JSON/console/etc.), and exits 1 so CI knows coverage was incomplete. It also warns (without changing the value) when a localhost/single-origin target is crawled with an explicit--concurrencygreater than the dev preset's 1, suggesting--concurrency 1. - Sitemap-first discovery for homepage/page sources (closer to how Google crawls) Previously, auditing a homepage URL only link-crawled — it never read the site's declared sitemap, so a programmatic site with thousands of sparsely-linked (or build-frozen, under-linked) URLs was under-discovered. Nested
<sitemapindex>files were already followed when a sitemap URL was passed directly, but nothing _discovered_ the sitemap from a plain page URL. The HTML-source path now does sitemap-first discovery before link-crawling: 1. readsSitemap:directives from robots.txt (there can be several), and 2. failing that, probes/sitemap.xmlthen/sitemap_index.xml. Discovered sitemap URLs are SSRF-validated, same-origin- and robots-Disallow- filtered, then fetched first (they're authoritative); the existing link-crawl then fills any remaining discovery budget and dedups against them. When no sitemap exists this is a no-op and the audit link-crawls exactly as before. Hardening:<sitemapindex>recursion is now depth-capped (5 levels) in addition to the existingvisitedcycle guard, and individual sitemap fetches are capped at 50 MB (the sitemaps.org limit) so a hostile/misconfigured sitemap can't eat the byte budget.fetchRobotsMetanow also returns the parsedSitemap:directives. Deferred (noted, not in this change):.xml.gzsitemaps served asapplication/gzipwithoutContent-Encoding: gzip(the common gzip case viaContent-Encodingalready works through fetch), URL normalization before sitemap sampling, and running the change-driven monitoring matrix over discovered sitemaps on homepage-sourced audits. - make cluster/link-graph traversal iterative to fix "Maximum call stack size exceeded" on large sites On a large, densely cross-linked site (the 351-page reproduction), report generation crashed with
RangeError: Maximum call stack size exceededand produced zero output. Clusterable rules (spam/near-duplicate,spam/entity-swap,spam/doorway-pattern) emit one finding per related pair, so a single fully-connected component yields C(N,2) pairwise findings. While collapsing a component into one cluster finding,enrichFindingscomputed the similarity range withMath.min(...similarities)/Math.max(...similarities). The spread operator passes every array element as a separate call argument, and V8 caps the argument count (~131072) — so a similarity array of hundreds of thousands of pairs overflowed the call stack. The similarity range is now computed with a single iterative pass, and the post-groupingpassthrough.push(...groupedPassthrough)spread was replaced with an element-wise loop for the same reason. Output is unchanged. Added a deterministic, synthetic regression test (tests/enrich-findings-large-cluster.test.ts) that builds a 600-node fully-connected near-duplicate component (179,700 pairs) and assertsenrichFindingsno longer throws. Follow-up: the samepush(...spread)overflow existed earlier in the pipeline, which the enrichment-only test missed. InauditSourcethe rule-aggregation steps spread the per-rule findings into the running list (findings.push(...tag(rule.findings))andallFindings.push(...)) — so a dense site overflowed at rule aggregation _before_ enrichment was reached. All such spreads in the audit path are now routed through an iterativepushAllhelper (rule aggregation, sitemap-URL and discovered-page accumulation), plus the latentMath.max(...counts.values())in the site classifier (100k+ distinct titles) and the spreads instratified-sampleand the AIfetch-sitemaptool. Added an integration test (tests/integration/large-corpus-no-overflow.test.ts) that drives the fullauditSourceover a 600-page directory corpus — it first asserts the guarded-against spread still overflows on the runtime, then that the audit completes. This is the test that caught the second (pre-enrichment) site. - fix unscoped title selector picking up SVG <title>; add SVG-title diagnostic
parseHtmlPageextracted the page title with an unscoped$("title")selector, which also matches inline SVG<title>elements (e.g. a logo's accessibility label). On pages with no<head><title>but an SVG logo, the SVG label was reported as the page title — the cause of "307 podcast episodes all titled Spotify". Title extraction is now scoped to$("head > title"). The parser recordstitleSource("head"or"none") and, when the head title is missing, captures the first inline SVG<title>text assvgTitleSample. Thecontent/title-uniquenessrule uses these to emit a diagnostic naming the SVG-title trap instead of a generic missing-title message.
- Harden the public JSON output contract. Bump
- v0.6.4
- Export
SCORED_CATEGORY_KEYS— the canonical list of verdict-contributing category keys (everyCategoryKeyexcept the weight-0auditdiagnostics bucket). Gives downstream consumers (MCP, CLI, web) a single source of truth for "the scored categories" instead of each hardcoding `["integrity", clause keeps it in sync if a key is renamed.
- Export
- v0.6.2
- HOTFIX: Wikipedia bloom filter inlined as base64. Production deployments on Vercel serverless were failing with
ENOENT: no such file or directory, open '/var/task/packages/core/data/wikipedia-trigrams.bin'. The Vercel bundler doesn't auto-include data files referenced via runtime path resolution. Inlined the 8 KB binary as a base64 string inalgorithms/wikipedia-paraphrase.ts(10,924 chars in source). Removes filesystem dependency entirely; works in any runtime (Node, Bun, Cloudflare Workers, Vercel serverless, edge).data/wikipedia-trigrams.binremoved from npmfilesarray. Build scriptbun run build-wikipedia-bloomis preserved — operators regenerating the corpus must also re-inline the base64 string (one-line node command in the file's docstring). - No behavioral change: same bloom filter, same hash functions, same ~5% FP rate. Tests verify decoded
Uint8Arraymatches the original binary.
- HOTFIX: Wikipedia bloom filter inlined as base64. Production deployments on Vercel serverless were failing with
- v0.6.1
- Closes the v0.6.0 calibration validation gap. The cutover code path (
siteVerdictFromTemplatesas default verdict source) was unmeasured at v0.6.0 release because all calibration corpus sites classified asunclear/small-marketingdue to the pinned-URL methodology giving the classifier only 25 URLs per site. v0.6.1 fixes this with a caller-supplied URL override. - New `AuditOptions.classifierUrls?: ReadonlyArray<string>`. When provided, the classifier and
detectTemplatesuse this list instead of the sitemap-discovered or pinned-URL set. Audit phase still runs againstpinnedUrls(cost-bounded). Distinct frompinnedUrls(audit set) andforce.urls(monitoring supplement). Use case: calibration fixtures audit a small sample but classify against full sitemap. - Engine wire-through at
auditor.ts:2334: explicit caller override takes priority over sitemap-derived URLs. - Calibration script `--seed-classifier-urls` mode fetches each corpus site's live sitemap.xml (recursively expanding sitemap-index entries; cap 5000 URLs per site), writes URL list back to corpus JSON's
classifierUrlsfield per site. - Verification result: 2 fixture sites now exercise the v0.6 path with real validation:
- Jasper classifies
programmatic-directory@0.78(was small-marketing), 9 templates detected, v0.6 path executes — verdict shifts caution → ready (improvement) - Airbyte classifies
programmatic-directory@0.9(was small-marketing), 6 templates detected, v0.6 path executes — verdict shifts caution → ready (improvement) - v0.6 cutover validated:
siteVerdictFromTemplatesproduces sensible (and in these cases better-than-legacy) verdicts on real programmatic-directory shapes. Closes task #99 + the validation gap doc atdocs/superpowers/calibration/2026-05-07-v0.6.0-validation-gap.md.
- Closes the v0.6.0 calibration validation gap. The cutover code path (
- v0.6.0
- Audit-as-template cutover (spec §16 v0.6.0 row).
siteVerdictFromTemplatesis now the default site verdict source when ≥1 template has ≥5% coverage of total discovered URLs (spec §15.1). When no template meets the threshold the engine falls through to the legacy risk-ladder verdict — bit-identical behaviour for single-template sites andunclear/small-marketingclassifications. Theriskscore is unchanged; only the verdict derivation switches. - JSON output schema documented as v0.6 stable. The
templates: Template[]field onAuditResultis now part of the stable public API. Thefindings: RuleResult[]flat list remains for backwards compatibility through v0.6.x; sunset decision deferred to v0.7 per spec §15.4. - Web dashboard per-URL findings list demoted to drill-down. When ≥2 templates are detected, the per-URL
FindingsPanelis wrapped in a<details>element collapsed by default ("Show all N per-URL findings"). Template cards remain the primary surface. Legacy / single-template audits render the expanded findings list unchanged. - Calibration verified: all 9 fixture-mode sites produce identical verdicts post-cutover because they classify as
unclearorsmall-marketingand the template activation gate correctly bypasses the new path. Pre-existing 2 failures (segment, numbeo) unchanged.
- Audit-as-template cutover (spec §16 v0.6.0 row).
- v0.5.15
- Fixed HTML fixture corpus + audit-from-disk for fully deterministic calibration. New
localFixtureDir?: stringfield on corpus site entries. New--snapshot [<filter>]mode inscripts/calibration-reputable-pseo.tscaptures HTML for each pinned URL topackages/core/calibration/fixtures/<host>/, with_manifest.jsonmapping original URLs to filenames +sitemap.xml+robots.txt. The engine's existing filesystem-mode inloadPagesFromSourceextended (~20 LOC) to read_manifest.jsonand present fixture pages with original HTTP URLs restored. - 8 sites snapshotted: G2, Wise, Typeform, Segment, Jasper, Ramp, Numbeo, Airbyte. 215 HTML files, 61 MB total committed to the repo. Strips
<script>/<style>blocks before save (preserves JSON-LD for schema rules; ~80% size reduction). Fixtures NOT bundled in the npm package —filesarray stays["dist", "data/wikipedia-trigrams.bin"]. Repo-only artifact. - Byte-identical verification: two consecutive fixture-mode runs across all 8 sites produce identical verdicts + risks: G2 ready/20, Wise concerning/45, Typeform caution/24, Segment critical/74, Jasper caution/27, Ramp caution/25, Numbeo concerning/60, Airbyte caution/25. Calibration is now fully deterministic — no network, no content drift, no random sampling.
- Newly-visible Numbeo verdict: was previously origin-erroring (30s p95). Fixtures reveal it stably scores
concerning/60against current ceilingcaution. Pre-existing calibration miss now made visible. Ceiling NOT updated in this release (per spec constraint); a follow-up calibration patch may raise it once the rule fire pattern is reviewed. - Stale-fixture failure mode: missing files referenced in a valid
_manifest.jsonpropagate as errors (fail-loud — programmer error). Directory without manifest falls through to existing path-based behavior. - 5 new tests in
tests/integration/audit-fixture-manifest.test.ts. Pre-existing failures unchanged at 2. - Refresh workflow:
bun run scripts/calibration-reputable-pseo.ts --snapshotre-captures all sites;--snapshot wisere-captures one. Quarterly cadence recommended; per-site refresh on suspected staleness.
- Fixed HTML fixture corpus + audit-from-disk for fully deterministic calibration. New
- v0.5.14
- Value-add composite extended to 7 signals.
content/wikipedia-paraphrase(shipped standalone in v0.5.13) now contributes to the composite as the 7th signal. Each signal now weighted at 1/7 ≈ 14.3% (was 1/6 ≈ 16.7%). Boundary cases at score=0.30 / score=0.50 may shift by ≤2.4 percentage points per signal — below severity-band granularity for most pages. - Test math updates: two existing value-add tests reshaped to reflect the new 7-signal denominator. The "bestfirenze pattern" test now includes a wikipedia-paraphrase finding among its inputs (without it, the pattern lands at exactly score=0.5 boundary and no longer fires); the "freshness=0.5" test adds the same. New behavior documented in test comments.
- The composite continues to fire ONE finding per page when score < 0.5, severity critical < 0.3, otherwise error.
- Value-add composite extended to 7 signals.
- v0.5.13
- Engine bug fix in
links/unreachable-from-rootrule: previously ran its full unreachable-from-root BFS on pinned-URL audits becauseisSampledAuditwasfalsein pinned mode. Pinned URLs ARE structurally a sample (same as random-sampled crawls). Fixed by changing the dispatcher guard toisSampledAudit || hasPinnedUrlsEarly. The rule now correctly suppresses on the pinned-URL path, eliminating 22 false-positive findings on Wise's locale-hub URLs. - Corpus annotation update: even after the false positives cleared, Wise stably scored
concerning(risk=45) due to 8 real blockers — 5content/title-uniquenesserrors + 3content/meta-uniquenesserrors on locale-hub pages that legitimately share titles. TheblockerFloormechanism kicks in at density 0.32 → floor 45. These ARE real signals; raised Wise'sexpectedVerdictCeilingfromcaution→concerningwith full annotation in the corpus JSON. - Net: Wise now passes the corpus test at
concerning. No other site's verdict changed. Test count: pre-existing failures dropped 4 → 2 (Typeform also resolved as side-effect of the BFS fix). - New rule
content/wikipedia-paraphrasedetects Wikipedia-paraphrased content via trigram bloom filter. Fireswarning/lowwhen ≥40% of page trigrams hit the reference corpus. - Bloom filter: 8 KB binary at
packages/core/data/wikipedia-trigrams.bin. Parameters: m=65536 bits, k=3 FNV-1a-32 hashes, ~5,032 unique trigrams indexed from a curated 12-article public-domain Wikipedia sample (CC BY-SA 4.0 attributions inscripts/wikipedia-samples/NOTICE.md). - Calibration evidence: verbatim Marie Curie paragraph → rate 0.72 (fires); original pSEO/commercial copy → rate <0.15 (no fire); random commercial English → rate <0.30 (FP rate within spec). Zero new fires across the 8 auditable reputable-pSEO corpus sites.
- New
bun run build-wikipedia-bloomscript regenerates the binary fromscripts/wikipedia-samples/. - RULE_IMPACTS:
baseImpact: 10, perInstance: 3, maxImpact: 25(lowest among content rules — speculative-confidence + warning). - Composite integration deferred: the v0.5.8
content/value-addcomposite still aggregates 6 signals. Extending to 7 (with this new rule) ships as v0.5.14.
- Engine bug fix in
- v0.5.12
- Pinned URL sampling for stable calibration runs. New
AuditOptions.pinnedUrls?: ReadonlyArray<string>— when provided non-empty, the auditor SKIPS sitemap discovery + random sampling and audits ONLY these specific URLs. Distinct from the existingforce.urls(which supplements the sample); pinnedUrls REPLACES it. Same-origin validation throws on cross-origin URLs (HTTP sources only). Filesystem sources interpret pinnedUrls as relative paths. - AuditSummary.auditedUrls added — sorted list of URLs the engine actually audited. Surfaced for the
--repincapture flow but useful for any caller that needs to know what got audited (vs what was discovered). - Why this matters: a verification pass on 2026-05-06 found that consecutive calibration runs against the same engine code produced 4/7 different site verdicts. Random page sampling within each site was the dominant variance source. With pinned URLs, two consecutive runs of Numbeo produced identical verdicts (concerning, 12 pages each) — proves the methodology fix works.
- Calibration script (
scripts/calibration-reputable-pseo.ts) gets--repin [<filter>]mode that runs each site once and writes the discovered sample URLs back to the corpus JSON's newpinnedUrlsfield. Default invocation reads pinnedUrls and audits those exact URLs. Backward compat: sites with empty pinnedUrls fall back to current random sampling. - Subtle decision: pinned URL 404s fail-soft (page is dropped from the audit set, run continues). Same as existing sitemap-discovered 404 behavior. Fail-loud would let one stale URL block the entire run.
- 7 new tests in
tests/integration/audit-pinned-urls.test.tscovering pinned-mode behavior, same-origin validation, sample-size override, andforce.urlsinteraction.
- Pinned URL sampling for stable calibration runs. New
- v0.5.11
- New rule `content/common-phrase-reuse` detecting overuse of pSEO marketing clichés. 42 phrases inline in 5 categories: location clichés ("in the heart of", "gateway to"), generic pSEO marketing ("discover the best", "trusted by thousands"), aggregator/listing ("top rated", "best of the best"), fake authority ("experts agree", "industry leaders"), filler hedges ("varies depending on", "wide variety of"). Case-insensitive substring match. Fires ONE finding per page when ≥3 distinct phrases match. Severity
warning, confidencelow. RULE_IMPACTS:baseImpact: 12, perInstance: 4, maxImpact: 30. - Value-add composite extended to 6 signals.
content/value-addnow aggregates: originality (regurgitated-content), freshness, citable facts, E-E-A-T, translation completeness, AND common-phrase reuse. Each signal weighted equally at 1/6 ≈ 16.7% (was 1/5 = 20%). Severity boundaries unchanged (<0.3 critical, <0.5 error, ≥0.5 no finding). Side effect: bestfirenze "severe" pattern shifts critical → error because the 1/6 denominator changes its score from 0.20 to 0.333 — boundary documented in updated tests. - Calibration on reputable corpus: zero new findings. The ≥3 threshold protects legitimate marketing copy that uses 1-2 of these phrases legitimately. No phrases were removed during calibration.
- 13 new tests (10 for the new rule, 3 for the 6-signal composite math). Total: 1024 passing (was 1010), 4 pre-existing baseline failures unchanged.
- New rule `content/common-phrase-reuse` detecting overuse of pSEO marketing clichés. 42 phrases inline in 5 categories: location clichés ("in the heart of", "gateway to"), generic pSEO marketing ("discover the best", "trusted by thousands"), aggregator/listing ("top rated", "best of the best"), fake authority ("experts agree", "industry leaders"), filler hedges ("varies depending on", "wide variety of"). Case-insensitive substring match. Fires ONE finding per page when ≥3 distinct phrases match. Severity
- v0.5.10
- v0.6 phase 3 — formatters render per-template cards. New shared module
formatters/template-cards.tsexportsrenderTemplateCardsConsole,renderTemplateCardsMarkdown,renderTemplateCardsHtml, plusshouldRenderTemplateCardsandTEMPLATE_CARDS_CSS. Each format-specific formatter (console.ts,markdown.ts,html.ts) calls the helper whensummary.templates.length >= 2 && !options.legacyFlat. JSON formatter unchanged —templatesrides through automatically. - Each card renders: signature (mono/code), grade chip via
gradeOf(risk), top-driver one-liner ("8/10 samples fail spam/thin-content"), URL coverage stat, uniformity bar with red/yellow/green tints at 0.4/0.7 thresholds. - Falls back gracefully on legacy audits:
shouldRenderTemplateCardsreturns false whentemplatesisundefined,null, or has fewer than 2 entries — so pre-v0.5.9 audit JSON files re-rendered through these formatters produce the flat list without error or warning. - 38 new formatter tests:
formatters/template-cards.test.ts(24) covers per-format rendering edge cases;formatters/legacy-flat.test.ts(14) verifies the flag bypass path.
- v0.6 phase 3 — formatters render per-template cards. New shared module
- v0.5.9
- v0.6 reframe phase 1 — template detection + per-template scoring (opt-in). Audit result gains an additive
templates: Template[]field populated when site classification isprogrammatic-directory/ecommerce/docsANDdetectTemplates(urls)returns ≥2 surviving clusters (≥1% URL coverage, ≥5 URLs each). EachTemplatecarries its signature, total URLs, audited URLs, per-templateverdict/risk/categories, and avarianceblock with per-rule fire-rates, uniformity score (1 − mean(stdev(per-rule binary fire pattern across samples))), and top-driver rule.findingsflat list is unchanged — phase 1 is purely additive.RuleResultgains optionaltemplate?: stringtagging for findings whosepageUrlbelongs to a detected cluster. - `siteVerdictFromTemplates(templates)` helper exported but NOT yet wired to override the headline verdict — that cutover lands in v0.6.0 per the rollout in
docs/superpowers/specs/2026-05-04-pseolint-v0.6-audit-as-template-reframe.md§16. Filters to templates with ≥5% coverage, picks the worst verdict. - Activation gating: degenerate sites (
unclear/small-marketing/blog) bypass v0.6 detection — the legacy single-template path runs and the v0.5.3-v0.5.8 signal stack catches them. Bestfirenze regression confirms this — its 6 URLs collapse to one template signature, gating fails, legacy path holds. - 31 new tests —
template-detection.test.ts(17) covers cluster filter math, ≥2-template threshold, URL→template lookup, long-tail bucket;per-template-scoring.test.ts(14) covers per-template verdict/risk computation, variance metric uniformity, top-driver, site verdict §15.1 logic. Total: 967 passing, 4 pre-existing baseline failures unchanged.
- v0.6 reframe phase 1 — template detection + per-template scoring (opt-in). Audit result gains an additive
- v0.5.8
- New rule `content/value-add` — composite that aggregates 5 existing signals into a single 0-1 per-page quality score: originality (1.0 if
content/regurgitated-contentdoesn't fire), freshness (aeo/freshness-signals), citable facts (aeo/citable-facts), E-E-A-T (content/eeat-signals), translation completeness (content/translation-no-op). Each signal weighted equally. Missing signals treated as 1.0 (best-case) — the rule only penalises _confirmed_ problems, not absence of evidence. - Severity bands: score < 0.3 → critical, 0.3 ≤ score < 0.5 → error, ≥ 0.5 → no finding (no noise on already-clean pages).
- Architecture: second-pass design —
valueAddRule(pages, allFindings)runs after the per-rule loop assembles findings. Doesn't re-parse pages; reads existing finding tags. SamesuppressedRuleSet/isRuleEnabledguards as other rules. - Calibration: bestfirenze regression test confirms the rule fires critical on its degenerate pages. Reputable corpus (G2, Wise, Webflow, Jasper, Ramp) — zero new findings; pre-existing 4 calibration failures unchanged. RULE_IMPACTS:
baseImpact: 25, perInstance: 8, maxImpact: 50. 13 new tests.
- New rule `content/value-add` — composite that aggregates 5 existing signals into a single 0-1 per-page quality score: originality (1.0 if
- v0.5.7
- Cheerio refactor for `content/regurgitated-content` — replaced 4 fragile HTML-structure regexes (
IMG_SRC_RE,IFRAME_SRC_RE,REVIEW_BLOCK_RE,GOOGLE_MAPS_NOOPENER_RE) with cheerio DOM selectors. Eliminates catastrophic-backtracking risk on adversarial HTML and fixes false-positives where the literal "Powered by Google" text appears inside an attribute value. Behavior on existing fixtures unchanged. - Bestfirenze.com regression test — new calibration test at
tests/calibration/bestfirenze-regression.test.tsguarding the v0.5.3-v0.5.6 stack end-to-end on a known-degenerate site (6-page directory, 0 unique content per page across/en/fr/it/de/es). Asserts: classifier guard trips →unclear, no severity demotions,spam/thin-contentfires at native error,content/translation-no-opcorrectly skips (30-word floor),content/regurgitated-contentfires, risk floors at ≥60, verdict ≥ concerning. +10 assertions. - Reputable-corpus sweep documentation at
docs/superpowers/calibration/2026-05-04-v0.5.7-sweep.md— zero verdict drift on currently-passing reputable sites (G2, Wise, Webflow, Jasper, Ramp); new translation-no-op + regurgitated-content rules don't fire on any reputable site (no false-positive exposure on current corpus); blocker density floor adds ≤1 point to Wise (0.16 ratio). Two threshold tweaks recommended for future review (not applied):content/title-uniquenessdemotion inunclear/programmatic-directoryprofiles; per-profile severity forcontent/translation-no-oponprogrammatic-directory(multilingual catalogs).
- Cheerio refactor for `content/regurgitated-content` — replaced 4 fragile HTML-structure regexes (
- v0.5.6
- Version bump to align with CLI 0.5.6 (CLI inline upload). Engine surface unchanged.
- v0.5.5
- Stratified URL sampling across templates in
planScrapeStrategy. When the candidate URL list exceeds budget by 1.5× AND clustering produces ≥2 templates with no single template dominating >80%, sample slots are allocated proportionally across template clusters (with amin(20, 10% × budget)long-tail bucket so singleton paths like/aboutaren't starved). Watched URLs (force-refetch) bypass the budget entirely. On a 100k-URL site with one giant/listing/:slugcluster (90k pages) and smaller/category/:slug(8k) and/help/:slug(2k) clusters, the audit now covers all three rather than 200 slots from the dominant cluster. 9 new unit tests intests/scrape-strategy.test.ts. - New rule `content/regurgitated-content` detecting Google Places API regurgitation patterns (the bestfirenze.com value-add gap). Heuristic-only, fires at
warningseverity when ≥2 of 5 signals are present per page: (1) "Powered by Google" attribution, (2) ≥60% Google-hosted images (googleusercontent.com,lh3.googleusercontent.com,maps.googleapis.com/maps/api/place/photo,streetviewpixels-pa.googleapis.com), (3) Google Static Maps embeds or Maps iframes, (4) Places API JS markers (PlacesService,AutocompleteService), (5) ≥5 review-style blocks with star ratings + paragraphs but no E-E-A-T author signal. RULE_IMPACTS:baseImpact: 15, perInstance: 5, maxImpact: 35. 9 new tests. v0.6 will add Wikipedia/Tripadvisor n-gram overlap with external corpus checks.
- Stratified URL sampling across templates in
- v0.5.4
- New rule
content/translation-no-opfor cross-language identical-content detection. When sibling pages share locale-path differences (e.g./en/fr/it/de/esof the same template) but body content is ≥95% similar, fire aserrorseverity with categorycontent(bucketintegrity). Catches the bestfirenze.com pattern at the rule level, complementing the v0.5.3 classifier degeneration guard. RULE_IMPACTS:baseImpact: 30, perInstance: 10, maxImpact: 60. 8 new unit tests covering positive (5 identical locale variants → 1 finding), negative (translated content with similarity < 0.95 → no finding), edge cases (single variant, non-locale-prefixed siblings, root template collapse).
- New rule
- v0.5.3
- Add
force.urlsaudit option (andforceRefetchUrlsonplanScrapeStrategy) — caller-curated "watched pages" list that always refetches, short-circuiting the monitoring matrix with a newRefetchReasonof"watched". Watched URLs absent from the sitemap are still audited. - Grading rethink: classifier degeneration guard, blocker density floor, verdict/grade alignment. Self-audit on bestfirenze.com (a 6-page tourism directory with 0 unique content per page across
/en/fr/it/de/eslocale variants) returned grade B / risk 37 / verdict "caution" — verdict and grade disagreeing because the classifier labelled the corpus assmall-marketing, which then demotedspam/thin-content,aeo/citable-facts,spam/doorway-patterntoinfo. Three calibration fixes: - Classifier degeneration guard (
site-classifier.ts): newapplyDegenerationGuard+corpusStatsFromPagesexports. AfterclassifySitereturnssmall-marketingorblog, the guard inspects parsed-page stats — if median word count < 50 OR ≥50% of pages share an identical title (with ≥4 pages), the classification is downgraded tounclearwith adegeneration-guard-trippedsignal.profileFor()recognises the signal and returns a no-overrides scoring profile so natural rule severities fire. - Blocker density floor (
scoreFromFindings): now takespageCountand floors risk byblockers / pageCountdensity. Bands at ≥0.15 / ≥0.3 / ≥0.5 floor at 25 / 45 / 60. Reputable directories sit at <0.05 and are unaffected; bestfirenze (5/6 = 0.83) floors at 60. - Verdict/grade alignment in dashboard layer (
@pseolint/web0.0.7): band labels now align with the engine's verdict ladder so a "B / caution" visual mismatch is impossible. Net effect on bestfirenze.com: classificationsmall-marketing→unclear, severity demotions removed, blocker count climbs, density floor applies — risk lands ≥60 (D / critical) with verdict and grade reading the same vocabulary.
- Add
- v0.5.0
- v0.5.0 (continued) — AI orchestrator: 25 tools, fix-manifest output, validators + diffs Net-new public API:
orchestrate(opts)— drives an LLM through 25 deterministic tools (sitemap fetch, template clustering, per-page rule checks, AEO probes against live Anthropic/Perplexity/Gemini, SerpAPI) and produces a fix manifest of concrete patches (replace_h1,rewrite_meta,add_jsonld,add_faq_block,rewrite_intro,add_internal_link,remove_thin_block, plus domain-levelrobots_txt/sitemap_xml/canonical_strategy). Architecture: rules become tools the LLM calls; the LLM picks order; budget caps (LLM tokens + external probe USD, pre-flight + reactive enforcement) bound spend. Watchdog injects a convergence reminder every N tool calls. Page cache by reference (HTML never travels in conversation history) keeps token cost bounded as audits scale. Manifest validation (Phase 4): 11 deterministic patch validators (Schema.org required-properties, robots.txt structure, sitemap XML, cheerio HTML safety with allowlisted tags, etc.) run on every LLM-proposed patch. Failed patches are dropped from the manifest and surfaced invalidation.failureswith structured location info — the LLM never gets the chance to ship a malformed JSON-LD block or an unsafe<iframe>to a user. Structured diffs (diffManifest): every patch maps to one of 5PatchDiffkinds (text_replace, html_insert, html_remove, file_replace, guidance) suitable for direct UI rendering with HTML-escaped attributes. External probe tooling:query_serp(SerpAPI, $0.005/call),ask_ai_engine(Anthropic / Perplexity Sonar / Gemini citation probes),validate_jsonld,check_robots,check_indexability,check_domain_llms_txt,check_domain_crawler_access. All cost-tracked. Public exports:orchestrate,runOrchestrator,orchestratorTools,defineTool,validateManifest,diffManifest,manifestSchema,buildSystemPrompt,DEFAULT_BUDGET, plus typesFixManifest,BudgetCaps,UsageSnapshot,StopReason,SessionEvent,SessionResult,PatchDiff,ManifestDiff,ManifestValidationReport. AbortSignal threading through every I/O tool. AsyncLocalStorage-backed page cache. Verified end-to-end across 4 dogfood runs against pseolint.dev (final run: 36 tool calls, $0.55, 4 minutes wall, completed manifest with 5/6 patches passing validators — one rejected meta-description for being 167 > 160 chars). - v0.5.0 — Change-driven monitoring Why: Monitoring runs on a 4k-page site re-fetched everything. Rule evaluation is microseconds; the fetch is seconds. The pre-v0.5
--sinceflag did change-detection at the wrong layer — it post-filtered findings on already-fetched pages, paying the network cost on every URL just to skip a few microseconds of CPU. v0.5 moves the decision upstream of the fetch so unchanged URLs are never network-touched. Architecture: NewplanScrapeStrategy()pure module evaluates each candidate URL against a 7-reason decision matrix BEFORE fetching: new, age floor (default 7d), ruleset version mismatch, open findings recheck, sitemap<lastmod>newer than prior fetch, GSC delta (Pro), or no skip evidence. URLs that match a refetch reason are fetched as today; URLs that matchunchangedare skipped entirely and their findings are carried forward from prior state withcarriedForward: trueandlastVerifiedAtmarkers. Expected savings depend strongly on sitemap hygiene: - Sites with `<lastmod>` in sitemap.xml (Next.js, WordPress/Yoast, Astro): up to ~95% fetch reduction on steady-state monitoring runs once the prior state has aged past the recheck-trigger findings.
- Sites without `<lastmod>` (custom-rolled sitemaps, older CMSes): every URL hits the
no-signalreason and gets refetched. v0.5 monitoring helps via faster cache revalidation but doesn't skip the round-trip. A future HEAD-fallback path (deferred) will close this gap. Severity gate on therecheckreason: onlyerror,critical, andwarningfindings trigger a per-run recheck.infofindings carry forward without re-fetching the page. Without the gate, any URL with any finding would refetch and the carry-forward path would be dead code. Breaking: - State schema bumped to v2. Existing
.pseolint/state.jsonfiles from v0.4.x are discarded with a warning on first read; users get one full baseline audit, then incremental monitoring kicks in. - Auto-monitoring is the new default when a prior state file exists. Pre-v0.5 required
--sinceto opt in. Use--mode=freshto force a full re-audit even with prior state present. Added: planScrapeStrategy()exported from@pseolint/core— pure decision matrix; testable without I/O.CORE_RULESET_VERSIONconstant. Bump when adding rules or materially changing rule logic so monitoring runs re-evaluate previously-skipped URLs against the new ruleset.AuditSummary.scrapePlanreportsfetched/carriedForwardcounts, per-reason breakdown, ruleset version, and last full audit timestamp.RuleResult.carriedForwardandRuleResult.lastVerifiedAtmark findings carried over from a prior run for staleness reasoning.UrlStateEntry.findingsnow persists full RuleResult records (not just IDs) so future runs can carry them forward.parseSitemapUrlsWithLastmod()exported — sitemap walker now surfaces<lastmod>alongside URLs.- CLI:
--mode=monitoring|freshand--age-floor-days=N. Changed: --sinceis now an alias for--mode=monitoring(kept for back-compat). Behavior is unchanged for users who passed it explicitly.collectUrlsFromSitemapreturns{ urls, lastmodByUrl }instead ofstring[]. Internal API; no external consumers.RunStateadds requiredrulesetVersionandlastFullAuditAt. See spec:docs/superpowers/specs/2026-05-01-change-driven-monitoring-design.md.
- v0.5.0 (continued) — AI orchestrator: 25 tools, fix-manifest output, validators + diffs Net-new public API:
- v0.4.3
- v0.4.3 — classification-driven scoring, fixes credibility crisis Trigger: an external reviewer ran pseolint on his own site and didn't believe the verdict. Three-site dogfood (nextjs.org, wordpress.com, shopify.com) confirmed the issue — nextjs.org would score
concerning(60) on a meaningful sample, with 7 of 11 actionable findings being AEO-style "your marketing page doesn't read like a fact database." Citation category alone drove 25 risk points just from the AEO bucket maxing out. This release rewires scoring so the verdict is classification-aware: a docs site is judged by what matters for docs sites; a programmatic directory is judged by what matters for pSEO; a marketing site isn't penalized for being marketing-shaped. Changes: - New
SCORING_PROFILESmap keyed onSiteType. Each profile defines per-category weights + severity overrides + confidence overrides. Applied when classifier confidence ≥ 0.7; below that, conservativeuncleardefaults. - New
RULE_IMPACTSmap gives every rule its ownbaseImpact + perInstance × maxImpactcurve, replacing the global 4-tierSEVERITY_WEIGHTS. Spam findings amplify with cluster size; AEO findings stay capped low. - New
Confidencetype (high | medium | low | speculative) onRuleResult. Per-rule emit logic on 10 rules. Low-confidence findings carry a caveat in the message and contribute proportionally less to scoring. - New site classifier types:
docs(Docusaurus / Nextra / GitBook / VuePress shape) and improvedecommerce(Shopify / WooCommerce shape). Plus atryClassifyLocalizedMarketing()detector that prevents stripe.com / vercel.com / linear.app from being misclassified asprogrammatic-directorybecause of/[lang]/URL prefixes. - Re-tuned tech rule impacts:
tech/hreflang-consistencyis now a single base-impact finding regardless of how many language pairs break (one declaration breaks them all — count shouldn't compound).tech/canonical-consistencylowered to base 8, perInstance 1. - Formatters surface "Audited as <type> (NN% confidence)." prominently. Confidence caveats render after low-/speculative-confidence findings.
- Marketing copy on /, /tools, /rules, /symptoms, /limits clarifies scope: "pseolint audits programmatic-SEO + AI Overview readiness. Not a general SEO audit — for Core Web Vitals and broken links use Sitebulb, Screaming Frog, or Ahrefs." Dogfood results (post-change): ``
nextjs.org ready (14) → ready (15) no regression react.dev caution (23) → ready (15) improved stripe.com [aborted] → ready (9) correctly classifies localized wordpress.com caution (24) → ready (12) improved shopify.com concerning(58)→ caution(28) -30 risk; remaining findings real`Tests: 663/663 pass (was 646 in v0.4.2 — +17 new tests for classifier types, scoring profiles, confidence emission, formatter output). **Reasoning trail:** seedocs/superpowers/specs/2026-04-30-pseolint-scoring-credibility.md` for the full diagnosis + design rationale.
- v0.4.3 — classification-driven scoring, fixes credibility crisis Trigger: an external reviewer ran pseolint on his own site and didn't believe the verdict. Three-site dogfood (nextjs.org, wordpress.com, shopify.com) confirmed the issue — nextjs.org would score
- v0.4.2
- v0.4.2 — page-skip extensions, framework-aware web defaults, template bucketing, fixplan artifact @pseolint/core (0.4.1 → 0.4.2) Three new page-skip filters extending the v0.4.1 noindex / auth machinery:
skipBoilerplate?: boolean(defaultfalse) — skip cookie / legal / consent / imprint pages via title, H1, or URL pathname matching well-known compliance-page patterns (/privacy,/terms,/cookies,/gdpr,/ccpa,/impressum,/disclaimer,/accessibility,/do-not-sell, etc.). Single-signal trigger because patterns are anchored — a marketing page that mentions "privacy" in its body won't fire. NewdetectBoilerplatePage(page)exported from./page-filter.js.skipSearchPages?: boolean(defaultfalse) — skip pages with search-result URL hallmarks: query parameterq/query/search/s/keyword, or pathname starting with/search. Per Google's own guidance these should benoindex'd but many sites don't tag them.skipEmptyBody?: boolean(defaultfalse) — skip un-hydrated SPA shells: body text < 100 chars, script tags present, no substantive<noscript>fallback. The right fix is--render, not content rules.pageSkipReason()now returns"noindex" | "auth-detected" | Theaudit/skipped-by-policy` diagnostic surfaces all five reason categories with per-reason counts. Plus a refactor for output ergonomics:- New
bucketByTemplate(findings)helper at./formatters/bucket-findings.js. Console + markdown formatters now collapse findings sharing a template signature into one line (× 23 instances on /templates/[state]-llc-fees the legacy format. Site-wide / non-template buckets render as× 2 affected pages`. - New
formatFixplan(summary)formatter at./formatters/fixplan.js. Emits a markdown checklist of fixes ordered by effort (quick wins → moderate → structural → other), each item bucketed by template, with a Skipped section breaking down noindex / auth-detected counts and a footer wallclock estimate. Designed for paste-into-GitHub-issue use. pseolint CLI (0.4.1 → 0.4.2) - New flags:
--skip-boilerplate,--skip-search-pages,--skip-empty-bodymirror the correspondingAuditOptions. All off-by-default to preserve CLI back-compat. @pseolint/mcp (0.4.1 → 0.4.2) - Workspace dep bump to pick up the new
AuditOptionsfields. No tool surface changes; callers can pass the new flags via the MCP audit tool's options if they want them. Web app (0.0.4 → 0.0.5) The hosted form audit pipeline now adapts to the audited site's framework: - New
FRAMEWORK_IGNORE_PATTERNSmap keyed onnextjs | wordpress | idiomatic-but-non-marketing routes (e.g. WordPress/wp-includes/, Shopify/cart, Next/_next/data/, etc.) layer additively on top of the baseWEB_AUDIT_DEFAULT_IGNORE`. - New
detectFrameworkFromUrl(url, signal?)helper does a single preflight HEAD/GET of the source root, checksx-powered-by,x-vercel-id,x-shopify-*response headers + script-src body signatures, and returns the framework key. Wrapped in anAbortControllerwith a hard 5s timeout so a slow root doesn't block the audit. run-audit.tsruns framework detection in its own Inngest step (detect-framework) before the audit step. Failure / timeout falls through toresolveAuditIgnorePatterns(undefined)(base list only). Tests: 646/646 pass. core + cli + mcp + action all build clean.
- v0.4.1
- v0.4.1 — config UX fixes + page-skip policy @pseolint/core (0.4.0 → 0.4.1)
- New
respectNoindex?: boolean(defaulttrue) — pages explicitly markednoindex(via<meta name="robots">orX-Robots-Tagheader) are excluded from rule evaluation. The site owner already opted out of SEO indexing for them; auditing produces noise the reader can't act on. The two noindex-conflict rules (tech/canonical-noindex-conflict,tech/robots-noindex-conflict) andtech/hreflang-consistencystill receive noindex'd pages so they can flag accidental noindex'ing / inconsistent hreflang declarations. - New
skipDetectedAuth?: boolean(defaultfalse) — heuristic detection of login / signup / password-reset / verify-email pages via password-input density, page title pattern (brand-suffix-stripped), and H1 pattern. Requires 2+ signals for a positive verdict, keeping false-positives low on marketing pages with single-signal characteristics. - New
audit/skipped-by-policydiagnostic surfaces every URL skipped by the above policies insummary.diagnostics.auditFindings— the accidentally-noindex'd page now shows up as a visible skip line instead of being absent without explanation. - New
warnUnmatchedIgnore?: boolean(defaultfalse) — per-pattern warning for unmatched--ignorepatterns is now opt-in. The CLI sets it to true only when--ignorecame from the command line. Config-loaded patterns (e.g.pseolint.config.tswith broad safety patterns like**/api/**) no longer spam warnings when the patterns legitimately don't match a small site's surface. A consolidated `none of the N ignore patterns fires when ALL patterns miss, regardless of source. - New helpers exported from the entry point:
detectNoindex,detectAuthPage,pageSkipReasonfrom./page-filter.js. pseolint CLI (0.4.0 → 0.4.1) pseolint.config.tsfiles are now auto-loaded via cosmiconfig (jiti loader). Previously only.js/.cjs/.mjs/.jsonconfigs were picked up;.tsfiles silently fell through, forcing users to inline--ignorepatterns. Bothpseolint.config.tsand.mtsvariants are now in the searchPlaces list.- New flag
--no-respect-noindex— audit pages marked noindex anyway (useful when investigating an accidentally-noindex'd page). - New flag
--skip-detected-auth— opt into the heuristic auth-page skip. @pseolint/mcp (0.4.0 → 0.4.1) - Picks up the core changes via the workspace dep bump. No tool surface changes; auth-page detection / noindex respect are now available to callers that pass the relevant
AuditOptionsfields. Web app Public-form audits run throughapps/web/src/inngest/functions/run-audit.tsnow apply opinionated defaults: aWEB_AUDIT_DEFAULT_IGNOREURL pattern list (framework metadata + auth + admin + API + WordPress conventions) plusrespectNoindex: trueandskipDetectedAuth: true. End users running audits via the public form no longer see noise from utility routes. Tests: 604 / 604 pass. Engine + CLI + MCP + action all typecheck clean.
- v0.4.0
- v0.4.0 — engine redesign Breaking-change release coordinated across all three packages. @pseolint/core (0.3.3 → 0.4.0)
- Replaced numeric
score(0–100, lower = better) with the verdict ladderready(≤20) |caution(≤40) |concerning(≤60) |critical(>60). The numeric value is still emitted asriskfor sorting and CI gating. - Consolidated 8 source-tree categories into 4 scoring super-categories:
integrity(spam + content + cannibal, weight 0.50),discoverability(links + tech, 0.20),citation(aeo + schema, 0.25),data(0.05). - Dropped 8 noisy rules (cannibal/title-overlap, cannibal/keyword-collision, content/heading-uniqueness, links/hub-pages, plus four others). Total rule count is now 32.
- New
AuditSummaryshape:schemaVersion: "2026-04-v0.4",verdict,risk,headline,categories[].grade/issues, `issues.{blockers, - New
siteClassificationfield (§4.11): pre-flight URL-pattern + sitemap - framework heuristics infersmall-marketing | blog | programmatic- (spam/template-coverage,spam/template-diversity,spam/entity-swap,cannibal/url-pattern) on small sites unless the caller passesstrict: true`. - New
AuditOptions.strictflag to bypass classification-driven rule suppression. pseolint CLI (0.3.2 → 0.4.0) - New flags:
--ci-threshold <severity>(replaces numeric--thresholdfor CI gating),--explain(full bucketed finding view),--strict(bypass pSEO-only rule suppression). --thresholddeprecated with a runtime warning — still functional for one minor.- New
pseolint diff <baseline> <current>subcommand: verdict-rank deltas + fixed/regressed/new findings between two AuditSummary JSON reports. Tolerates mixed v0.3 / v0.4 JSON. - Console formatter rewritten around the verdict ladder. JSON, HTML, and Markdown formatters all consume the new shape.
--watchflag reserved (planned for v0.4.1, not implemented). @pseolint/mcp (0.3.1 → 0.4.0)- All three tools (
audit_site,explain_score,check_page_technical) migrated to the v0.4AuditSummaryshape:summary.score→summary.risk,summary.findings→ flattened fromissues.*,summary.categoryScores→summary.categories. explain_scorenow surfacessiteClassification(type, confidence, suppressed-rule count) when present.CROSS_PAGE_RULESset updated to remove the 4 dropped cross-page rules. Migration Pre-existing v0.3 JSON reports remain readable —/r/[slug]and/r/comparein the web app detectsummary.schemaVersionand route through legacy renderers. New CI gates should switch from--threshold 40(numeric) to--ci-threshold concerning(semantic). Spec:docs/superpowers/specs/2026-04-29-pseolint-v0.4-engine-redesign.md.
- v0.3.3
- v0.3.3: safeMode preset, CLI safety flags, MCP safe-by-default Consolidates the safety work from render-analytics blocking (v0.3.1), SSRF / AbortSignal / robots-honor (v0.3.2), and the safeMode preset (v0.3.3) for the CLI and MCP packages, which skipped the prior intermediate releases on npm. ## @pseolint/core → 0.3.3 Incremental over 0.3.2 (which shipped SSRF guard, AbortSignal support, and robots.txt honour for our own crawler):
safeMode: "saas" | "cli"preset onAuditOptions— flipsguardSsrf,respectRobotsTxt,followRedirects,maxCrawlDiscovered, andmaxFetchBytesdefaults in one knob. Individual option overrides still win.safeFetch(url, options?)— SSRF-safe fetch for non-audit use cases (webhook URL verification, favicon lookups, etc.). WrapscachedFetchwithvalidateTargetHostbaked in.maxCrawlDiscovered— hard ceiling on link-discovery fan-out so a malicious site with many self-links can't extend crawl up to the byte budget. Default 5000 (2000 undersafeMode: "saas").followRedirects: falseoption — returns 3xx as-is so security- sensitive audits can report redirects without following them. New exports:safeFetch,SafeModetype. ## pseolint (CLI) → 0.3.1 First CLI release since 0.3.0; bundles the v0.3.1 / v0.3.2 / v0.3.3 CLI-facing work:- Render-mode analytics blocking flags:
--analytics <block|allow|allow-first-party>,--block-host <host>(repeatable). Prevents rendered audits from firing GA / Plausible / PostHog / Mixpanel / Hotjar / Sentry beacons on every page. --safe-mode <saas|cli>— applies the core preset.--no-respect-robots— audit sitemap URLs even when the target's robots.txt Disallow's them (use for your own staging sites).--no-follow-redirects— report 3xx as-is.ctrl-Chandler — SIGINT triggers a clean abort viaAbortController; in-flight fetches cancel cleanly instead of the process being hard- killed mid-read. Secondctrl-Cwithin ~1 s forces exit.pseolint.config.tsschema extended forsafeMode,respectRobotsTxt,followRedirects,guardSsrf,maxCrawlDiscovered. ## @pseolint/mcp → 0.3.1 First MCP release since 0.3.0; picks up all of v0.3.1 / v0.3.2 / v0.3.3 via the core update, plus one meaningful default flip:- All three tool handlers (
audit_site,explain_score,check_page_technical) now defaultsafeMode: "saas". AI assistants running in end-user environments can't be tricked into scanning cloud metadata / localhost / RFC1918 networks via a malicious URL argument.guardSsrf,respectRobotsTxt, tighter caps all flip on. ## Not changed@pseolint/action— runs in GitHub-public-network runners, auto- propagates new core features through its existingAuditSummaryrendering. No separate bump. ## Test state 528 / 528 tests pass. Typecheck clean across core / cli / mcp / action.
- v0.3.0
- feat: add AEO rule category with 8 rules for AI Overview readiness Introduces
aeo/*— a new scored rule category focused on Answer Engine Optimization. Brings the total to 42 rules across 8 categories (7 scored data/*). While SpamBrain rules protect against Google penalties, AEO rules audit whether pSEO pages are structured to be cited in AI Overviews (ChatGPT, Perplexity, Gemini, Claude, Google AI Overviews). Sites invisible to AI Overviews lose ~68% of traffic vs ~12% for cited sites. New rules:aeo/llms-txt— checks for/llms.txtat the origin and validates the minimal shape (H1 title, at least one##section, markdown link entries).aeo/crawler-access— parsesrobots.txtper user-agent and flags blocked AI crawlers (GPTBot, ChatGPT-User, ClaudeBot, PerplexityBot, Bytespider, Google-Extended, CCBot, Applebot-Extended). Warns per blocked crawler, errors when all are blocked.aeo/freshness-signals— checks each page for a dateModified signal (JSON-LD,article:modified_time, visible "Last updated" text). Warns when absent, emits info when older than 180 days.aeo/faq-coverage— detects FAQ-style content (question-phrased H2s or URL patterns like/how-to-*,/what-is-*,*-faq) that lacks FAQPage or HowTo JSON-LD.aeo/answer-first— scores the first paragraph after the H1 for extractable-answer quality: concrete facts, named entities, complete sentence, boilerplate detection, and template-opener detection via entity masking across the corpus.aeo/citable-facts— counts unique, entity-specific citable facts per page (dollar amounts, percentages, timeframes, dates, Form numbers). Filters out template facts shared across the majority of masked pages.aeo/non-replicable-value— detects pages that are pure informational text with no interactive element, downloadable asset, or gated content — pages AI can fully summarize without sending a click.aeo/content-modularity— splits pages by H2/H3 and flags sections that aren't independently extractable (cross-references like "as mentioned above", vague headings like "More Info", paragraphs over 200 words). ### Scoring Category weights re-balanced.aeojoins the composite score at 15%: before (0.2.x): spam 0.40 / content 0.25 / links 0.15 / tech 0.10 / schema 0.05 / cannibal 0.05 after (0.3.0): spam 0.35 / content 0.20 / aeo 0.15 / links 0.12 / tech 0.08 / schema 0.05 / cannibal 0.05AuditSummary.categoryScoresnow includes anaeofield. ### Config New flat threshold keys inAuditOptions.rules(all optional, sensible defaults):answerFirstMaxWords(default 100) — opener length cap foraeo/answer-firstcitableFactsMin(default 3) — below this a page errorscitableFactsTarget(default 8) — at or above this a page passesfreshnessMaxStaleDays(default 180) — age at whichdateModifiedis flagged stalemodularityMaxParagraphWords(default 200) —aeo/content-modularitymodularityMinSelfContainedRatio(default 0.7) —aeo/content-modularityfaqMinQuestionHeadings(default 2) —aeo/faq-coveragePage-groupoverridesstill apply normally for severity tuning. ### AEO sub-score and console sectioncategoryScores.aeois the sub-score (raw 0–100 damage — lower is better) and has its own label scheme distinct from the SpamBrain Risk label:- 0–20 AI-Ready — pages structured for citation
- 21–40 Partial — some citable, others vulnerable
- 41–60 Vulnerable — most pages will be summarized away without clicks
- 61–80 Invisible — pages offer nothing AI can't synthesize itself
- 81–100 Ghost — blocked from AI + no citable structure; traffic will crater
aeoScoreLabel(score)is exported from@pseolint/coreso downstream formatters can surface the label. The console formatter renders a dedicated AEO: AI Overview Readiness section between group scores and AI triage when anyaeo/*findings are present. ### AI triage Prompt bumped to1.1.0(additive). The system prompt now explicitly distinguishes two threat families — SpamBrain penalty risk (spam/cannibal/ content/data/tech/schema/links) and AI Overview invisibility (aeo/\*) — and asks for at least one root cause from each when both families are present. A newfindingCountByCategoryfield in the prompt payload gives the model per-category totals for weighting. - feat(core): diff-mode audits + per-rule scope declarations Adds a declarative
RULE_SCOPEmap atrules/scope.tsthat marks every rule ID as either"page"(output depends only on a single parsed page) or"corpus"(needs the full set of pages — clustering, cross-page comparisons, the link graph, robots.txt). NewAuditOptions.mode: "full"(default) — runs all rules; identical to prior behaviour."diff"— skips corpus-scoped rules so daily diff-audits can re-evaluate only changed pages without re-running clustering / link-graph / sitemap rules against the unchanged corpus. Consumed by the hosted monitoring pipeline (apps/webinngest/functions/monitor-domains.ts). Local CLI and MCP flows are unaffected — the default remains"full". Public API additions:RULE_SCOPE—Record<string, RuleScope>, complete map of all 42 current rule IDs.isRuleAllowedInDiff(ruleId)— returns true for page-scoped rules; unknown IDs default to corpus (safer).RuleScopetype —"page" | "corpus". AEO rules are included in the scope map:aeo/freshness-signals,aeo/faq-coverage,aeo/answer-first,aeo/citable-facts,aeo/non-replicable-value,aeo/content-modularityare page-scoped;aeo/llms-txtandaeo/crawler-accessare corpus-scoped (they need origin-level state). Note:answer-firstandcitable-factsperform cross-page template detection internally, but in diff mode they receive only the changed pages — template-fact and template-opener detection are effectively scoped to the diff set, not the historical corpus. This is acceptable for daily re-runs; full audits remain the source of truth for corpus-wide patterns.
- feat: add AEO rule category with 8 rules for AI Overview readiness Introduces
- v0.2.2
- fix(publish): rewrite
workspace:*to real semver in published dependencies The 0.2.1 tarballs shipped withworkspace:*in theirdependencieslists, which npm cannot resolve. Anynpx pseolintornpm install pseolintsilently failed. Republishing viachangeset publishrewrites the workspace protocol to the real version range.
- fix(publish): rewrite
pseolint
- v0.7.3
- v0.7.3 — render-aware checks, AI content-effort, and bring-your-own inputs. Verdict moderators never change the raw
risknumber, so CI gates keyed off--ci-thresholdstay stable. - Bring-your-own authority. New
--authority-score <0-100>CLI flag andauthorityScoreconfig key, alongside the existing MCPauthorityScoreparam and a per-domain setting in the Pro web dashboard.>= 80shifts the verdict one tier lenient,<= 30one tier stricter. The engine stays authority-blind by design. - AI content-effort signal. New
--content-effort(+--content-effort-model) CLI flag,AuditOptions.contentEffort, and an optional MCPcontentEffortparam. An LLM judges a 0-100 content originality/effort score from sampled page text (≤10 pages, content-hash cached) that moderates the verdict ±1 tier. Opt-in, needsANTHROPIC_API_KEY, no-ops safely without one (~$0.003/page on claude-sonnet-4-6). Resolved score is written tosummary.contentEffort.score. Runs automatically for Pro audits in the web app. - Render-aware crawl checks.
--render(Playwright, Node-only) now feeds two render-diff rules:tech/csr-bailoutflags pages whose substantive content/interactivity exists only after client-side JS (invisible to crawlers + the first indexing pass), andtech/soft-404probes one synthetic nonexistent URL per template cluster (an HTTP 200 means the directory will index unbounded junk). Both no-op without--render/ outside programmatic directories. - MCP rule knowledge as resources. The MCP server now exposes the pSEO rule catalog as resources (
pseolint://rulesindex +pseolint://rules/<ruleId>per-rule Markdown) so assistants can explain findings without guessing. The open knowledge bundle is also served at/okfand linked fromllms.txt. - Updated dependencies [3361782]
- @pseolint/core@0.7.3
- @pseolint/mcp@0.7.3
- v0.7.3 — render-aware checks, AI content-effort, and bring-your-own inputs. Verdict moderators never change the raw
- v0.7.2
- fix(core): schema/consistency no longer false-positives on pages with multiple JSON-LD blocks. The v0.7.1 per-cluster rewrite compared the UNION of @types across a cluster, so a template where every page legitimately emits several blocks (e.g. TechArticle + FAQPage + Organization) read as "mixed types" and fired on every cluster (6 FPs on pseolint.dev's own audit). Now it compares each page's @type SET signature and fires only when pages in the same template cluster genuinely disagree.
- fix(core): v0.7.2 rule-design batch — graded thresholds + presence-quality. Follow-up to the v0.7.1 FP-elimination batch, addressing the two deferred root causes (C: binary/absolute thresholds, D: presence-not-quality). Verified against the 24-fixture calibration corpus: zero new false positives vs the prior metrics, and the crawl-size verdict flips are gone. C — binary-threshold redesigns:
- spam/boilerplate-ratio: continuous document-frequency weighting replaces the floor(N\*0.8)+1 skeleton cliff; 2-band severity. Verdict no longer flips when one more sibling page is crawled.
- spam/template-diversity: log-bucketed coarsening of the structureSignature so single-template sites with minor chrome variation are no longer read as diverse (the exact-count signature shared with near-duplicate/doorway-pattern is untouched); confidence band.
- content/value-add: continuous categoriesPresent/4 E-E-A-T sub-score replacing the 3-step hard-threshold value; 2-band severity (drops "critical").
- content/wikipedia-paraphrase: min-length guard + threshold 0.40→0.55 above the legal/medical topic-overlap baseline; advisory language, stays low-confidence. D — presence-quality (validate the value, not just its presence):
- schema/required-fields: empty arrays / whitespace / nameless author objects count as missing.
- schema/json-ld-valid: @type accepts string OR all-string array (["Article","NewsArticle"] no longer false-positives).
- tech/og-completeness: whitespace values count as missing; severity graded (title/description warning, image-only info).
- content/eeat-signals: transparency signal reads contentText not raw html; about-link must be same-host.
- Updated dependencies [cc24997]
- Updated dependencies [3c9cb0d]
- @pseolint/core@0.7.2
- @pseolint/mcp@0.7.2
- v0.7.1
- v0.7.1 — rule false-positive elimination batch (post unique-value design review). Stops the engine flagging healthy sites without weakening real detection. Each fix is TDD'd and validated against the reputable-pSEO fixtures.
- links/orphan-pages, links/cluster-connectivity — suppressed on sampled crawls (the linking/target page is often un-fetched; reliable only on a full crawl).
- tech/canonical-consistency — collapse "canonicalizes outside crawl scope" to one site-level note when all pages point at the same alternate host (staging/ preview/localhost), instead of one finding per page; dedup HTTP-vs-HTML.
- tech/sitemap-completeness — normalize sitemap URLs before the set-diff (kills trailing-slash/query false "missing"); demote the missing aggregate to warning.
- schema/consistency — flag @type variance per template cluster (structureSignature), not site-wide (was a guaranteed FP on any multi-template site).
- aeo/crawler-access — honor robots
Allowdirectives per RFC 9309 (allow-all no longer reported as fully blocked). - Severity/confidence bands — error/critical demoted to warning on weak or forecast signals: thin-content medium band, summary-bait, translation-no-op, entity-swap (low mask coverage), soft-404 (OR-weighted confidence model). Note: bundled as a patch (0.x) despite a behavior/scoring shift and the
rules.uniqueValueMinWords→rules.uniqueValueDensityconfig rename. - Updated dependencies [d9797e4]
- Updated dependencies [ce06ef7]
- @pseolint/core@0.7.1
- @pseolint/mcp@0.7.1
- v0.7.0
- v0.7.0 — Calibration & authority foundations
- Two-sided calibration harness + score-vs-outcome instrument. New
calibrationMetrics()reports how well the risk score tracks real winning/penalized outcomes against a labeled corpus — threshold-free AUC, class-separation gap, per-band empirical penalty rate, and the over-flag / recall-leak confusion-zone sites. Adetectabilitycorpus field separates the engine's addressable ceiling from structurally-undetectable (off-page) cases. - Corpus-derived entity auto-masking (
deriveEntityPatterns): clusters pages by URL template and masks tokens that vary across siblings, lifting policy-violating recall (44% → 56% on the calibration corpus) and fixing the reputable-vs-spam risk inversion. - Domain-authority moderation scaffolding: a pluggable
AuthorityProvider(CompositeAuthorityProvidermax-combine,OpenPageRankProvider,CommonCrawlProvider) feeds the existing verdict-shift. Fail-safe no-op until an authority source is configured — no behaviour change by default. - Updated dependencies [ba1c6ca]
- Updated dependencies
- @pseolint/core@0.7.0
- @pseolint/mcp@0.7.0
- v0.6.4
- flush a partial
truncated:truereport on watchdog abort + warn on localhost concurrency override When the backpressure watchdog aborted a crawl mid-flight (the real user run at--concurrency 5against a single-process dev server), theOriginDegradedErrorpropagated out ofauditSource()and every downstream phase (dedup, rules, enrichment, scoring, report assembly) was skipped — the CLI caught the error, printed "aborted — origin looks degraded", and exited 1 with zero output. Protecting the origin is correct; throwing away everything collected is not.loadPagesFromSource()now fills a caller-owned salvage sink incrementally, so a mid-crawl abort no longer loses the pages already fetched.auditSource()catches the watchdog abort at the page-loading boundary (and at the next abort checkpoint, for fetch implementations that ignore the abort signal), recovers the partial page set, runs the rest of the pipeline over it, and returns a normalAuditSummarywithtruncated: trueandtruncatedReasonset to the origin-degraded message. A zero-page abort still returns a valid truncated summary instead of crashing. External aborts (ctrl-C / parent timeout) and--no-backpressureare unchanged. The CLI now prints a clear⚠ PARTIAL REPORTbanner to stderr for a truncated run, still writes/emits the report (JSON/console/etc.), and exits 1 so CI knows coverage was incomplete. It also warns (without changing the value) when a localhost/single-origin target is crawled with an explicit--concurrencygreater than the dev preset's 1, suggesting--concurrency 1. - Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- @pseolint/core@0.6.5
- @pseolint/mcp@0.6.5
- flush a partial
- v0.6.2
- Track
@pseolint/core0.6.2: Wikipedia bloom filter now inlined as base64 — fixes ENOENT errors on Vercel serverless deployments. CLI surface unchanged.
- Track
- v0.6.1
- Track
@pseolint/core0.6.1: newAuditOptions.classifierUrlslets callers feed the full URL set to the classifier while auditing a subset. Closes the v0.6.0 calibration validation gap. CLI surface unchanged.
- Track
- v0.6.0
- Track
@pseolint/core0.6.0:siteVerdictFromTemplateswired as default verdict source (spec §15.1). Verdict for sites with qualifying templates now comes from the worst-template-≥5%-coverage rule. Legacy single-template andunclear/small-marketingsites unaffected. CLI surface unchanged.
- Track
- v0.5.15
- Track
@pseolint/core0.5.15: filesystem-mode inloadPagesFromSourcenow reads_manifest.jsonto present fixture HTML with original URLs. Calibration corpus is now fully deterministic via the fixture system. CLI surface unchanged.
- Track
- v0.5.14
- Track
@pseolint/core0.5.14: value-add composite now aggregates 7 signals (added wikipedia-paraphrase). Each signal weighted at 1/7. CLI surface unchanged.
- Track
- v0.5.13
- Track
@pseolint/core0.5.13: Wise calibration drift fixed (engine bug inlinks/unreachable-from-rootfor pinned-URL audits + corpus annotation update); newcontent/wikipedia-paraphraserule fires automatically onpseolint scanwhen page text overlaps ≥40% with the bundled Wikipedia trigram corpus.
- Track
- v0.5.12
- Track
@pseolint/core0.5.12 (calibration stability via pinned URL sampling). CLI surface unchanged — pinned URLs are an internal calibration concern. The newAuditOptions.pinnedUrlsis available to programmatic callers if needed.
- Track
- v0.5.11
- Track
@pseolint/core0.5.11: newcontent/common-phrase-reuserule fires automatically onpseolint scanwhen ≥3 pSEO marketing clichés are present per page; value-add composite now aggregates 6 signals (added common-phrase reuse).
- Track
- v0.5.10
- v0.6 phase 3 — per-template flags on `pseolint scan`.
--per-template(default ON) — render per-template cards above the per-URL findings list when ≥2 templates are detected. Use--no-per-templateto disable.--template <signature>— filter findings to one template'sauditedUrls(e.g.--template /listing/:slug). Silently ignored when the signature doesn't match any detected template (CI configs may go stale across runs; soft-fail beats hard-fail).--legacy-flat— opt-out flag that takes priority over--per-templatewhen both are set. Renders the legacy per-URL-only view for users who have built tooling against the flat list.- 11 new CLI test cases covering flag parsing, defaults, and mutual-exclusivity resolution.
- v0.5.9
- Track
@pseolint/core0.5.9: v0.6 phase 1 lands in the engine (additivetemplatesfield on audit JSON output for multi-template sites). CLI surface unchanged — phase 3 (v0.5.11) wires--per-templateand--legacy-flatflags.
- Track
- v0.5.8
- Track
@pseolint/core0.5.8: newcontent/value-addcomposite rule fires automatically onpseolint scanwhen the per-page quality score lands below 0.5.
- Track
- v0.5.7
- Track
@pseolint/core0.5.7:content/regurgitated-contentnow uses cheerio for DOM traversal (no behavior change); bestfirenze.com regression test added to the calibration suite; reputable-corpus sweep doc shipped.
- Track
- v0.5.6
- Inline upload after scan. New
--upload-to <endpoint>,--upload-token <token>,--upload-domain-id <id>flags on the main scan command. After the audit completes, the JSON summary is POSTed to<endpoint>/api/audits/uploadwith the bearer token. Replaces the two-step flow (pseolint scan -o report.jsonthenpseolint upload report.json) with a single command. Token + domain ID can also come fromPSEOLINT_TOKEN/PSEOLINT_DOMAIN_ID/PSEOLINT_ENDPOINTenv vars. The summary uploaded is always canonical JSON regardless of--format. Token issuance lives in the dashboard at/dashboard/settings/tokens. - New shared
uploadSummary({ summary, token, domainId, endpoint })helper incommands/upload.ts. The standalonepseolint upload <report>command now delegates to it after reading the report file.
- Inline upload after scan. New
- v0.5.5
- Track
@pseolint/core0.5.5: large pSEO sites now get stratified URL coverage across templates (matters when sample budget < total candidate URLs), andcontent/regurgitated-contentflags Google Places API regurgitation patterns. No CLI flag changes — both fire automatically.
- Track
- v0.5.4
- Track
@pseolint/core0.5.4: newcontent/translation-no-oprule fires automatically onpseolint scanwhen locale-prefixed siblings share identical content. No CLI flag changes.
- Track
- v0.5.3
- Track
@pseolint/core0.5.3: pulls in the engine grading rethink (classifier degeneration guard, blocker density floor) and the newforce.urlsaudit option used by hosted monitoring. No CLI surface changes — the new options are caller-controlled and not exposed as flags yet (see v0.5.4 for--upload-toand--watch).
- Track
- v0.5.0
- v0.5.0 (continued) —
pseolint orchestratesubcommand Wraps@pseolint/corev0.5.0's neworchestrate()API as a CLI command. ``bash export ANTHROPIC_API_KEY=sk-... pseolint orchestrate https://example.com \ --max-cost 3 \ --ndjson session.ndjson \ --manifest-out manifest.json`Live event stream to stdout with ANSI colors: tool calls, results, thoughts, per-step token + cost + duration, watchdog firings, budget warnings, session terminus. End-of-run summary: verdict + 4 category grades, page / template / domain patch counts,valid/totalpatches with the first 10 dropped patches surfaced. Three exit codes: 0 = completed + all patches valid · 1 = session didn't reach finish_audit · 2 = completed but some patches were rejected by validators (manifest still written). Options mirror the public API:--ai-provider,--ai-model,--ai-key,--max-cost <usd>,--max-tool-calls <n>,--max-wall-seconds <n>,--watchdog <n>(0 disables),--ndjson <path>,--manifest-out <path>,--quiet,--no-color`. - v0.5.0 — Change-driven monitoring CLI surface Tracks
@pseolint/corev0.5.0. New flags: --mode <monitoring|fresh>— explicit monitoring control.monitoringapplies the pre-fetch decision matrix (default when prior state exists);freshforces a full re-audit even when prior state is present.--age-floor-days <n>— minimum days since a URL's last fetch before monitoring forces a re-fetch regardless of other signals (default: 7).--sinceis now an alias for--mode=monitoring(back-compat). End-of-run summary line when monitoring is active: ``Monitoring: 47/4012 URLs re-scraped (recheck=23, lastmod=12, age=8, new=4), 3965 carried forward.`Whenfetched < intended, the line shows both ("X/Y (intended)"), surfacing URLs the matrix wanted to refetch but downstream filters dropped (robots, byte budget, content-type, 4xx). Filesystem-source bypass now logs a one-line warning when explicit--mode=monitoringor--sinceis requested against a local source. See spec:docs/superpowers/specs/2026-05-01-change-driven-monitoring-design.md`.
- v0.5.0 (continued) —
- v0.4.3
- v0.4.3 — classification-driven scoring, fixes credibility crisis Trigger: an external reviewer ran pseolint on his own site and didn't believe the verdict. Three-site dogfood (nextjs.org, wordpress.com, shopify.com) confirmed the issue — nextjs.org would score
concerning(60) on a meaningful sample, with 7 of 11 actionable findings being AEO-style "your marketing page doesn't read like a fact database." Citation category alone drove 25 risk points just from the AEO bucket maxing out. This release rewires scoring so the verdict is classification-aware: a docs site is judged by what matters for docs sites; a programmatic directory is judged by what matters for pSEO; a marketing site isn't penalized for being marketing-shaped. Changes: - New
SCORING_PROFILESmap keyed onSiteType. Each profile defines per-category weights + severity overrides + confidence overrides. Applied when classifier confidence ≥ 0.7; below that, conservativeuncleardefaults. - New
RULE_IMPACTSmap gives every rule its ownbaseImpact + perInstance × maxImpactcurve, replacing the global 4-tierSEVERITY_WEIGHTS. Spam findings amplify with cluster size; AEO findings stay capped low. - New
Confidencetype (high | medium | low | speculative) onRuleResult. Per-rule emit logic on 10 rules. Low-confidence findings carry a caveat in the message and contribute proportionally less to scoring. - New site classifier types:
docs(Docusaurus / Nextra / GitBook / VuePress shape) and improvedecommerce(Shopify / WooCommerce shape). Plus atryClassifyLocalizedMarketing()detector that prevents stripe.com / vercel.com / linear.app from being misclassified asprogrammatic-directorybecause of/[lang]/URL prefixes. - Re-tuned tech rule impacts:
tech/hreflang-consistencyis now a single base-impact finding regardless of how many language pairs break (one declaration breaks them all — count shouldn't compound).tech/canonical-consistencylowered to base 8, perInstance 1. - Formatters surface "Audited as <type> (NN% confidence)." prominently. Confidence caveats render after low-/speculative-confidence findings.
- Marketing copy on /, /tools, /rules, /symptoms, /limits clarifies scope: "pseolint audits programmatic-SEO + AI Overview readiness. Not a general SEO audit — for Core Web Vitals and broken links use Sitebulb, Screaming Frog, or Ahrefs." Dogfood results (post-change): ``
nextjs.org ready (14) → ready (15) no regression react.dev caution (23) → ready (15) improved stripe.com [aborted] → ready (9) correctly classifies localized wordpress.com caution (24) → ready (12) improved shopify.com concerning(58)→ caution(28) -30 risk; remaining findings real`Tests: 663/663 pass (was 646 in v0.4.2 — +17 new tests for classifier types, scoring profiles, confidence emission, formatter output). **Reasoning trail:** seedocs/superpowers/specs/2026-04-30-pseolint-scoring-credibility.md` for the full diagnosis + design rationale. - Updated dependencies
- @pseolint/core@0.4.3
- @pseolint/mcp@0.4.3
- v0.4.3 — classification-driven scoring, fixes credibility crisis Trigger: an external reviewer ran pseolint on his own site and didn't believe the verdict. Three-site dogfood (nextjs.org, wordpress.com, shopify.com) confirmed the issue — nextjs.org would score
- v0.4.2
- v0.4.2 — page-skip extensions, framework-aware web defaults, template bucketing, fixplan artifact @pseolint/core (0.4.1 → 0.4.2) Three new page-skip filters extending the v0.4.1 noindex / auth machinery:
skipBoilerplate?: boolean(defaultfalse) — skip cookie / legal / consent / imprint pages via title, H1, or URL pathname matching well-known compliance-page patterns (/privacy,/terms,/cookies,/gdpr,/ccpa,/impressum,/disclaimer,/accessibility,/do-not-sell, etc.). Single-signal trigger because patterns are anchored — a marketing page that mentions "privacy" in its body won't fire. NewdetectBoilerplatePage(page)exported from./page-filter.js.skipSearchPages?: boolean(defaultfalse) — skip pages with search-result URL hallmarks: query parameterq/query/search/s/keyword, or pathname starting with/search. Per Google's own guidance these should benoindex'd but many sites don't tag them.skipEmptyBody?: boolean(defaultfalse) — skip un-hydrated SPA shells: body text < 100 chars, script tags present, no substantive<noscript>fallback. The right fix is--render, not content rules.pageSkipReason()now returns"noindex" | "auth-detected" | Theaudit/skipped-by-policy` diagnostic surfaces all five reason categories with per-reason counts. Plus a refactor for output ergonomics:- New
bucketByTemplate(findings)helper at./formatters/bucket-findings.js. Console + markdown formatters now collapse findings sharing a template signature into one line (× 23 instances on /templates/[state]-llc-fees the legacy format. Site-wide / non-template buckets render as× 2 affected pages`. - New
formatFixplan(summary)formatter at./formatters/fixplan.js. Emits a markdown checklist of fixes ordered by effort (quick wins → moderate → structural → other), each item bucketed by template, with a Skipped section breaking down noindex / auth-detected counts and a footer wallclock estimate. Designed for paste-into-GitHub-issue use. pseolint CLI (0.4.1 → 0.4.2) - New flags:
--skip-boilerplate,--skip-search-pages,--skip-empty-bodymirror the correspondingAuditOptions. All off-by-default to preserve CLI back-compat. @pseolint/mcp (0.4.1 → 0.4.2) - Workspace dep bump to pick up the new
AuditOptionsfields. No tool surface changes; callers can pass the new flags via the MCP audit tool's options if they want them. Web app (0.0.4 → 0.0.5) The hosted form audit pipeline now adapts to the audited site's framework: - New
FRAMEWORK_IGNORE_PATTERNSmap keyed onnextjs | wordpress | idiomatic-but-non-marketing routes (e.g. WordPress/wp-includes/, Shopify/cart, Next/_next/data/, etc.) layer additively on top of the baseWEB_AUDIT_DEFAULT_IGNORE`. - New
detectFrameworkFromUrl(url, signal?)helper does a single preflight HEAD/GET of the source root, checksx-powered-by,x-vercel-id,x-shopify-*response headers + script-src body signatures, and returns the framework key. Wrapped in anAbortControllerwith a hard 5s timeout so a slow root doesn't block the audit. run-audit.tsruns framework detection in its own Inngest step (detect-framework) before the audit step. Failure / timeout falls through toresolveAuditIgnorePatterns(undefined)(base list only). Tests: 646/646 pass. core + cli + mcp + action all build clean.- Updated dependencies
- @pseolint/core@0.4.2
- @pseolint/mcp@0.4.2
- v0.4.1
- v0.4.1 — config UX fixes + page-skip policy @pseolint/core (0.4.0 → 0.4.1)
- New
respectNoindex?: boolean(defaulttrue) — pages explicitly markednoindex(via<meta name="robots">orX-Robots-Tagheader) are excluded from rule evaluation. The site owner already opted out of SEO indexing for them; auditing produces noise the reader can't act on. The two noindex-conflict rules (tech/canonical-noindex-conflict,tech/robots-noindex-conflict) andtech/hreflang-consistencystill receive noindex'd pages so they can flag accidental noindex'ing / inconsistent hreflang declarations. - New
skipDetectedAuth?: boolean(defaultfalse) — heuristic detection of login / signup / password-reset / verify-email pages via password-input density, page title pattern (brand-suffix-stripped), and H1 pattern. Requires 2+ signals for a positive verdict, keeping false-positives low on marketing pages with single-signal characteristics. - New
audit/skipped-by-policydiagnostic surfaces every URL skipped by the above policies insummary.diagnostics.auditFindings— the accidentally-noindex'd page now shows up as a visible skip line instead of being absent without explanation. - New
warnUnmatchedIgnore?: boolean(defaultfalse) — per-pattern warning for unmatched--ignorepatterns is now opt-in. The CLI sets it to true only when--ignorecame from the command line. Config-loaded patterns (e.g.pseolint.config.tswith broad safety patterns like**/api/**) no longer spam warnings when the patterns legitimately don't match a small site's surface. A consolidated `none of the N ignore patterns fires when ALL patterns miss, regardless of source. - New helpers exported from the entry point:
detectNoindex,detectAuthPage,pageSkipReasonfrom./page-filter.js. pseolint CLI (0.4.0 → 0.4.1) pseolint.config.tsfiles are now auto-loaded via cosmiconfig (jiti loader). Previously only.js/.cjs/.mjs/.jsonconfigs were picked up;.tsfiles silently fell through, forcing users to inline--ignorepatterns. Bothpseolint.config.tsand.mtsvariants are now in the searchPlaces list.- New flag
--no-respect-noindex— audit pages marked noindex anyway (useful when investigating an accidentally-noindex'd page). - New flag
--skip-detected-auth— opt into the heuristic auth-page skip. @pseolint/mcp (0.4.0 → 0.4.1) - Picks up the core changes via the workspace dep bump. No tool surface changes; auth-page detection / noindex respect are now available to callers that pass the relevant
AuditOptionsfields. Web app Public-form audits run throughapps/web/src/inngest/functions/run-audit.tsnow apply opinionated defaults: aWEB_AUDIT_DEFAULT_IGNOREURL pattern list (framework metadata + auth + admin + API + WordPress conventions) plusrespectNoindex: trueandskipDetectedAuth: true. End users running audits via the public form no longer see noise from utility routes. Tests: 604 / 604 pass. Engine + CLI + MCP + action all typecheck clean. - Updated dependencies
- @pseolint/core@0.4.1
- @pseolint/mcp@0.4.1
- v0.4.0
- v0.4.0 — engine redesign Breaking-change release coordinated across all three packages. @pseolint/core (0.3.3 → 0.4.0)
- Replaced numeric
score(0–100, lower = better) with the verdict ladderready(≤20) |caution(≤40) |concerning(≤60) |critical(>60). The numeric value is still emitted asriskfor sorting and CI gating. - Consolidated 8 source-tree categories into 4 scoring super-categories:
integrity(spam + content + cannibal, weight 0.50),discoverability(links + tech, 0.20),citation(aeo + schema, 0.25),data(0.05). - Dropped 8 noisy rules (cannibal/title-overlap, cannibal/keyword-collision, content/heading-uniqueness, links/hub-pages, plus four others). Total rule count is now 32.
- New
AuditSummaryshape:schemaVersion: "2026-04-v0.4",verdict,risk,headline,categories[].grade/issues, `issues.{blockers, - New
siteClassificationfield (§4.11): pre-flight URL-pattern + sitemap - framework heuristics infersmall-marketing | blog | programmatic- (spam/template-coverage,spam/template-diversity,spam/entity-swap,cannibal/url-pattern) on small sites unless the caller passesstrict: true`. - New
AuditOptions.strictflag to bypass classification-driven rule suppression. pseolint CLI (0.3.2 → 0.4.0) - New flags:
--ci-threshold <severity>(replaces numeric--thresholdfor CI gating),--explain(full bucketed finding view),--strict(bypass pSEO-only rule suppression). --thresholddeprecated with a runtime warning — still functional for one minor.- New
pseolint diff <baseline> <current>subcommand: verdict-rank deltas + fixed/regressed/new findings between two AuditSummary JSON reports. Tolerates mixed v0.3 / v0.4 JSON. - Console formatter rewritten around the verdict ladder. JSON, HTML, and Markdown formatters all consume the new shape.
--watchflag reserved (planned for v0.4.1, not implemented). @pseolint/mcp (0.3.1 → 0.4.0)- All three tools (
audit_site,explain_score,check_page_technical) migrated to the v0.4AuditSummaryshape:summary.score→summary.risk,summary.findings→ flattened fromissues.*,summary.categoryScores→summary.categories. explain_scorenow surfacessiteClassification(type, confidence, suppressed-rule count) when present.CROSS_PAGE_RULESset updated to remove the 4 dropped cross-page rules. Migration Pre-existing v0.3 JSON reports remain readable —/r/[slug]and/r/comparein the web app detectsummary.schemaVersionand route through legacy renderers. New CI gates should switch from--threshold 40(numeric) to--ci-threshold concerning(semantic). Spec:docs/superpowers/specs/2026-04-29-pseolint-v0.4-engine-redesign.md.- Updated dependencies
- @pseolint/core@0.4.0
- @pseolint/mcp@0.4.0
- v0.3.1
- v0.3.3: safeMode preset, CLI safety flags, MCP safe-by-default Consolidates the safety work from render-analytics blocking (v0.3.1), SSRF / AbortSignal / robots-honor (v0.3.2), and the safeMode preset (v0.3.3) for the CLI and MCP packages, which skipped the prior intermediate releases on npm. ## @pseolint/core → 0.3.3 Incremental over 0.3.2 (which shipped SSRF guard, AbortSignal support, and robots.txt honour for our own crawler):
safeMode: "saas" | "cli"preset onAuditOptions— flipsguardSsrf,respectRobotsTxt,followRedirects,maxCrawlDiscovered, andmaxFetchBytesdefaults in one knob. Individual option overrides still win.safeFetch(url, options?)— SSRF-safe fetch for non-audit use cases (webhook URL verification, favicon lookups, etc.). WrapscachedFetchwithvalidateTargetHostbaked in.maxCrawlDiscovered— hard ceiling on link-discovery fan-out so a malicious site with many self-links can't extend crawl up to the byte budget. Default 5000 (2000 undersafeMode: "saas").followRedirects: falseoption — returns 3xx as-is so security- sensitive audits can report redirects without following them. New exports:safeFetch,SafeModetype. ## pseolint (CLI) → 0.3.1 First CLI release since 0.3.0; bundles the v0.3.1 / v0.3.2 / v0.3.3 CLI-facing work:- Render-mode analytics blocking flags:
--analytics <block|allow|allow-first-party>,--block-host <host>(repeatable). Prevents rendered audits from firing GA / Plausible / PostHog / Mixpanel / Hotjar / Sentry beacons on every page. --safe-mode <saas|cli>— applies the core preset.--no-respect-robots— audit sitemap URLs even when the target's robots.txt Disallow's them (use for your own staging sites).--no-follow-redirects— report 3xx as-is.ctrl-Chandler — SIGINT triggers a clean abort viaAbortController; in-flight fetches cancel cleanly instead of the process being hard- killed mid-read. Secondctrl-Cwithin ~1 s forces exit.pseolint.config.tsschema extended forsafeMode,respectRobotsTxt,followRedirects,guardSsrf,maxCrawlDiscovered. ## @pseolint/mcp → 0.3.1 First MCP release since 0.3.0; picks up all of v0.3.1 / v0.3.2 / v0.3.3 via the core update, plus one meaningful default flip:- All three tool handlers (
audit_site,explain_score,check_page_technical) now defaultsafeMode: "saas". AI assistants running in end-user environments can't be tricked into scanning cloud metadata / localhost / RFC1918 networks via a malicious URL argument.guardSsrf,respectRobotsTxt, tighter caps all flip on. ## Not changed@pseolint/action— runs in GitHub-public-network runners, auto- propagates new core features through its existingAuditSummaryrendering. No separate bump. ## Test state 528 / 528 tests pass. Typecheck clean across core / cli / mcp / action. - Updated dependencies
- @pseolint/core@0.3.3
- @pseolint/mcp@0.3.1
- v0.3.0
- feat: add AEO rule category with 8 rules for AI Overview readiness Introduces
aeo/*— a new scored rule category focused on Answer Engine Optimization. Brings the total to 42 rules across 8 categories (7 scored data/*). While SpamBrain rules protect against Google penalties, AEO rules audit whether pSEO pages are structured to be cited in AI Overviews (ChatGPT, Perplexity, Gemini, Claude, Google AI Overviews). Sites invisible to AI Overviews lose ~68% of traffic vs ~12% for cited sites. New rules:aeo/llms-txt— checks for/llms.txtat the origin and validates the minimal shape (H1 title, at least one##section, markdown link entries).aeo/crawler-access— parsesrobots.txtper user-agent and flags blocked AI crawlers (GPTBot, ChatGPT-User, ClaudeBot, PerplexityBot, Bytespider, Google-Extended, CCBot, Applebot-Extended). Warns per blocked crawler, errors when all are blocked.aeo/freshness-signals— checks each page for a dateModified signal (JSON-LD,article:modified_time, visible "Last updated" text). Warns when absent, emits info when older than 180 days.aeo/faq-coverage— detects FAQ-style content (question-phrased H2s or URL patterns like/how-to-*,/what-is-*,*-faq) that lacks FAQPage or HowTo JSON-LD.aeo/answer-first— scores the first paragraph after the H1 for extractable-answer quality: concrete facts, named entities, complete sentence, boilerplate detection, and template-opener detection via entity masking across the corpus.aeo/citable-facts— counts unique, entity-specific citable facts per page (dollar amounts, percentages, timeframes, dates, Form numbers). Filters out template facts shared across the majority of masked pages.aeo/non-replicable-value— detects pages that are pure informational text with no interactive element, downloadable asset, or gated content — pages AI can fully summarize without sending a click.aeo/content-modularity— splits pages by H2/H3 and flags sections that aren't independently extractable (cross-references like "as mentioned above", vague headings like "More Info", paragraphs over 200 words). ### Scoring Category weights re-balanced.aeojoins the composite score at 15%: before (0.2.x): spam 0.40 / content 0.25 / links 0.15 / tech 0.10 / schema 0.05 / cannibal 0.05 after (0.3.0): spam 0.35 / content 0.20 / aeo 0.15 / links 0.12 / tech 0.08 / schema 0.05 / cannibal 0.05AuditSummary.categoryScoresnow includes anaeofield. ### Config New flat threshold keys inAuditOptions.rules(all optional, sensible defaults):answerFirstMaxWords(default 100) — opener length cap foraeo/answer-firstcitableFactsMin(default 3) — below this a page errorscitableFactsTarget(default 8) — at or above this a page passesfreshnessMaxStaleDays(default 180) — age at whichdateModifiedis flagged stalemodularityMaxParagraphWords(default 200) —aeo/content-modularitymodularityMinSelfContainedRatio(default 0.7) —aeo/content-modularityfaqMinQuestionHeadings(default 2) —aeo/faq-coveragePage-groupoverridesstill apply normally for severity tuning. ### AEO sub-score and console sectioncategoryScores.aeois the sub-score (raw 0–100 damage — lower is better) and has its own label scheme distinct from the SpamBrain Risk label:- 0–20 AI-Ready — pages structured for citation
- 21–40 Partial — some citable, others vulnerable
- 41–60 Vulnerable — most pages will be summarized away without clicks
- 61–80 Invisible — pages offer nothing AI can't synthesize itself
- 81–100 Ghost — blocked from AI + no citable structure; traffic will crater
aeoScoreLabel(score)is exported from@pseolint/coreso downstream formatters can surface the label. The console formatter renders a dedicated AEO: AI Overview Readiness section between group scores and AI triage when anyaeo/*findings are present. ### AI triage Prompt bumped to1.1.0(additive). The system prompt now explicitly distinguishes two threat families — SpamBrain penalty risk (spam/cannibal/ content/data/tech/schema/links) and AI Overview invisibility (aeo/\*) — and asks for at least one root cause from each when both families are present. A newfindingCountByCategoryfield in the prompt payload gives the model per-category totals for weighting. - Updated dependencies [01627a8]
- Updated dependencies [bfcccc0]
- @pseolint/core@0.3.0
- @pseolint/mcp@0.3.0
- feat: add AEO rule category with 8 rules for AI Overview readiness Introduces
- v0.2.3
- fix(cli): run when the installed binary is a symlink (
npm link, Windows global shim) The direct-run guard comparedimport.meta.urlto a URL built fromprocess.argv[1], which diverges when the real file lives on another path (symlink/junction). The process exited 0 without running any command, including--versionand audits. - fix(publish): rewrite
workspace:*to real semver in published dependencies The 0.2.1 tarballs shipped withworkspace:*in theirdependencieslists, which npm cannot resolve. Anynpx pseolintornpm install pseolintsilently failed. Republishing viachangeset publishrewrites the workspace protocol to the real version range. - Updated dependencies
- @pseolint/core@0.2.2
- @pseolint/mcp@0.2.2
- fix(cli): run when the installed binary is a symlink (
@pseolint/mcp
- v0.7.4
- v0.7.4 — pluggable cache backend + internal refinements.
- Pluggable HTTP cache backend (`@pseolint/core`). The audit cache's storage now sits behind a
CacheBackend { get, set }interface. The defaultFilesystemCacheBackendpreserves the existing dir-based behaviour byte-for-byte (the CLI is unchanged), so a host can supply its own store — e.g. an R2-backed cache on ephemeral-filesystem serverless — viaAuditOptions.cache.backend. New exports:CacheBackend,FilesystemCacheBackend,AnyCacheEntry,RedirectPointerEntry,CACHE_ENTRY_SCHEMA_VERSION. All fetch revalidation (ETag/304), redirect-pointer, and negative-cache logic stays backend-agnostic, and every backend call is fail-safe — a backend error degrades to a cache miss (read) or a logged no-op (write), never aborting an audit. - `./rules/scope` subpath export (`@pseolint/core`). A dependency-light entry exposing
SCORED_RULE_COUNT,RULE_SCOPE, andisRuleAllowedInDiffwithout pulling the full engine barrel — for consumers (browser/edge bundles) that only need rule-scope metadata. - MCP JSON char-cap is now read per call (`@pseolint/mcp`).
PSEOLINT_MCP_JSON_CHAR_CAPis resolved at request time instead of frozen at module load, so the oversized-payload envelope threshold can be tuned/tested without a restart. No protocol change. - Updated dependencies
- @pseolint/core@0.7.4
- v0.7.3
- v0.7.3 — render-aware checks, AI content-effort, and bring-your-own inputs. Verdict moderators never change the raw
risknumber, so CI gates keyed off--ci-thresholdstay stable. - Bring-your-own authority. New
--authority-score <0-100>CLI flag andauthorityScoreconfig key, alongside the existing MCPauthorityScoreparam and a per-domain setting in the Pro web dashboard.>= 80shifts the verdict one tier lenient,<= 30one tier stricter. The engine stays authority-blind by design. - AI content-effort signal. New
--content-effort(+--content-effort-model) CLI flag,AuditOptions.contentEffort, and an optional MCPcontentEffortparam. An LLM judges a 0-100 content originality/effort score from sampled page text (≤10 pages, content-hash cached) that moderates the verdict ±1 tier. Opt-in, needsANTHROPIC_API_KEY, no-ops safely without one (~$0.003/page on claude-sonnet-4-6). Resolved score is written tosummary.contentEffort.score. Runs automatically for Pro audits in the web app. - Render-aware crawl checks.
--render(Playwright, Node-only) now feeds two render-diff rules:tech/csr-bailoutflags pages whose substantive content/interactivity exists only after client-side JS (invisible to crawlers + the first indexing pass), andtech/soft-404probes one synthetic nonexistent URL per template cluster (an HTTP 200 means the directory will index unbounded junk). Both no-op without--render/ outside programmatic directories. - MCP rule knowledge as resources. The MCP server now exposes the pSEO rule catalog as resources (
pseolint://rulesindex +pseolint://rules/<ruleId>per-rule Markdown) so assistants can explain findings without guessing. The open knowledge bundle is also served at/okfand linked fromllms.txt. - Updated dependencies [3361782]
- @pseolint/core@0.7.3
- v0.7.3 — render-aware checks, AI content-effort, and bring-your-own inputs. Verdict moderators never change the raw
- v0.7.2
- fix(core): schema/consistency no longer false-positives on pages with multiple JSON-LD blocks. The v0.7.1 per-cluster rewrite compared the UNION of @types across a cluster, so a template where every page legitimately emits several blocks (e.g. TechArticle + FAQPage + Organization) read as "mixed types" and fired on every cluster (6 FPs on pseolint.dev's own audit). Now it compares each page's @type SET signature and fires only when pages in the same template cluster genuinely disagree.
- fix(core): v0.7.2 rule-design batch — graded thresholds + presence-quality. Follow-up to the v0.7.1 FP-elimination batch, addressing the two deferred root causes (C: binary/absolute thresholds, D: presence-not-quality). Verified against the 24-fixture calibration corpus: zero new false positives vs the prior metrics, and the crawl-size verdict flips are gone. C — binary-threshold redesigns:
- spam/boilerplate-ratio: continuous document-frequency weighting replaces the floor(N\*0.8)+1 skeleton cliff; 2-band severity. Verdict no longer flips when one more sibling page is crawled.
- spam/template-diversity: log-bucketed coarsening of the structureSignature so single-template sites with minor chrome variation are no longer read as diverse (the exact-count signature shared with near-duplicate/doorway-pattern is untouched); confidence band.
- content/value-add: continuous categoriesPresent/4 E-E-A-T sub-score replacing the 3-step hard-threshold value; 2-band severity (drops "critical").
- content/wikipedia-paraphrase: min-length guard + threshold 0.40→0.55 above the legal/medical topic-overlap baseline; advisory language, stays low-confidence. D — presence-quality (validate the value, not just its presence):
- schema/required-fields: empty arrays / whitespace / nameless author objects count as missing.
- schema/json-ld-valid: @type accepts string OR all-string array (["Article","NewsArticle"] no longer false-positives).
- tech/og-completeness: whitespace values count as missing; severity graded (title/description warning, image-only info).
- content/eeat-signals: transparency signal reads contentText not raw html; about-link must be same-host.
- Updated dependencies [cc24997]
- Updated dependencies [3c9cb0d]
- @pseolint/core@0.7.2
- v0.7.1
- v0.7.1 — rule false-positive elimination batch (post unique-value design review). Stops the engine flagging healthy sites without weakening real detection. Each fix is TDD'd and validated against the reputable-pSEO fixtures.
- links/orphan-pages, links/cluster-connectivity — suppressed on sampled crawls (the linking/target page is often un-fetched; reliable only on a full crawl).
- tech/canonical-consistency — collapse "canonicalizes outside crawl scope" to one site-level note when all pages point at the same alternate host (staging/ preview/localhost), instead of one finding per page; dedup HTTP-vs-HTML.
- tech/sitemap-completeness — normalize sitemap URLs before the set-diff (kills trailing-slash/query false "missing"); demote the missing aggregate to warning.
- schema/consistency — flag @type variance per template cluster (structureSignature), not site-wide (was a guaranteed FP on any multi-template site).
- aeo/crawler-access — honor robots
Allowdirectives per RFC 9309 (allow-all no longer reported as fully blocked). - Severity/confidence bands — error/critical demoted to warning on weak or forecast signals: thin-content medium band, summary-bait, translation-no-op, entity-swap (low mask coverage), soft-404 (OR-weighted confidence model). Note: bundled as a patch (0.x) despite a behavior/scoring shift and the
rules.uniqueValueMinWords→rules.uniqueValueDensityconfig rename. - Updated dependencies [d9797e4]
- Updated dependencies [ce06ef7]
- @pseolint/core@0.7.1
- v0.7.0
- v0.7.0 — Calibration & authority foundations
- Two-sided calibration harness + score-vs-outcome instrument. New
calibrationMetrics()reports how well the risk score tracks real winning/penalized outcomes against a labeled corpus — threshold-free AUC, class-separation gap, per-band empirical penalty rate, and the over-flag / recall-leak confusion-zone sites. Adetectabilitycorpus field separates the engine's addressable ceiling from structurally-undetectable (off-page) cases. - Corpus-derived entity auto-masking (
deriveEntityPatterns): clusters pages by URL template and masks tokens that vary across siblings, lifting policy-violating recall (44% → 56% on the calibration corpus) and fixing the reputable-vs-spam risk inversion. - Domain-authority moderation scaffolding: a pluggable
AuthorityProvider(CompositeAuthorityProvidermax-combine,OpenPageRankProvider,CommonCrawlProvider) feeds the existing verdict-shift. Fail-safe no-op until an authority source is configured — no behaviour change by default. - Updated dependencies [ba1c6ca]
- Updated dependencies
- @pseolint/core@0.7.0
- v0.6.5
- surface partial-coverage (
truncated) audits in the MCP structured output The core engine flushes atruncated: truereport (withtruncatedReason) when its backpressure watchdog aborts a crawl mid-flight on a degraded origin — counts, risk, and the verdict are then lower bounds. The MCP server hand-builds thestructuredContentpayload per tool and omittedtruncated, so an AI client saw a partial audit as if it were complete. It also omittedschemaVersion, leaving programmatic consumers unable to branch on the output contract. - All three audit tools (
pseolint_audit_site,pseolint_explain_score,pseolint_check_page_technical) now emitschemaVersionand, on a truncated run,truncated+truncatedReasoninstructuredContent. The matching optional fields were added to each tool'soutputSchemawith descriptions so clients know coverage is partial and to treat verdict/risk/pageCount as lower bounds. - The human-readable text of
pseolint_audit_siteandpseolint_explain_scorenow prepends a "⚠ Partial audit (origin degraded)" warning line when the run was truncated, so chat-UI users see the caveat without inspecting structured data. - Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- @pseolint/core@0.6.5
- surface partial-coverage (
- v0.6.4
- Add MCP Registry support: a schema-valid
server.json(namespaceio.github.ouranos-labs/pseolint) plus themcpNameownership field inpackage.json, so the server can be published toregistry.modelcontextprotocol.iofor client discovery. - Align the MCP server with 2026 MCP guidelines. Breaking: all tools are now namespaced with a
pseolint_prefix to avoid collisions with other MCP servers loaded alongside this one: audit_site→pseolint_audit_siteexplain_score→pseolint_explain_scorecheck_page_technical→pseolint_check_page_technicalorchestrate_audit→pseolint_orchestrate_auditUpdate any saved prompts or client configs that reference the old names. Non-breaking improvements:- Every tool now declares an
outputSchemaand returnsstructuredContent(modern SDK pattern), so clients get validated structured data alongside the human-readable text.pseolint_audit_siteembeds up to 100 severity-ordered findings, withfindingCountfor the true total andfindingsTruncatedwhen the array was shortened; structuredcategoriesare the four scored buckets only (sourced from core'sSCORED_CATEGORY_KEYS, excluding the weight-0auditdiagnostics bucket). isErroris now reserved for genuine execution failures. A site exceeding the risk threshold (or an orchestrate run that stops early) is a _successful_ call: readpassed/verdict/reason/completedinstead ofisError. Previously these returnedisError: true.- Output is size-bounded without ever emitting malformed data: human-readable console/summary text is character-capped with a CLI pointer, and oversized
format:"json"payloads collapse to a compact valid-JSON envelope (truncation marker + findings-free summary + CLI pointer) rather than being string-sliced into unparseable JSON or left unbounded. pseolint_audit_siteandpseolint_explain_scorenow expose theauthorityScore(bring-your-own domain authority, 0-100) andsampleSeed(deterministic stratified sampling) parameters that the engine already supported — previously documented in the README but not wired up.- All numeric inputs are range-constrained with descriptive errors. The orchestrator budget ceilings are explicit MCP-surface safety bounds (core itself imposes none): max $50 / 500 tool calls / 900s, well above the conservative MCP defaults of $2 / 60 / 180.
- Added
idempotentHintannotations:truefor the read-only audit tools,falseforpseolint_orchestrate_audit(LLM-driven, non-deterministic). - Size caps are env-tunable for parity with the existing page-sample cap:
PSEOLINT_MCP_CHAR_LIMIT,PSEOLINT_MCP_JSON_CHAR_CAP,PSEOLINT_MCP_FINDINGS_CAP(alongsidePSEOLINT_MCP_SAMPLE_CAP). - Added a test suite (56 tests, ~82% branch / 98% line on the server) covering two layers: contract tests that drive the real server through the MCP SDK client over an in-memory transport with the engine mocked (tool registration, schema-validated
structuredContent,isErrorsemantics, output size bounding incl. boundary cases, input-validation, option forwarding, env knobs), plus a true end-to-end suite that runs the real engine (auditSource/formatJson/formatConsole) against a static filesystem fixture to catch core↔MCP drift. - Updated dependencies
- @pseolint/core@0.6.4
- Add MCP Registry support: a schema-valid
- v0.6.2
- Track
@pseolint/core0.6.2: Wikipedia bloom filter inlined as base64 (production hotfix for Vercel serverless ENOENT errors).
- Track
- v0.6.1
- Track
@pseolint/core0.6.1: validates v0.6 cutover code path against real programmatic-directory sites in the calibration corpus. MCP audit-tool output now reflects template-aware verdicts on Jasper-shaped + Airbyte-shaped sites correctly. MCP surface unchanged.
- Track
- v0.6.0
- Track
@pseolint/core0.6.0:siteVerdictFromTemplateswired as default verdict source. MCPaudittool output now reflects the template-aware verdict for multi-template sites.templatesfield onAuditResultis stable public API. MCP surface otherwise unchanged.
- Track
- v0.5.15
- Track
@pseolint/core0.5.15: filesystem-mode_manifest.jsonsupport enables deterministic fixture-based calibration. MCP surface unchanged.
- Track
- v0.5.14
- Track
@pseolint/core0.5.14: value-add composite now 7 signals.
- Track
- v0.5.13
- Track
@pseolint/core0.5.13: Wise calibration fix + newcontent/wikipedia-paraphrasestandalone signal in audit-tool output.
- Track
- v0.5.12
- Track
@pseolint/core0.5.12 (calibration stability via pinned URL sampling). MCP surface unchanged.
- Track
- v0.5.11
- Track
@pseolint/core0.5.11: newcontent/common-phrase-reuserule surfaces in audit-tool output for pages with ≥3 pSEO clichés; value-add composite now aggregates 6 signals.
- Track
- v0.5.10
- Track
@pseolint/core0.5.10 (v0.6 phase 3 — CLI + MCP). Theaudit_sitetool description now documents thetemplatesarray that appears in the JSON output when ≥2 URL templates are detected: each entry carriessignature,verdict,risk,categories,auditedUrls,variance.uniformityScore, andvariance.topDriver. Theexplain_scoretool's text output now includes a "Per-template breakdown" section when templates are present. Both are additive — existing consumers readingfindingsare unaffected.
- Track
- v0.5.9
- Track
@pseolint/core0.5.9: v0.6 phase 1 —templatesfield now appears in audit-tool output when the site has ≥2 detected templates. Additive; existing consumers unaffected.
- Track
- v0.5.8
- Track
@pseolint/core0.5.8: newcontent/value-addcomposite rule shows up in audit-tool output for low-value-add pages.
- Track
- v0.5.7
- Track
@pseolint/core0.5.7: cheerio-based DOM traversal incontent/regurgitated-contentrule (security + correctness); bestfirenze regression test added.
- Track
- v0.5.6
- Version bump to align with CLI 0.5.6 (CLI inline upload). MCP surface unchanged.
- v0.5.5
- Track
@pseolint/core0.5.5: stratified URL sampling +content/regurgitated-contentGoogle Places regurgitation detection. MCP audit-tool output reflects both automatically.
- Track
- v0.5.4
- Track
@pseolint/core0.5.4: newcontent/translation-no-oprule surfaces in the audit-tool output for sites with locale-prefixed sibling pages.
- Track
- v0.5.3
- Track
@pseolint/core0.5.3: engine grading rethink (classifier degeneration guard + blocker density floor + verdict/grade alignment). MCP tool surface unchanged.
- Track
- v0.5.0
- v0.5.0 — AI orchestrator MCP tool + tracks
@pseolint/corev0.5.0 New tool: `orchestrate_audit` — exposes the AI-orchestrated auditor (added in@pseolint/corev0.5.0) to MCP hosts (Claude Desktop, Cursor, Windsurf, Claude Code). Unlikeaudit_sitewhich lists rule findings,orchestrate_auditdrives an LLM through 25 deterministic tools to produce a fix manifest — concrete copy-paste patches (rewritten H1s, JSON-LD blocks, robots.txt diffs, internal-link suggestions) validated against deterministic schemas before the result is returned. Conservative defaults for MCP-invoked sessions: $2 / 60 tool calls / 180 seconds wall (vs the CLI's $5 / 100 / 300). Can be raised by the caller if the user explicitly asks for a deeper run. Two output modes:summary(terse text suitable for chat UI with verdict + categories + top-3 patches per bucket) andjson(full manifest + validation report + structured diff payload). Cost transparency: each invocation reports actual USD spend alongside the manifest summary. Patches that fail deterministic validators are dropped and surfaced separately so the LLM-host conversation stays grounded in what actually shipped vs what was proposed. Tracks@pseolint/corev0.5.0 — also pulls in change-driven monitoring (audit_site monitoring runs are ~95% cheaper on sites with sitemap<lastmod>).
- v0.5.0 — AI orchestrator MCP tool + tracks
- v0.4.3
- v0.4.3 — classification-driven scoring, fixes credibility crisis Trigger: an external reviewer ran pseolint on his own site and didn't believe the verdict. Three-site dogfood (nextjs.org, wordpress.com, shopify.com) confirmed the issue — nextjs.org would score
concerning(60) on a meaningful sample, with 7 of 11 actionable findings being AEO-style "your marketing page doesn't read like a fact database." Citation category alone drove 25 risk points just from the AEO bucket maxing out. This release rewires scoring so the verdict is classification-aware: a docs site is judged by what matters for docs sites; a programmatic directory is judged by what matters for pSEO; a marketing site isn't penalized for being marketing-shaped. Changes: - New
SCORING_PROFILESmap keyed onSiteType. Each profile defines per-category weights + severity overrides + confidence overrides. Applied when classifier confidence ≥ 0.7; below that, conservativeuncleardefaults. - New
RULE_IMPACTSmap gives every rule its ownbaseImpact + perInstance × maxImpactcurve, replacing the global 4-tierSEVERITY_WEIGHTS. Spam findings amplify with cluster size; AEO findings stay capped low. - New
Confidencetype (high | medium | low | speculative) onRuleResult. Per-rule emit logic on 10 rules. Low-confidence findings carry a caveat in the message and contribute proportionally less to scoring. - New site classifier types:
docs(Docusaurus / Nextra / GitBook / VuePress shape) and improvedecommerce(Shopify / WooCommerce shape). Plus atryClassifyLocalizedMarketing()detector that prevents stripe.com / vercel.com / linear.app from being misclassified asprogrammatic-directorybecause of/[lang]/URL prefixes. - Re-tuned tech rule impacts:
tech/hreflang-consistencyis now a single base-impact finding regardless of how many language pairs break (one declaration breaks them all — count shouldn't compound).tech/canonical-consistencylowered to base 8, perInstance 1. - Formatters surface "Audited as <type> (NN% confidence)." prominently. Confidence caveats render after low-/speculative-confidence findings.
- Marketing copy on /, /tools, /rules, /symptoms, /limits clarifies scope: "pseolint audits programmatic-SEO + AI Overview readiness. Not a general SEO audit — for Core Web Vitals and broken links use Sitebulb, Screaming Frog, or Ahrefs." Dogfood results (post-change): ``
nextjs.org ready (14) → ready (15) no regression react.dev caution (23) → ready (15) improved stripe.com [aborted] → ready (9) correctly classifies localized wordpress.com caution (24) → ready (12) improved shopify.com concerning(58)→ caution(28) -30 risk; remaining findings real`Tests: 663/663 pass (was 646 in v0.4.2 — +17 new tests for classifier types, scoring profiles, confidence emission, formatter output). **Reasoning trail:** seedocs/superpowers/specs/2026-04-30-pseolint-scoring-credibility.md` for the full diagnosis + design rationale. - Updated dependencies
- @pseolint/core@0.4.3
- v0.4.3 — classification-driven scoring, fixes credibility crisis Trigger: an external reviewer ran pseolint on his own site and didn't believe the verdict. Three-site dogfood (nextjs.org, wordpress.com, shopify.com) confirmed the issue — nextjs.org would score
- v0.4.2
- v0.4.2 — page-skip extensions, framework-aware web defaults, template bucketing, fixplan artifact @pseolint/core (0.4.1 → 0.4.2) Three new page-skip filters extending the v0.4.1 noindex / auth machinery:
skipBoilerplate?: boolean(defaultfalse) — skip cookie / legal / consent / imprint pages via title, H1, or URL pathname matching well-known compliance-page patterns (/privacy,/terms,/cookies,/gdpr,/ccpa,/impressum,/disclaimer,/accessibility,/do-not-sell, etc.). Single-signal trigger because patterns are anchored — a marketing page that mentions "privacy" in its body won't fire. NewdetectBoilerplatePage(page)exported from./page-filter.js.skipSearchPages?: boolean(defaultfalse) — skip pages with search-result URL hallmarks: query parameterq/query/search/s/keyword, or pathname starting with/search. Per Google's own guidance these should benoindex'd but many sites don't tag them.skipEmptyBody?: boolean(defaultfalse) — skip un-hydrated SPA shells: body text < 100 chars, script tags present, no substantive<noscript>fallback. The right fix is--render, not content rules.pageSkipReason()now returns"noindex" | "auth-detected" | Theaudit/skipped-by-policy` diagnostic surfaces all five reason categories with per-reason counts. Plus a refactor for output ergonomics:- New
bucketByTemplate(findings)helper at./formatters/bucket-findings.js. Console + markdown formatters now collapse findings sharing a template signature into one line (× 23 instances on /templates/[state]-llc-fees the legacy format. Site-wide / non-template buckets render as× 2 affected pages`. - New
formatFixplan(summary)formatter at./formatters/fixplan.js. Emits a markdown checklist of fixes ordered by effort (quick wins → moderate → structural → other), each item bucketed by template, with a Skipped section breaking down noindex / auth-detected counts and a footer wallclock estimate. Designed for paste-into-GitHub-issue use. pseolint CLI (0.4.1 → 0.4.2) - New flags:
--skip-boilerplate,--skip-search-pages,--skip-empty-bodymirror the correspondingAuditOptions. All off-by-default to preserve CLI back-compat. @pseolint/mcp (0.4.1 → 0.4.2) - Workspace dep bump to pick up the new
AuditOptionsfields. No tool surface changes; callers can pass the new flags via the MCP audit tool's options if they want them. Web app (0.0.4 → 0.0.5) The hosted form audit pipeline now adapts to the audited site's framework: - New
FRAMEWORK_IGNORE_PATTERNSmap keyed onnextjs | wordpress | idiomatic-but-non-marketing routes (e.g. WordPress/wp-includes/, Shopify/cart, Next/_next/data/, etc.) layer additively on top of the baseWEB_AUDIT_DEFAULT_IGNORE`. - New
detectFrameworkFromUrl(url, signal?)helper does a single preflight HEAD/GET of the source root, checksx-powered-by,x-vercel-id,x-shopify-*response headers + script-src body signatures, and returns the framework key. Wrapped in anAbortControllerwith a hard 5s timeout so a slow root doesn't block the audit. run-audit.tsruns framework detection in its own Inngest step (detect-framework) before the audit step. Failure / timeout falls through toresolveAuditIgnorePatterns(undefined)(base list only). Tests: 646/646 pass. core + cli + mcp + action all build clean.- Updated dependencies
- @pseolint/core@0.4.2
- v0.4.1
- v0.4.1 — config UX fixes + page-skip policy @pseolint/core (0.4.0 → 0.4.1)
- New
respectNoindex?: boolean(defaulttrue) — pages explicitly markednoindex(via<meta name="robots">orX-Robots-Tagheader) are excluded from rule evaluation. The site owner already opted out of SEO indexing for them; auditing produces noise the reader can't act on. The two noindex-conflict rules (tech/canonical-noindex-conflict,tech/robots-noindex-conflict) andtech/hreflang-consistencystill receive noindex'd pages so they can flag accidental noindex'ing / inconsistent hreflang declarations. - New
skipDetectedAuth?: boolean(defaultfalse) — heuristic detection of login / signup / password-reset / verify-email pages via password-input density, page title pattern (brand-suffix-stripped), and H1 pattern. Requires 2+ signals for a positive verdict, keeping false-positives low on marketing pages with single-signal characteristics. - New
audit/skipped-by-policydiagnostic surfaces every URL skipped by the above policies insummary.diagnostics.auditFindings— the accidentally-noindex'd page now shows up as a visible skip line instead of being absent without explanation. - New
warnUnmatchedIgnore?: boolean(defaultfalse) — per-pattern warning for unmatched--ignorepatterns is now opt-in. The CLI sets it to true only when--ignorecame from the command line. Config-loaded patterns (e.g.pseolint.config.tswith broad safety patterns like**/api/**) no longer spam warnings when the patterns legitimately don't match a small site's surface. A consolidated `none of the N ignore patterns fires when ALL patterns miss, regardless of source. - New helpers exported from the entry point:
detectNoindex,detectAuthPage,pageSkipReasonfrom./page-filter.js. pseolint CLI (0.4.0 → 0.4.1) pseolint.config.tsfiles are now auto-loaded via cosmiconfig (jiti loader). Previously only.js/.cjs/.mjs/.jsonconfigs were picked up;.tsfiles silently fell through, forcing users to inline--ignorepatterns. Bothpseolint.config.tsand.mtsvariants are now in the searchPlaces list.- New flag
--no-respect-noindex— audit pages marked noindex anyway (useful when investigating an accidentally-noindex'd page). - New flag
--skip-detected-auth— opt into the heuristic auth-page skip. @pseolint/mcp (0.4.0 → 0.4.1) - Picks up the core changes via the workspace dep bump. No tool surface changes; auth-page detection / noindex respect are now available to callers that pass the relevant
AuditOptionsfields. Web app Public-form audits run throughapps/web/src/inngest/functions/run-audit.tsnow apply opinionated defaults: aWEB_AUDIT_DEFAULT_IGNOREURL pattern list (framework metadata + auth + admin + API + WordPress conventions) plusrespectNoindex: trueandskipDetectedAuth: true. End users running audits via the public form no longer see noise from utility routes. Tests: 604 / 604 pass. Engine + CLI + MCP + action all typecheck clean. - Updated dependencies
- @pseolint/core@0.4.1
- v0.4.0
- v0.4.0 — engine redesign Breaking-change release coordinated across all three packages. @pseolint/core (0.3.3 → 0.4.0)
- Replaced numeric
score(0–100, lower = better) with the verdict ladderready(≤20) |caution(≤40) |concerning(≤60) |critical(>60). The numeric value is still emitted asriskfor sorting and CI gating. - Consolidated 8 source-tree categories into 4 scoring super-categories:
integrity(spam + content + cannibal, weight 0.50),discoverability(links + tech, 0.20),citation(aeo + schema, 0.25),data(0.05). - Dropped 8 noisy rules (cannibal/title-overlap, cannibal/keyword-collision, content/heading-uniqueness, links/hub-pages, plus four others). Total rule count is now 32.
- New
AuditSummaryshape:schemaVersion: "2026-04-v0.4",verdict,risk,headline,categories[].grade/issues, `issues.{blockers, - New
siteClassificationfield (§4.11): pre-flight URL-pattern + sitemap - framework heuristics infersmall-marketing | blog | programmatic- (spam/template-coverage,spam/template-diversity,spam/entity-swap,cannibal/url-pattern) on small sites unless the caller passesstrict: true`. - New
AuditOptions.strictflag to bypass classification-driven rule suppression. pseolint CLI (0.3.2 → 0.4.0) - New flags:
--ci-threshold <severity>(replaces numeric--thresholdfor CI gating),--explain(full bucketed finding view),--strict(bypass pSEO-only rule suppression). --thresholddeprecated with a runtime warning — still functional for one minor.- New
pseolint diff <baseline> <current>subcommand: verdict-rank deltas + fixed/regressed/new findings between two AuditSummary JSON reports. Tolerates mixed v0.3 / v0.4 JSON. - Console formatter rewritten around the verdict ladder. JSON, HTML, and Markdown formatters all consume the new shape.
--watchflag reserved (planned for v0.4.1, not implemented). @pseolint/mcp (0.3.1 → 0.4.0)- All three tools (
audit_site,explain_score,check_page_technical) migrated to the v0.4AuditSummaryshape:summary.score→summary.risk,summary.findings→ flattened fromissues.*,summary.categoryScores→summary.categories. explain_scorenow surfacessiteClassification(type, confidence, suppressed-rule count) when present.CROSS_PAGE_RULESset updated to remove the 4 dropped cross-page rules. Migration Pre-existing v0.3 JSON reports remain readable —/r/[slug]and/r/comparein the web app detectsummary.schemaVersionand route through legacy renderers. New CI gates should switch from--threshold 40(numeric) to--ci-threshold concerning(semantic). Spec:docs/superpowers/specs/2026-04-29-pseolint-v0.4-engine-redesign.md.- Updated dependencies
- @pseolint/core@0.4.0
- v0.3.1
- v0.3.3: safeMode preset, CLI safety flags, MCP safe-by-default Consolidates the safety work from render-analytics blocking (v0.3.1), SSRF / AbortSignal / robots-honor (v0.3.2), and the safeMode preset (v0.3.3) for the CLI and MCP packages, which skipped the prior intermediate releases on npm. ## @pseolint/core → 0.3.3 Incremental over 0.3.2 (which shipped SSRF guard, AbortSignal support, and robots.txt honour for our own crawler):
safeMode: "saas" | "cli"preset onAuditOptions— flipsguardSsrf,respectRobotsTxt,followRedirects,maxCrawlDiscovered, andmaxFetchBytesdefaults in one knob. Individual option overrides still win.safeFetch(url, options?)— SSRF-safe fetch for non-audit use cases (webhook URL verification, favicon lookups, etc.). WrapscachedFetchwithvalidateTargetHostbaked in.maxCrawlDiscovered— hard ceiling on link-discovery fan-out so a malicious site with many self-links can't extend crawl up to the byte budget. Default 5000 (2000 undersafeMode: "saas").followRedirects: falseoption — returns 3xx as-is so security- sensitive audits can report redirects without following them. New exports:safeFetch,SafeModetype. ## pseolint (CLI) → 0.3.1 First CLI release since 0.3.0; bundles the v0.3.1 / v0.3.2 / v0.3.3 CLI-facing work:- Render-mode analytics blocking flags:
--analytics <block|allow|allow-first-party>,--block-host <host>(repeatable). Prevents rendered audits from firing GA / Plausible / PostHog / Mixpanel / Hotjar / Sentry beacons on every page. --safe-mode <saas|cli>— applies the core preset.--no-respect-robots— audit sitemap URLs even when the target's robots.txt Disallow's them (use for your own staging sites).--no-follow-redirects— report 3xx as-is.ctrl-Chandler — SIGINT triggers a clean abort viaAbortController; in-flight fetches cancel cleanly instead of the process being hard- killed mid-read. Secondctrl-Cwithin ~1 s forces exit.pseolint.config.tsschema extended forsafeMode,respectRobotsTxt,followRedirects,guardSsrf,maxCrawlDiscovered. ## @pseolint/mcp → 0.3.1 First MCP release since 0.3.0; picks up all of v0.3.1 / v0.3.2 / v0.3.3 via the core update, plus one meaningful default flip:- All three tool handlers (
audit_site,explain_score,check_page_technical) now defaultsafeMode: "saas". AI assistants running in end-user environments can't be tricked into scanning cloud metadata / localhost / RFC1918 networks via a malicious URL argument.guardSsrf,respectRobotsTxt, tighter caps all flip on. ## Not changed@pseolint/action— runs in GitHub-public-network runners, auto- propagates new core features through its existingAuditSummaryrendering. No separate bump. ## Test state 528 / 528 tests pass. Typecheck clean across core / cli / mcp / action. - Updated dependencies
- @pseolint/core@0.3.3
- v0.3.0
- feat: add AEO rule category with 8 rules for AI Overview readiness Introduces
aeo/*— a new scored rule category focused on Answer Engine Optimization. Brings the total to 42 rules across 8 categories (7 scored data/*). While SpamBrain rules protect against Google penalties, AEO rules audit whether pSEO pages are structured to be cited in AI Overviews (ChatGPT, Perplexity, Gemini, Claude, Google AI Overviews). Sites invisible to AI Overviews lose ~68% of traffic vs ~12% for cited sites. New rules:aeo/llms-txt— checks for/llms.txtat the origin and validates the minimal shape (H1 title, at least one##section, markdown link entries).aeo/crawler-access— parsesrobots.txtper user-agent and flags blocked AI crawlers (GPTBot, ChatGPT-User, ClaudeBot, PerplexityBot, Bytespider, Google-Extended, CCBot, Applebot-Extended). Warns per blocked crawler, errors when all are blocked.aeo/freshness-signals— checks each page for a dateModified signal (JSON-LD,article:modified_time, visible "Last updated" text). Warns when absent, emits info when older than 180 days.aeo/faq-coverage— detects FAQ-style content (question-phrased H2s or URL patterns like/how-to-*,/what-is-*,*-faq) that lacks FAQPage or HowTo JSON-LD.aeo/answer-first— scores the first paragraph after the H1 for extractable-answer quality: concrete facts, named entities, complete sentence, boilerplate detection, and template-opener detection via entity masking across the corpus.aeo/citable-facts— counts unique, entity-specific citable facts per page (dollar amounts, percentages, timeframes, dates, Form numbers). Filters out template facts shared across the majority of masked pages.aeo/non-replicable-value— detects pages that are pure informational text with no interactive element, downloadable asset, or gated content — pages AI can fully summarize without sending a click.aeo/content-modularity— splits pages by H2/H3 and flags sections that aren't independently extractable (cross-references like "as mentioned above", vague headings like "More Info", paragraphs over 200 words). ### Scoring Category weights re-balanced.aeojoins the composite score at 15%: before (0.2.x): spam 0.40 / content 0.25 / links 0.15 / tech 0.10 / schema 0.05 / cannibal 0.05 after (0.3.0): spam 0.35 / content 0.20 / aeo 0.15 / links 0.12 / tech 0.08 / schema 0.05 / cannibal 0.05AuditSummary.categoryScoresnow includes anaeofield. ### Config New flat threshold keys inAuditOptions.rules(all optional, sensible defaults):answerFirstMaxWords(default 100) — opener length cap foraeo/answer-firstcitableFactsMin(default 3) — below this a page errorscitableFactsTarget(default 8) — at or above this a page passesfreshnessMaxStaleDays(default 180) — age at whichdateModifiedis flagged stalemodularityMaxParagraphWords(default 200) —aeo/content-modularitymodularityMinSelfContainedRatio(default 0.7) —aeo/content-modularityfaqMinQuestionHeadings(default 2) —aeo/faq-coveragePage-groupoverridesstill apply normally for severity tuning. ### AEO sub-score and console sectioncategoryScores.aeois the sub-score (raw 0–100 damage — lower is better) and has its own label scheme distinct from the SpamBrain Risk label:- 0–20 AI-Ready — pages structured for citation
- 21–40 Partial — some citable, others vulnerable
- 41–60 Vulnerable — most pages will be summarized away without clicks
- 61–80 Invisible — pages offer nothing AI can't synthesize itself
- 81–100 Ghost — blocked from AI + no citable structure; traffic will crater
aeoScoreLabel(score)is exported from@pseolint/coreso downstream formatters can surface the label. The console formatter renders a dedicated AEO: AI Overview Readiness section between group scores and AI triage when anyaeo/*findings are present. ### AI triage Prompt bumped to1.1.0(additive). The system prompt now explicitly distinguishes two threat families — SpamBrain penalty risk (spam/cannibal/ content/data/tech/schema/links) and AI Overview invisibility (aeo/\*) — and asks for at least one root cause from each when both families are present. A newfindingCountByCategoryfield in the prompt payload gives the model per-category totals for weighting. - Updated dependencies [01627a8]
- Updated dependencies [bfcccc0]
- @pseolint/core@0.3.0
- feat: add AEO rule category with 8 rules for AI Overview readiness Introduces
- v0.2.2
- fix(publish): rewrite
workspace:*to real semver in published dependencies The 0.2.1 tarballs shipped withworkspace:*in theirdependencieslists, which npm cannot resolve. Anynpx pseolintornpm install pseolintsilently failed. Republishing viachangeset publishrewrites the workspace protocol to the real version range. - Updated dependencies
- @pseolint/core@0.2.2
- fix(publish): rewrite
@pseolint/action
- v0.7.5
- Updated dependencies [0966c22]
- Updated dependencies [6231a7e]
- @pseolint/core@0.7.5
- v0.7.4
- Updated dependencies
- @pseolint/core@0.7.4
- v0.7.3
- v0.7.3 — render-aware checks, AI content-effort, and bring-your-own inputs. Verdict moderators never change the raw
risknumber, so CI gates keyed off--ci-thresholdstay stable. - Bring-your-own authority. New
--authority-score <0-100>CLI flag andauthorityScoreconfig key, alongside the existing MCPauthorityScoreparam and a per-domain setting in the Pro web dashboard.>= 80shifts the verdict one tier lenient,<= 30one tier stricter. The engine stays authority-blind by design. - AI content-effort signal. New
--content-effort(+--content-effort-model) CLI flag,AuditOptions.contentEffort, and an optional MCPcontentEffortparam. An LLM judges a 0-100 content originality/effort score from sampled page text (≤10 pages, content-hash cached) that moderates the verdict ±1 tier. Opt-in, needsANTHROPIC_API_KEY, no-ops safely without one (~$0.003/page on claude-sonnet-4-6). Resolved score is written tosummary.contentEffort.score. Runs automatically for Pro audits in the web app. - Render-aware crawl checks.
--render(Playwright, Node-only) now feeds two render-diff rules:tech/csr-bailoutflags pages whose substantive content/interactivity exists only after client-side JS (invisible to crawlers + the first indexing pass), andtech/soft-404probes one synthetic nonexistent URL per template cluster (an HTTP 200 means the directory will index unbounded junk). Both no-op without--render/ outside programmatic directories. - MCP rule knowledge as resources. The MCP server now exposes the pSEO rule catalog as resources (
pseolint://rulesindex +pseolint://rules/<ruleId>per-rule Markdown) so assistants can explain findings without guessing. The open knowledge bundle is also served at/okfand linked fromllms.txt. - Updated dependencies [3361782]
- @pseolint/core@0.7.3
- v0.7.3 — render-aware checks, AI content-effort, and bring-your-own inputs. Verdict moderators never change the raw
- v0.7.2
- fix(core): schema/consistency no longer false-positives on pages with multiple JSON-LD blocks. The v0.7.1 per-cluster rewrite compared the UNION of @types across a cluster, so a template where every page legitimately emits several blocks (e.g. TechArticle + FAQPage + Organization) read as "mixed types" and fired on every cluster (6 FPs on pseolint.dev's own audit). Now it compares each page's @type SET signature and fires only when pages in the same template cluster genuinely disagree.
- fix(core): v0.7.2 rule-design batch — graded thresholds + presence-quality. Follow-up to the v0.7.1 FP-elimination batch, addressing the two deferred root causes (C: binary/absolute thresholds, D: presence-not-quality). Verified against the 24-fixture calibration corpus: zero new false positives vs the prior metrics, and the crawl-size verdict flips are gone. C — binary-threshold redesigns:
- spam/boilerplate-ratio: continuous document-frequency weighting replaces the floor(N\*0.8)+1 skeleton cliff; 2-band severity. Verdict no longer flips when one more sibling page is crawled.
- spam/template-diversity: log-bucketed coarsening of the structureSignature so single-template sites with minor chrome variation are no longer read as diverse (the exact-count signature shared with near-duplicate/doorway-pattern is untouched); confidence band.
- content/value-add: continuous categoriesPresent/4 E-E-A-T sub-score replacing the 3-step hard-threshold value; 2-band severity (drops "critical").
- content/wikipedia-paraphrase: min-length guard + threshold 0.40→0.55 above the legal/medical topic-overlap baseline; advisory language, stays low-confidence. D — presence-quality (validate the value, not just its presence):
- schema/required-fields: empty arrays / whitespace / nameless author objects count as missing.
- schema/json-ld-valid: @type accepts string OR all-string array (["Article","NewsArticle"] no longer false-positives).
- tech/og-completeness: whitespace values count as missing; severity graded (title/description warning, image-only info).
- content/eeat-signals: transparency signal reads contentText not raw html; about-link must be same-host.
- Updated dependencies [cc24997]
- Updated dependencies [3c9cb0d]
- @pseolint/core@0.7.2
- v0.7.1
- v0.7.1 — rule false-positive elimination batch (post unique-value design review). Stops the engine flagging healthy sites without weakening real detection. Each fix is TDD'd and validated against the reputable-pSEO fixtures.
- links/orphan-pages, links/cluster-connectivity — suppressed on sampled crawls (the linking/target page is often un-fetched; reliable only on a full crawl).
- tech/canonical-consistency — collapse "canonicalizes outside crawl scope" to one site-level note when all pages point at the same alternate host (staging/ preview/localhost), instead of one finding per page; dedup HTTP-vs-HTML.
- tech/sitemap-completeness — normalize sitemap URLs before the set-diff (kills trailing-slash/query false "missing"); demote the missing aggregate to warning.
- schema/consistency — flag @type variance per template cluster (structureSignature), not site-wide (was a guaranteed FP on any multi-template site).
- aeo/crawler-access — honor robots
Allowdirectives per RFC 9309 (allow-all no longer reported as fully blocked). - Severity/confidence bands — error/critical demoted to warning on weak or forecast signals: thin-content medium band, summary-bait, translation-no-op, entity-swap (low mask coverage), soft-404 (OR-weighted confidence model). Note: bundled as a patch (0.x) despite a behavior/scoring shift and the
rules.uniqueValueMinWords→rules.uniqueValueDensityconfig rename. - Updated dependencies [d9797e4]
- Updated dependencies [ce06ef7]
- @pseolint/core@0.7.1
- v0.7.0
- Unified to the 0.7.0 release line (calibration & authority foundations).
- Updated dependencies [ba1c6ca]
- Updated dependencies
- @pseolint/core@0.7.0
- v0.4.6
- Updated dependencies [44d018f]
- Updated dependencies [ea4e822]
- @pseolint/core@0.6.6
- v0.4.5
- surface partial-coverage (
truncated) audits in the GitHub Action gate The core engine now returnsAuditSummary.truncated/truncatedReasonwhen the backpressure watchdog aborts a crawl mid-flight, in which case the report is partial and verdict/risk/counts are lower bounds. The Action gated CI pass/fail solely onsummary.riskand ignoredtruncated, so a partial audit could silently PASS a CI gate on incomplete coverage. The Action now makes the partial status unmissable. On a truncated run it emits acore.warningnaming the reason and page count, sets two new outputs (truncated,truncated-reason), and adds a ⚠ blockquote to the PR comment ("Partial coverage — crawl aborted … treat a pass as provisional"). A partial run is not auto-failed (a partial pass is still informative), but a new optional inputfail-on-truncated(defaultfalse) lets workflows opt into failing the check when coverage was partial even if risk is under threshold.action.ymlnow documents all outputs (score,risk,verdict,pageCount,truncated,truncated-reason) and the new input. - Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- @pseolint/core@0.6.5
- surface partial-coverage (
- v0.4.4
- Updated dependencies
- @pseolint/core@0.6.4
- v0.4.3
- Updated dependencies
- @pseolint/core@0.4.3
- v0.4.2
- Updated dependencies
- @pseolint/core@0.4.2
- v0.4.1
- Updated dependencies
- @pseolint/core@0.4.1
- v0.1.2
- Updated dependencies
- @pseolint/core@0.4.0
- v0.1.1
- Updated dependencies
- @pseolint/core@0.3.3
- v0.1.0
- feat: add AEO rule category with 8 rules for AI Overview readiness Introduces
aeo/*— a new scored rule category focused on Answer Engine Optimization. Brings the total to 42 rules across 8 categories (7 scored data/*). While SpamBrain rules protect against Google penalties, AEO rules audit whether pSEO pages are structured to be cited in AI Overviews (ChatGPT, Perplexity, Gemini, Claude, Google AI Overviews). Sites invisible to AI Overviews lose ~68% of traffic vs ~12% for cited sites. New rules:aeo/llms-txt— checks for/llms.txtat the origin and validates the minimal shape (H1 title, at least one##section, markdown link entries).aeo/crawler-access— parsesrobots.txtper user-agent and flags blocked AI crawlers (GPTBot, ChatGPT-User, ClaudeBot, PerplexityBot, Bytespider, Google-Extended, CCBot, Applebot-Extended). Warns per blocked crawler, errors when all are blocked.aeo/freshness-signals— checks each page for a dateModified signal (JSON-LD,article:modified_time, visible "Last updated" text). Warns when absent, emits info when older than 180 days.aeo/faq-coverage— detects FAQ-style content (question-phrased H2s or URL patterns like/how-to-*,/what-is-*,*-faq) that lacks FAQPage or HowTo JSON-LD.aeo/answer-first— scores the first paragraph after the H1 for extractable-answer quality: concrete facts, named entities, complete sentence, boilerplate detection, and template-opener detection via entity masking across the corpus.aeo/citable-facts— counts unique, entity-specific citable facts per page (dollar amounts, percentages, timeframes, dates, Form numbers). Filters out template facts shared across the majority of masked pages.aeo/non-replicable-value— detects pages that are pure informational text with no interactive element, downloadable asset, or gated content — pages AI can fully summarize without sending a click.aeo/content-modularity— splits pages by H2/H3 and flags sections that aren't independently extractable (cross-references like "as mentioned above", vague headings like "More Info", paragraphs over 200 words). ### Scoring Category weights re-balanced.aeojoins the composite score at 15%: before (0.2.x): spam 0.40 / content 0.25 / links 0.15 / tech 0.10 / schema 0.05 / cannibal 0.05 after (0.3.0): spam 0.35 / content 0.20 / aeo 0.15 / links 0.12 / tech 0.08 / schema 0.05 / cannibal 0.05AuditSummary.categoryScoresnow includes anaeofield. ### Config New flat threshold keys inAuditOptions.rules(all optional, sensible defaults):answerFirstMaxWords(default 100) — opener length cap foraeo/answer-firstcitableFactsMin(default 3) — below this a page errorscitableFactsTarget(default 8) — at or above this a page passesfreshnessMaxStaleDays(default 180) — age at whichdateModifiedis flagged stalemodularityMaxParagraphWords(default 200) —aeo/content-modularitymodularityMinSelfContainedRatio(default 0.7) —aeo/content-modularityfaqMinQuestionHeadings(default 2) —aeo/faq-coveragePage-groupoverridesstill apply normally for severity tuning. ### AEO sub-score and console sectioncategoryScores.aeois the sub-score (raw 0–100 damage — lower is better) and has its own label scheme distinct from the SpamBrain Risk label:- 0–20 AI-Ready — pages structured for citation
- 21–40 Partial — some citable, others vulnerable
- 41–60 Vulnerable — most pages will be summarized away without clicks
- 61–80 Invisible — pages offer nothing AI can't synthesize itself
- 81–100 Ghost — blocked from AI + no citable structure; traffic will crater
aeoScoreLabel(score)is exported from@pseolint/coreso downstream formatters can surface the label. The console formatter renders a dedicated AEO: AI Overview Readiness section between group scores and AI triage when anyaeo/*findings are present. ### AI triage Prompt bumped to1.1.0(additive). The system prompt now explicitly distinguishes two threat families — SpamBrain penalty risk (spam/cannibal/ content/data/tech/schema/links) and AI Overview invisibility (aeo/\*) — and asks for at least one root cause from each when both families are present. A newfindingCountByCategoryfield in the prompt payload gives the model per-category totals for weighting. - Updated dependencies [01627a8]
- Updated dependencies [bfcccc0]
- @pseolint/core@0.3.0
- feat: add AEO rule category with 8 rules for AI Overview readiness Introduces
- v0.0.1
- Updated dependencies
- @pseolint/core@0.2.2
@pseolint/extension
- v1.0.1
- Updated dependencies [0966c22]
- Updated dependencies [6231a7e]
- @pseolint/core@0.7.5
- v1.0.0
- Scout→Win funnel: SERP badges are now clickable and open the pseolint.dev audit pre-filled with position-adapted context (target URL, search query, top competitor) so the free extension hands off cleanly to the paid audit.
- SERP context adaptation: the overlay activates only on the Web ("All") results vertical and resets to a clean state on SPA navigation (new query, vertical switch, pagination); it stays dormant on Images/News/Shopping.
- Fixed badge rendering: verdict badges now mount inside the result
<h3>title (previously collided with the ⋮ action menu and inherited a flipped transform). - The dev-only MCP bridge (
ws://localhost:4000, for driving the extension from a terminal/LLM) is compile-stripped from production builds via--define; `bun run - Removed a bundle leak: the service worker no longer includes the cheerio HTML parser (the server-only soft-404 probe moved to its own core module), keeping the extension truly dependency-free.
- v0.0.3
- Updated dependencies
- @pseolint/core@0.7.4
- v0.0.2
- Updated dependencies [cc24997]
- Updated dependencies [3c9cb0d]
- @pseolint/core@0.7.2
- v0.0.1
- Updated dependencies [d9797e4]
- Updated dependencies [ce06ef7]
- @pseolint/core@0.7.1
@pseolint/web
- v0.7.5
- Updated dependencies [0966c22]
- Updated dependencies [6231a7e]
- @pseolint/core@0.7.5
- v0.7.4
- Updated dependencies
- @pseolint/core@0.7.4
- @pseolint/mcp@0.7.4
- v0.7.3
- v0.7.3 — render-aware checks, AI content-effort, and bring-your-own inputs. Verdict moderators never change the raw
risknumber, so CI gates keyed off--ci-thresholdstay stable. - Bring-your-own authority. New
--authority-score <0-100>CLI flag andauthorityScoreconfig key, alongside the existing MCPauthorityScoreparam and a per-domain setting in the Pro web dashboard.>= 80shifts the verdict one tier lenient,<= 30one tier stricter. The engine stays authority-blind by design. - AI content-effort signal. New
--content-effort(+--content-effort-model) CLI flag,AuditOptions.contentEffort, and an optional MCPcontentEffortparam. An LLM judges a 0-100 content originality/effort score from sampled page text (≤10 pages, content-hash cached) that moderates the verdict ±1 tier. Opt-in, needsANTHROPIC_API_KEY, no-ops safely without one (~$0.003/page on claude-sonnet-4-6). Resolved score is written tosummary.contentEffort.score. Runs automatically for Pro audits in the web app. - Render-aware crawl checks.
--render(Playwright, Node-only) now feeds two render-diff rules:tech/csr-bailoutflags pages whose substantive content/interactivity exists only after client-side JS (invisible to crawlers + the first indexing pass), andtech/soft-404probes one synthetic nonexistent URL per template cluster (an HTTP 200 means the directory will index unbounded junk). Both no-op without--render/ outside programmatic directories. - MCP rule knowledge as resources. The MCP server now exposes the pSEO rule catalog as resources (
pseolint://rulesindex +pseolint://rules/<ruleId>per-rule Markdown) so assistants can explain findings without guessing. The open knowledge bundle is also served at/okfand linked fromllms.txt. - Updated dependencies [3361782]
- @pseolint/core@0.7.3
- pseolint@0.7.3
- @pseolint/mcp@0.7.3
- v0.7.3 — render-aware checks, AI content-effort, and bring-your-own inputs. Verdict moderators never change the raw
- v0.7.2
- fix(core): schema/consistency no longer false-positives on pages with multiple JSON-LD blocks. The v0.7.1 per-cluster rewrite compared the UNION of @types across a cluster, so a template where every page legitimately emits several blocks (e.g. TechArticle + FAQPage + Organization) read as "mixed types" and fired on every cluster (6 FPs on pseolint.dev's own audit). Now it compares each page's @type SET signature and fires only when pages in the same template cluster genuinely disagree.
- fix(core): v0.7.2 rule-design batch — graded thresholds + presence-quality. Follow-up to the v0.7.1 FP-elimination batch, addressing the two deferred root causes (C: binary/absolute thresholds, D: presence-not-quality). Verified against the 24-fixture calibration corpus: zero new false positives vs the prior metrics, and the crawl-size verdict flips are gone. C — binary-threshold redesigns:
- spam/boilerplate-ratio: continuous document-frequency weighting replaces the floor(N\*0.8)+1 skeleton cliff; 2-band severity. Verdict no longer flips when one more sibling page is crawled.
- spam/template-diversity: log-bucketed coarsening of the structureSignature so single-template sites with minor chrome variation are no longer read as diverse (the exact-count signature shared with near-duplicate/doorway-pattern is untouched); confidence band.
- content/value-add: continuous categoriesPresent/4 E-E-A-T sub-score replacing the 3-step hard-threshold value; 2-band severity (drops "critical").
- content/wikipedia-paraphrase: min-length guard + threshold 0.40→0.55 above the legal/medical topic-overlap baseline; advisory language, stays low-confidence. D — presence-quality (validate the value, not just its presence):
- schema/required-fields: empty arrays / whitespace / nameless author objects count as missing.
- schema/json-ld-valid: @type accepts string OR all-string array (["Article","NewsArticle"] no longer false-positives).
- tech/og-completeness: whitespace values count as missing; severity graded (title/description warning, image-only info).
- content/eeat-signals: transparency signal reads contentText not raw html; about-link must be same-host.
- Updated dependencies [cc24997]
- Updated dependencies [3c9cb0d]
- @pseolint/core@0.7.2
- pseolint@0.7.2
- @pseolint/mcp@0.7.2
- v0.7.1
- v0.7.1 — rule false-positive elimination batch (post unique-value design review). Stops the engine flagging healthy sites without weakening real detection. Each fix is TDD'd and validated against the reputable-pSEO fixtures.
- links/orphan-pages, links/cluster-connectivity — suppressed on sampled crawls (the linking/target page is often un-fetched; reliable only on a full crawl).
- tech/canonical-consistency — collapse "canonicalizes outside crawl scope" to one site-level note when all pages point at the same alternate host (staging/ preview/localhost), instead of one finding per page; dedup HTTP-vs-HTML.
- tech/sitemap-completeness — normalize sitemap URLs before the set-diff (kills trailing-slash/query false "missing"); demote the missing aggregate to warning.
- schema/consistency — flag @type variance per template cluster (structureSignature), not site-wide (was a guaranteed FP on any multi-template site).
- aeo/crawler-access — honor robots
Allowdirectives per RFC 9309 (allow-all no longer reported as fully blocked). - Severity/confidence bands — error/critical demoted to warning on weak or forecast signals: thin-content medium band, summary-bait, translation-no-op, entity-swap (low mask coverage), soft-404 (OR-weighted confidence model). Note: bundled as a patch (0.x) despite a behavior/scoring shift and the
rules.uniqueValueMinWords→rules.uniqueValueDensityconfig rename. - Updated dependencies [d9797e4]
- Updated dependencies [ce06ef7]
- @pseolint/core@0.7.1
- pseolint@0.7.1
- @pseolint/mcp@0.7.1
- v0.7.0
- v0.7.0 — Off-page-authority disclosure + docs freshness
/limitsnow discloses the off-page-authority blind spot: pseolint measures on-page structural risk and cannot see the off-page authority and user-behaviour signals Google weighs most heavily, so a thin templated page on a high-authority domain may rank fine while a clean-looking page can still be suppressed.- Version, rule-count, and scoring-model copy synced across the app to the current engine (44 rules across 8 categories; the v0.4 super-category verdict model).
- Updated dependencies [ba1c6ca]
- Updated dependencies
- @pseolint/core@0.7.0
- pseolint@0.7.0
- @pseolint/mcp@0.7.0
- v0.6.7
- Updated dependencies [44d018f]
- Updated dependencies [ea4e822]
- @pseolint/core@0.6.6
- v0.6.6
- surface partial (
truncated) audits in the web app The core engine flushes atruncated: truereport when its backpressure watchdog aborts a crawl mid-flight (degraded origin) — counts, risk, and the verdict are then lower bounds. The web app stored that summary to R2 and rendered it, but never surfaced the flag, so a degraded audit looked identical to a complete one. /r/[slug]now renders a prominent partial-coverage warning banner above the hero when the R2 summary hastruncated === true, includingtruncatedReasonwhen present. This reads straight off the summary blob — no DB column needed.- The
audittable gainstruncated(boolean, default false) andtruncated_reason(text) columns (migration0021_loud_emma_frost), mirrored fromAuditSummaryin the run-audit completion update so degraded audits are queryable/filterable without round-tripping R2. - The per-domain workspace (
/dashboard/[host]) shows a small "⚠ Partial" badge next to the latest-audit header when the run was truncated. - Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- @pseolint/core@0.6.5
- @pseolint/mcp@0.6.5
- pseolint@0.6.4
- surface partial (
- v0.6.5
- Updated dependencies
- @pseolint/core@0.6.4
- v0.6.4
- Stream A — secondary marketing refresh (tools / rules / symptoms / pricing).
- Tools page (`tools/page.tsx`): new "How rules feed into per-template verdicts" section near the top — four bullet points explaining per-page→uniformity-score aggregation (
spam/thin-content), corpus-wide non-template-scoped detection (spam/near-duplicate), per-page→template-level signal (aeo/citable-factsat 80% fire rate = one template finding), and site verdict viasiteVerdictFromTemplatesspec §15.1. Version reference updated to v0.6 throughout; comparison table "Audit limit" cell updated to K=10-per-template framing; SpamBrainaware column updated to "template-aware v0.6 engine; per-template verdicts". Version history paragraph updated to mention v0.6 template architecture. - Rules page (`rules/page.tsx`): new "Per-template aggregation — how rules feed verdicts" section before "How the rules map to SpamBrain" — describes Phase 1/Phase 2 pipeline, uniformity score, top driver concept, and the three aggregation patterns. Rule count updated to 32. Subtitle badge updated to "5 of 32 featured". Metadata description and footer CTA updated with per-template verdict language. FAQ answer updated to describe v0.6 per-template aggregation.
- Symptoms page (`symptoms/page.tsx`): new "Template-level symptoms — the v0.6 failure modes" section above the existing triage section — three named symptom types: "Thin pages on a template" (uniformity score ≥0.8), "Cross-template duplication" (
spam/near-duplicatecorpus-wide), "One bad template among many" (worst-template ≥5% coverage = critical site verdict). Intro paragraph updated with K=10 per template framing and note that v0.6 identifies the responsible template. Triage section updated to say "which template and which rules" rather than "which URLs". FAQ answer updated to mention template identification. - Pricing client (`pricing/pricing-client.tsx`):
COMPARISON_ROWS— "Pages per audit" row replaced with "Sampling model" showing K=10 vs K=20 distinction; new "Per-template verdict" row added; "Background monitoring" row updated to mentiontemplate_degradedalerts.PRO_FEATURES— "Unlimited monitored domains" detail updated with template_degraded mention; new "Per-template verdict — which template is broken" feature entry added (K=20/K=10 distinction, 8×10=80 fetches typical). "Free vs Pro" intro paragraph updated to reference@pseolint/core 0.6.0and T×K sampling model. "Why we chose this pricing" paragraph updated with v0.6 template engine mention andsiteVerdictFromTemplates. Self-hosted FAQ updated to reference core 0.6.0. Free tier FAQ updated with K=10-per-template framing. Pricing metadata updated. - `package.json`: version 0.6.3 → 0.6.4.
- Regulatory groundings preserved across all pages: SpamBrain, March 27 2026 core update, May 7 2024 site-reputation-abuse, March 5 2024 scaled-content-abuse — none stripped.
- v0.6.3
- Stream C — marketing-surface refresh + TemplateBreakdownHero visual.
- New component:
src/components/landing/template-breakdown-hero.tsx— v0.6 visual centerpiece. Renders a 3-card<TemplateCard>grid with realistic mock data (/listing/:slug8201 URLs risk 60,/category/:slug142 URLs risk 30,/article/:slug89 URLs risk 12), an annotation callout explainingsiteVerdictFromTemplatesspec §15.1 (≥5% coverage rule), and a side-by-side v0.5 flat-sample vs v0.6 per-template comparison footer with colour-coded mini bar charts. - Landing page (`page.tsx`): hero badge updated to "Template-aware SpamBrain + AEO · v0.6.3"; H1 reframed around template auditing; hero description emphasises "which templates are broken";
TemplateBreakdownHeroinjected between the hero grid and the Scope section; "What pseolint is" bullet list updated with v0.6 template-audit framing + CI gate copy updated to "fails when a template degrades"; "By the numbers" bullets updated to mention templates explicitly (T × K sampling model, Pro monitoring description); STATS chip changed from "Free-tier pages" to "K per template (Pro) = 10 URLs"; bottom CTA updated to "per-template verdict". - Methodology page (`methodology/page.tsx`): new "How v0.6 audits work" section near the top — Phase 1 (template detection, filter ≥1%, ≥5 URLs, ≥2 survivors), Phase 2 (K=10/20 per template, 32 rules), aggregation (worst template ≥5% coverage), variance metric (uniformity score formula); ASCII two-phase pipeline diagram; version badge updated to v0.6.3.
- Limits page (`limits/page.tsx`): Scope section updated — "K=10 per template" sampling model replaces flat-page counts; Pro monitoring run description now shows the T × K = 80 fetches typical figure; cumulative coverage copy updated "across all templates"; new "Why per-template?" callout box explains the v0.5→v0.6 cost-vs-coverage tradeoff; intro paragraph updated to reflect per-template language.
- `package.json`: version at 0.6.3 (was 0.6.0).
- v0.6.0
- Version-aligned with `@pseolint/core` 0.6.0 — the audit-as-template architectural cutover. Web app now ships at 0.6.0 to match the engine's "v0.6 stable" milestone. Functionally cumulative of all v0.0.x work since 0.0.10, plus the v0.6 cutover changes:
<FindingsPanel>now wrapped in a collapsed<details>element when the audit has ≥2 detected templates (template cards become primary surface, per-URL findings drill-down only). Legacy / single-template audits unchanged.- Tracks engine 0.5.11 → 0.6.0 cumulative changes that landed in apps/web through normal commits: GSC hotfix LIMIT 500, GSC origin-degradation autobind, template card grid (v0.5.10 phase 2), gentle-mode + auto-retry origin-degradation handling, AuditLogEvent additions (gsc.autobind._, gsc.rebind._, settings.domain.updated, audit.gentle_mode_applied).
- Web app remains private (not published to npm). Version bump is internal-coherence only — engine + dashboard are now versioned together.
- v0.0.10
- v0.6 phase 2 — per-template cards on the dashboard. The per-domain dashboard now renders a responsive 1/2/3-column grid of
TemplateCards above the per-URL findings list when the audit'ssummary.templates.length >= 2. Each card shows: signature (mono title), grade chip (viagradeOf(risk)), top-driver one-line summary ("8/10 samples fail spam/thin-content"), URL coverage stat (234 / 8200 URLs (2.9%)), and a uniformity bar with red/yellow/green tints at 0.4/0.7 thresholds. - Drill-down via URL hash (
#template=/listing/:slug) — clicking a card filters the per-URL findings list to that template'sauditedUrlsand survives reload + back/forward navigation. Clicking the active card or a "Clear filter" pill clears the selection. - Fallback unchanged. Single-template sites,
unclear/small-marketingclassifications, and all pre-v0.5.9 audits keep the legacy per-URL-only view (the>= 2 templatesgate from spec §15.3). - Persistence: confirmed templates ride through R2 (the full
AuditSummaryJSON is serialized verbatim viauploadSummary+fetchSummaryJson). No DB migration. Old audits havetemplates: []and fall through cleanly. - `AuditLogEvent` union extended with
template_degradedevent name; firing logic ships in v0.5.11. - 14 new tests across
template-card.test.ts+dashboard-templates.test.ts. Full suite: 97 pass (+14).
- v0.6 phase 2 — per-template cards on the dashboard. The per-domain dashboard now renders a responsive 1/2/3-column grid of
- v0.0.9
- GSC live integration completed. The rich GSC card on the per-domain dashboard (monthly trend, top templates, weighted-avg position, CTR) now renders live data for all connected GSC integrations. The card was already wired to query
gsc_page_metricsand the daily Inngest cron (sync-gsc.ts, schedule0 2 * * *UTC) was already populating the table — but the 4 computed values were dropped at theGscStatusStripcallsite; one prop-wiring fix activates the rich variant. - On-demand GSC refresh. New
POST /api/gsc/refresh/[host]route fires agsc/sync-requestedInngest event, handled by a new event-drivensync-gsc-on-demand.tsfunction. Rate-limited to 1/hour per user-host (effectively daily — bumpRateLimit's day-scoped key) to respect Google's 1200 QPD quota. Backed by a sharedlib/gsc-sync-core.tsso cron and on-demand share the upsert path. - 5 new tests in
tests/integration/gsc-sync.test.tscovering the full error surface ofsyncOneDomain(auth refresh failures, 429 backoff, empty response, partial chunk, total quota exhaustion).
- GSC live integration completed. The rich GSC card on the per-domain dashboard (monthly trend, top templates, weighted-avg position, CTR) now renders live data for all connected GSC integrations. The card was already wired to query
- v0.0.8
/api/auditsPOST handler: Pro branch now uses the sharedassertProAuditAllowedhelper fromlib/audit-gate.ts(introduced in v0.5.3). Removes ~40 LOC of inline gate duplication. Anon and free branches stay inline (different gate combinations). Status codes and response bodies unchanged; one Pro in-flight 429 message no longer interpolates(count/limit)since the helper doesn't surface those values (no test asserts the exact body). Picks up@pseolint/core0.5.4 with the newcontent/translation-no-oprule.
- v0.0.7
- Grade band labels aligned with engine verdict ladder.
lib/grade.tsband labels now matchverdictForRiskvocabulary: B reads "caution" (was "good"), D reads "critical" (was "severe"), tones shifted to warning so a "B 37 / caution" visual mismatch is impossible. The bestfirenze.com self-audit revealed the misalignment. - Vitest server-only stub (
tests/server-only-stub.ts+vitest.config.tsalias). Lets server-side modules (db/index.ts,lib/env.ts,lib/r2.ts, etc.) load under vitest without throwing the Next.js client-component guard. Also re-pointed a stalereserveAnonAuditSlotimport inaudit-rate-limit.test.tsatlib/anon-rate-limit.ts(split out in 8499ad9). - Watched pages (Pro). Pin up to 20 URLs per monitored domain; pinned URLs are force-refetched on every monitoring run regardless of diff-mode skip. New
watched_pagetable (migration0013_narrow_king_bedlam.sql), server actionsaddWatchedPage/removeWatchedPageinsrc/app/dashboard/domain-actions.tswith full validation (SSRF guard, host-match against the monitored domain, www-equivalence, atomic 20-page cap, duplicate rejection). Adding a URL fires an immediateaudit/requestedwithforce: { urls: [...] }— gated by the sameDAILY_AUDIT_CAPthe public POST/api/auditsroute enforces, so the audit-on-add path can't bypass cost protection. When the daily cap is hit the watched row stays pinned and the URL audits on the next monitoring tick. - Engine force-include wiring.
audit/requestedInngest event payload now carries optionalforce?: { urls?: string[] }, threaded intoauditSource(...)(consumes@pseolint/core@0.5.3). All four entry points pass watched URLs through:monitor-domains.tscron,lib/monitoring.tskickoff/re-activation,domain-actions.tsinitial-add, anddomain-actions.tsmanual re-audit. - Cumulative coverage card. Per-domain dashboard now surfaces total URLs audited across the full audit history (Postgres aggregate of
audits.pageCount), with a 30-day-window sub-line. Hidden silently when a domain has no completed audit history. NewgetCumulativeCoverage()inlib/monitoring.ts; new component atcomponents/dashboard/cumulative-coverage-card.tsx./limitspage copy updated to explain the cumulative-coverage framing for Pro monitoring. - Consolidated plan limits. Daily caps (5/50), anon cap (3), Pro re-audit sample size (500), Pro monitoring sample size (200), and downgraded monitoring sample size (50) are now named constants (
DAILY_AUDIT_CAP,PRO_REAUDIT_SAMPLE_SIZE,PRO_MONITOR_SAMPLE_SIZE,DOWNGRADED_MONITOR_SAMPLE_SIZE,WATCHED_PAGES_CAP) inlib/audit-limits.ts. Magic numbers eliminated fromaudits/route.ts,monitoring.ts,domain-actions.ts,monitor-domains.ts. The public-form 300-cap vs dashboard-re-audit 500-cap split is now documented in code. - New
auditLogevents:watched_page.added,watched_page.removed,watched_page.cap_reached. - Updated dependencies
- @pseolint/core@0.5.3
- Grade band labels aligned with engine verdict ladder.
- v0.0.6
- Updated dependencies
- @pseolint/core@0.4.3
- pseolint@0.4.3
- v0.0.5
- Updated dependencies
- @pseolint/core@0.4.2
- pseolint@0.4.2
- v0.0.4
- Updated dependencies
- @pseolint/core@0.4.1
- pseolint@0.4.1
- v0.0.3
- Updated dependencies
- @pseolint/core@0.4.0
- pseolint@0.4.0
- v0.0.2
- Updated dependencies
- @pseolint/core@0.3.3
- pseolint@0.3.1
- v0.0.1
- Updated dependencies [01627a8]
- Updated dependencies [bfcccc0]
- @pseolint/core@0.3.0
- pseolint@0.3.0