Repository navigation
Audit Skill
The audit skill scores a site across five content layers — SEO, AEO, GEO, perf and
security — plus a site-level cross-URL pass, an indexability-contradictions pass, and one
sitemap-hygiene check, returning findings that carry an observed value, an expected
value, a fix and a fixTier. It never edits the site; it only diagnoses, then hands the
list to a human, CI job, or omnirank fix.
Everything on this page is verified against scripts/py/omnirank/audit.py,
scripts/py/omnirank/gates/, scripts/py/omnirank/report.py, and skills/audit/.
Claude Code matches a skill against its SKILL.md description frontmatter, not a fixed
command name. The real description is: "Use when asked to audit a site's SEO, check AEO
or answer-engine readiness, diagnose why a page is not ranking or not being cited by AI,
verify structured data, or run pre-deploy discoverability checks on built HTML." Trigger
phrases and what will not trigger it: Claude-Code-Setup#what-should-i-say-to-trigger-each-skill.
Verbatim --help output from v0.4.0:
$ python3 -m omnirank.cli audit --help
usage: omnirank audit [-h] [--config CONFIG] [--out OUT]
[--fail-on [FAIL_ON ...]] [--detail] [--top TOP]
[url]
positional arguments:
url Site root. Omit when using --config.
options:
-h, --help show this help message and exit
--config CONFIG Path to omnirank.config.json
--out OUT Report path (default
.omnirank/reports/<date>-audit.json)
--fail-on [FAIL_ON ...]
Gate ids that force exit code 1. Overrides config.
--detail Print every finding individually instead of the
grouped summary (capped at 25, same as before v0.2.1).
--top TOP Limit the grouped summary to the top N groups
(default: all). Ignored with --detail.
| Flag | Required | Description |
|---|---|---|
url (positional) |
Only if --config is absent |
Site root to audit |
--config PATH |
No | Path to a validated omnirank.config.json — see Configuration-Reference
|
--out PATH |
No | Where the JSON report is written. Default: .omnirank/reports/<UTC-date>-audit.json
|
--fail-on [GATE ...] |
No | Zero or more gate names (not finding ids) that force exit code 1 when they carry an error-severity finding |
--detail |
No | Print the old, ungrouped, one-finding-per-block listing (capped at 25) instead of the grouped summary |
--top N |
No | Show only the first N groups of the grouped summary (default: all). Ignored with --detail
|
The help text calls --fail-on's arguments "gate ids" — that wording is imprecise. They
are gate names (h1, canonical, schema, ...), coarser than the per-finding id
(seo.h1.missing, seo.canonical.relative, ...). has_failures() matches on
finding.gate, never on finding.id. See Report-Schema for the full id convention.
Two behaviours worth being precise about:
-
Omitting
--fail-onentirely defers to the config'saudit.failOn(or[]— never fail — when auditing a bare URL with no config). -
Passing
--fail-onwith zero gate names explicitly overrides the config to an empty gate list, so the run always exits0regardless of whataudit.failOnsays.
Console output groups findings by id, shows a count, the shared expected/fix
text, and up to three example URLs — every group is shown, nothing is silently truncated.
--detail keeps the historical ungrouped, 25-finding-capped view. The JSON report is
unaffected by either flag: it always holds every finding individually.
| Layer | Audience | What it wants |
|---|---|---|
| SEO | Googlebot, Bingbot | Crawlable, canonical, correctly sized metadata, valid structured data, and no self-contradictions between what the site submits and what it forbids |
| AEO | AI Overviews, Copilot, voice assistants | A short, liftable, factual answer near the top of the page, sized for the page's script |
| GEO | ChatGPT, Claude, Perplexity, Gemini | Machine-ingestible ground truth (llms.txt, facts.json) plus explicit permission to cite |
| perf | Every crawler and user agent | Fast full response time, reasonable HTML weight, compression, and no excess render-blocking <head> scripts — derived from one HTTP response, no browser involved |
| security | Browsers rendering the page | No mixed content and an http→https redirect (the two that break crawling/rendering); response headers reported as inventory, never graded — new in v0.4.0, see Security-Layer |
Crawl hygiene, the site-level cross-URL pass, and the indexability-contradictions pass
(Contradictions) all carry layer: "seo" in the report — there is no separate
"hygiene", "site" or "contradictions" value. report.py defines Layer = Literal["seo", "aeo", "geo", "offsite", "smm", "perf", "security"]. perf has been a
real, populated layer since v0.2.0. security is new in v0.4.0 and is populated the same
way (security.run_page(page) per page, plus one site-level http:// redirect probe).
offsite and smm remain reserved for roadmap skills and are unused by any gate today.
As of 0.2.0, audit_site() keeps every fetched page's HTML alive as a PageData record
instead of discarding it after the per-page gates run — that is what makes the site-level
and contradiction passes below possible.
Every finding carries a gate (what --fail-on matches against), an id (a stable,
dotted identifier), and a fixTier (mechanical/templated/drafted/advisory/
infrastructure, described in Fix-Tiers-and-Applicability). For the complete,
registry-generated table of all 67 ids see Finding-Reference.
| Gate | Rule | Severity | Fix |
|---|---|---|---|
h1 |
Exactly one <h1>
|
error | Add a single <h1> naming the page's subject, or demote extras to <h2>
|
canonical |
Present, absolute (http(s)://...), self-referencing |
error | Add <link rel="canonical" href="..."> with an absolute URL |
title-length |
<title> present, ≤ 60 characters |
error if missing, warning if over | Add or shorten the <title>
|
description-length |
Meta description present, ≤ 160 characters | error if missing, warning if over | Add or clamp the description |
og |
og:title and og:image both present |
warning | Add the missing OpenGraph tags |
hreflang |
If any hreflang alternates exist, one is x-default
|
warning | Add <link rel="alternate" hreflang="x-default" href="...">
|
image-dims |
Every <img> has both width and height
|
warning | Set explicit dimensions so the browser reserves space |
lastmod-inflation |
See below | warning | Stamp lastmod from real publish/update timestamps |
schema |
See below | error / warning | — |
schema-fabrication |
See below | error | — |
schema-required |
See below | warning | — |
| Gate | Rule | Severity |
|---|---|---|
answer-block |
An element matches the configured selector (default .answer-block) |
error if absent |
answer-block |
The block's text falls inside its script's word/character band | error if outside the range |
answer-block |
The block contains no <ul>/<ol>/<li>
|
error if list markup is present |
faq |
At least 3 FAQ pairs, as <dl>/<dt>/<dd> or <details> elements |
warning if fewer (downgraded from error in v0.2.1) |
speakable |
Every speakable.cssSelector in any JSON-LD block resolves to a real element on the page |
error if any selector matches nothing |
The band is script-aware, not a fixed 40–60 words. str.split() returns a single
token for an entire Chinese, Japanese, Thai or Khmer paragraph, so a fixed band would flag
every compliant CJK answer block as far too short. bands.resolve_band() measures
characters (80–200 by default) for the cjk script family, keyed off the page's
<html lang>, and keeps word counting for Latin, Brahmic (Bengali, Hindi, Tamil,
...), Arabic and Cyrillic scripts. Configure per script with aeo.answerBlock — see
Configuration-Reference#aeoanswerblock. A missing <html lang> breaks this
silently — see lang under On-page gates below.
| Gate | Rule | Severity |
|---|---|---|
llms-txt |
/llms.txt returns HTTP 200 |
error |
llms-full |
/llms-full.txt returns HTTP 200 |
error |
facts-json |
/facts.json returns HTTP 200 and parses as JSON |
error |
ai-allowlist |
/robots.txt does not Disallow: / any of the AI crawler user-agents OmniRank checks |
error |
citation-licence |
/llms.txt contains a licence or attribution statement |
warning |
Finding ids are derived from the artifact's filename with its extension stripped, not
from the gate name. Match on id, not gate, when you need a specific check:
| Path | Gate | Missing-artifact id | Distinguishable 403 id |
|---|---|---|---|
llms.txt |
llms-txt |
geo.llms.missing |
(none — a 403 here also reports as geo.llms.missing) |
llms-full.txt |
llms-full |
geo.llms-full.missing |
geo.llms-full.forbidden |
facts.json |
facts-json |
geo.facts.missing |
geo.facts.forbidden |
A 403 on llms-full.txt or facts.json gets its own id because the cause is specific and
common — see the OpenNext/CloudFront 403 trap in GEO-Artifacts-Skill. facts.json
fetching successfully but failing to parse is reported under a fixed id,
geo.facts-json.invalid — the one case where the id keeps the gate's full name rather
than the filename stem.
ai-allowlist checks 19 AI-crawler user-agents against your published robots.txt:
GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, anthropic-ai, Claude-Web,
PerplexityBot, Perplexity-User, Google-Extended, Applebot-Extended,
Meta-ExternalAgent, Amazonbot, CCBot, Bytespider, Cohere-AI, DuckAssistBot,
Diffbot, YouBot, PetalBot. An agent counts as blocked if it has its own
User-agent: block containing a bare Disallow: /, or if it has no block of its own and
the wildcard (*) block blocks everything. A Disallow: /some-path does not count —
only a full-site block trips this gate.
| Gate | Rule | Severity |
|---|---|---|
schema |
At least one application/ld+json block exists |
error |
schema |
Every block parses as JSON | error |
schema |
Every top-level node has @type
|
error |
schema |
Every top-level node has @context
|
warning |
schema-fabrication |
Every AggregateRating node has a non-zero ratingCount (or reviewCount) |
error |
schema-fabrication |
Every Review node has an author
|
error |
schema-required |
Google's documented rich-result properties are present for Article/NewsArticle/BlogPosting, Product, FAQPage, BreadcrumbList, Organization and LocalBusiness — new in v0.4.0 | warning |
schema-required checks GOOGLE's requirement, not schema.org's. schema.org marks no
property required at all, so a node can be perfectly valid schema.org and still miss a
Google rich result — every seo.schema-required.missing-property finding says so
explicitly, and names both RICH_RESULT_RULES and the date the table was transcribed
(RICH_RESULT_RULES_AS_OF = "2026-08-04" in gates/jsonld.py). It is warning, not
error, because the table is a hand-transcribed snapshot with no freshness test yet, and
a stale required-property table would produce fabricated errors at error severity. One
finding per node, listing every missing property. A node carrying only @id/@type/
@context is a reference to an entity declared elsewhere and is never checked — flagging
it would report correct markup as broken.
"Top-level node" means each <script type="application/ld+json"> payload, or each child
of a @graph array. A @graph container's @context propagates to its children
automatically, so a child that omits its own @context inside a @graph is never warned
about. The fabrication checks walk every dict anywhere in the parsed tree (capped at 100
levels deep), so a Review or AggregateRating nested inside mainEntity or
itemReviewed is still caught. Both seo.py and jsonld.py match type= and rel=
attribute VALUES case-insensitively — meta name="Description", rel="Canonical",
hreflang="X-Default", and both application/LD+JSON and
application/ld+json; charset=utf-8 are all recognised correctly. The shared comparison
lives in omnirank/html.py.
The fabrication gates are not a style preference. An AggregateRating with no real
ratingCount is a manual-action risk with Google, and it corrodes the trust the markup
exists to build in the first place.
security.run_page(page) derives four gates from the already-fetched response's headers
(plus a CSP <meta http-equiv> fallback), and security.check_https_redirect() makes one
extra request per audit against the site's http:// origin. All findings carry
layer: "security", a layer that did not exist before v0.4.0.
Scope is deliberately narrow: OmniRank checks security only where insecurity demonstrably breaks crawling, indexing or rendering. Mozilla Observatory and testssl.sh already grade headers properly; an SEO tool scoring CSP strength would be doing a job it cannot do well. Full detail, including why only two of the six ids can move the score: Security-Layer.
| Gate | Finding id | Rule | Severity |
|---|---|---|---|
hsts |
security.hsts.missing |
An https:// response carries no Strict-Transport-Security header |
info |
hsts |
security.hsts.short-max-age |
Strict-Transport-Security present but max-age below 15,552,000 seconds (180 days — OmniRank's own floor, not a vendor requirement) |
info |
nosniff |
security.nosniff.missing |
X-Content-Type-Options is not exactly nosniff
|
info |
csp |
security.csp.absent |
No Content-Security-Policy, by header or <meta http-equiv>
|
info |
referrer-policy |
security.referrer-policy.missing |
No Referrer-Policy header |
info |
mixed-content |
security.mixed-content.subresource |
An https:// page requests a blockable subresource (script, iframe, or link rel=stylesheet|preload|modulepreload) over literal http://
|
error |
mixed-content |
security.mixed-content.passive-subresource |
An https:// page requests a passive subresource (img, or link rel=icon|apple-touch-icon|manifest|prefetch) over literal http:// — browsers auto-upgrade these rather than block them |
warning |
https-redirect |
security.https-redirect.missing |
The site's http:// origin does not 3xx-redirect to https://
|
error |
site.run(pages, sitemap_urls) needs the whole crawled set at once, so audit_site()
runs it once, after every per-page gate has finished. All five findings carry
layer: "seo".
| Gate | Finding id | Rule | Severity |
|---|---|---|---|
duplicate-title |
seo.duplicate-title.shared |
Two or more crawled pages share a <title>
|
warning |
duplicate-description |
seo.duplicate-description.shared |
Two or more crawled pages share a meta description | warning |
noindex-in-sitemap |
seo.noindex.in-sitemap |
A crawled page carries a noindex token yet its URL also appears in sitemap.xml
|
error |
canonical-cluster |
seo.canonical.chained |
A crawled page's canonical points at another crawled page that itself canonicalises elsewhere (A → B → C) | warning |
hreflang-reciprocity |
seo.hreflang.not-reciprocal |
A crawled page declares an hreflang alternate that does not link back (x-default exempt) |
warning |
One finding per duplicate group, not per URL — a 200-page site sharing one template title produces one finding, not 200. Targets outside the crawled set are never judged — a canonical or hreflang alternate pointing outside the sitemap produces no finding either way.
Defects provable from the site's own declarations, with no external truth required — the
category docs/research/2026-08-04-competitive-gap-analysis.md §1 identifies as the one
OmniRank can credibly own at 100% precision, since both halves of every contradiction come
from the site itself. Full detail, including the two rules every check in this module
follows and the real Python-version caveat: Contradictions.
| Gate | Finding id | Rule | Severity |
|---|---|---|---|
robots-sitemap |
seo.robots-sitemap.disallowed |
A URL listed in sitemap.xml is also Disallow-ed to Googlebot in robots.txt
|
error |
canonical-target |
seo.canonical-target.noindexed |
A page's canonical points at a URL that carries a noindex directive |
error |
canonical-target |
seo.canonical-target.not-found |
A canonical target returns 404 or 410
|
error |
canonical-target |
seo.canonical-target.redirects |
A canonical target itself 3xx-redirects | warning |
hreflang-noindex |
seo.hreflang-noindex.alternate |
A page declares an hreflang alternate at a page that carries noindex (attached to the declaring page; x-default exempt) |
error |
perf.run(page) derives every finding from the single HTTP response already captured for
that page. No browser is involved — OmniRank cannot measure Largest Contentful Paint,
Cumulative Layout Shift, Interaction to Next Paint, or produce a Lighthouse score.
| Gate | Finding id | Rule (constant) | Severity |
|---|---|---|---|
response-time |
perf.response-time.slow |
Response took ≥ RESPONSE_WARN_MS (2000 ms) |
warning |
response-time |
perf.response-time.critical |
Response took ≥ RESPONSE_ERROR_MS (5000 ms) |
error |
page-weight |
perf.page-weight.heavy |
Raw HTML exceeds HTML_WARN_BYTES (500,000 bytes) |
warning |
compression |
perf.compression.missing |
No content-encoding of gzip or deflate
|
warning |
render-blocking |
perf.render-blocking.head-scripts |
More than MAX_HEAD_SCRIPTS (2) blocking <script> tags in <head>
|
warning |
The gate is named response-time, not ttfb. page.elapsed_ms brackets the entire
client.get() call — DNS through reading the complete response body — not the time to
first byte. Treat every perf finding as a signal to investigate from wherever OmniRank's
own request ran (a laptop, a CI runner), never as a metric a real visitor experienced.
Full detail: docs/audit-guide.md#performance-perf-layer.
gates/onpage.py — body markup that search engines and assistive technology both read.
Kept out of seo.py, which owns only the <head>'s indexing signals: canonical, title,
description, OpenGraph, hreflang. Every finding carries layer: "seo".
| Gate | Finding id | Rule | Severity |
|---|---|---|---|
image-alt |
seo.image-alt.missing |
An <img> has no alt attribute at all |
warning |
heading-order |
seo.heading-order.skipped |
The outline jumps more than one level deeper (e.g. h1 straight to h3) — only the first skip on the page is reported |
warning |
link-text |
seo.link-text.empty |
An <a href> has no accessible name (no text, aria-label, title, or alt on a contained <img>) |
warning |
link-text |
seo.link-text.generic |
An <a href>'s accessible name is a generic phrase ("click here", "read more", …) that conveys nothing on its own |
info |
lang |
seo.lang.missing |
<html> has no non-empty lang attribute |
error |
alt="" is never flagged, under any circumstance. An empty alt is the spec's own
way to mark an image decorative, and a check that fires on it would tell users to make
correct markup worse. role="presentation", role="none" and aria-hidden="true" are
honoured the same way. Only a missing alt attribute — not an empty one — is a finding.
lang is error, the strongest severity in this group, because a missing lang makes
a DIFFERENT gate lie. page.lang feeds bands.resolve_band(); with no lang, a page
written in a script with no word separators (Japanese, Thai, …) is measured against the
space-delimited English word band it structurally cannot meet, and
aeo.answer-block.length reports a length problem that does not exist.
link-text.generic is info and must never fail a build, because the word list
behind it (GENERIC_ANCHORS) is English-only by construction: on a Bengali or Japanese
page it matches nothing and the gate is silent by design, not because the page is clean.
Links inside <nav> are exempt from both link-text findings.
If more than 90% of at least 10 <lastmod> entries in sitemap.xml share one date,
that date is flagged as re-stamped on every build rather than reflecting real per-page
change. This runs automatically, alongside check_sitemap() (below), as of v0.2.1.
hygiene.py defines check_removed() (404s should be warnings, 5xx errors on unknown
slugs should be errors) and check_sitemap() (every sitemap URL should return 200).
check_sitemap() is called automatically from audit_site(), as of v0.2.1. For
every sitemap target _collect() could not already confirm reachable, it distinguishes a
redirecting entry (warning) from a genuinely dead one (error).
check_removed() is not wired in, and crawl-hygiene no longer exists as a
--fail-on gate name at all. It needs an explicit list of URLs your site used to serve
— no config field supplies that list, so audit_site() had no way to call it
automatically. Shipping crawl-hygiene as a config-accepted gate name that could never
fire was its own kind of fabrication, so v0.2.1 removed it from the schema. Call the
function directly if you need it:
from omnirank.fetch import make_client
from omnirank.gates import hygiene
client = make_client()
findings = hygiene.check_removed(client, ["https://example.com/old-page"])The status policy both functions apply:
| Situation | Correct response | Result |
|---|---|---|
| Page moved, modern equivalent exists |
301/308 to the equivalent |
pass |
| Page removed, no equivalent | 410 Gone |
pass |
| Unknown slug on a dynamic route |
308 to the section hub |
pass |
Anything returning 404 (check_removed, manual) |
— | warning (seo.crawl-hygiene.not-found) |
Anything returning 5xx (check_removed, manual) |
— | error (seo.crawl-hygiene.server-error) |
| A sitemap URL that redirects (automatic since v0.2.1) | — | warning (seo.sitemap-health.redirect) |
| A sitemap URL that is dead (automatic since v0.2.1) | — | error (seo.sitemap-health.dead-url) |
A 5xx on an unknown slug most often means a dynamic route shipped with
dynamicParams = false in a Next.js App Router project.
Verified directly against scripts/py/omnirank/report.py:
ERROR_COST = 10
WARNING_COST = 3
GATE_CAP = 15Per gate, then per layer, normalised by the layer's own surface (v0.4.0). Each GATE's
contribution to its layer is capped first — min(GATE_CAP, 10*errors + 3*warnings) — and
those capped costs are summed and divided by the layer's own scoring surface, not
subtracted from a flat 100:
surface = max(1, scoring_gate_count(layer), gates_actually_seen)
denominator = GATE_CAP * surface
penalty = (100 * capped_cost_sum + denominator // 2) // denominator # round-half-up
score = max(0, 100 - penalty)This replaced a flat 100-point budget per layer, and the change is behavioural, not
cosmetic — scores from before and after v0.4.0 are not comparable. Under the old model,
one gate failing on every page of a site was capped at 15, same as now — but seven maxed
gates zeroed a layer whether that layer had seven gates or thirty. Every gate v0.4.0 added
would therefore have made saturation cheaper: seo's pre-v0.4.0 count of 16 gates took
44% of them maxed to floor the layer at 0; at the new count of 24 it would have taken only
29%. The flat budget also put an unreachable floor under small layers — security ships
only 2 scoring gates, so its worst possible score under a flat 100-point budget would have
been 70, no matter how broken those two gates were. Dividing by the layer's own surface
fixes both directions: one maxed gate always costs exactly 1/surface of the layer, a
layer floors to 0 only when every one of its registered gates is maxed, and a small
layer can still reach 0. scoring_gate_count(layer) (registry.py) excludes info-only
gates (the four security header gates) and unreachable gates (crawl-hygiene) from the
surface, because counting a gate that can never cost anything would put an artificial
floor under the score.
A real scoring bug this exact mechanism let through, and the fix that closed it. An
earlier version of this v0.4.0 change admitted every gate with a finding into the
scoring surface, including info-only ones — silently widening the denominator for free.
That meant a harmless info finding on a not-yet-seen gate could raise a layer's score:
verified directly against https://danluu.com, security scored 87 under the buggy
code and the correct value, 67, only after the fix — the identical error findings, the
identical header gates present the whole time. Report.score()'s own contract is that
adding a finding can never raise a score; a 40,000-trial randomised property test
(tests/test_report.py) now asserts exactly that, drawing findings from the real
registry. See Security-Layer#a-scoring-bug-worth-knowing-about for the full story.
Overall: the integer floor-division average of every layer's score —
sum(scores.values()) // len(scores). Not a rounded mean.
layers_run. seo and geo are marked run unconditionally at the start of
audit_site(). aeo, perf and security are only marked run after at least one
page was actually fetched — if zero pages parse, all three are absent from the score map
entirely, never a fabricated 100. A plain omnirank audit against a reachable site
reports all five layers; an overall divisor other than 5 is the signal at least one
layer never ran.
The unreachable-URL edge case, reproduced directly against v0.4.0:
$ python3 -m omnirank.cli audit https://example.com/nope-xyz
OmniRank 0.4.0 — https://example.com/nope-xyz
overall 70/100 geo 47 security 67 seo 96
1 URLs checked · 7 findings in 7 groups
NOT EVALUATED (7 gate(s) across 2 target(s) — see the JSON report for the reason enum)
robots-sitemap, site — https://example.com/nope-xyz [no-sitemap]
aeo, onpage, perf, security, seo — https://example.com/nope-xyz/ [page-unreachable]
Neither aeo, perf nor onpage appears in the score map — zero pages were parsed, so
none of the three ever touched any content. security does appear, at 67, even
though it too is listed in NOT EVALUATED for this URL: the per-page header/mixed-content
checks never ran, but check_https_redirect() is a site-level probe that ran anyway and
found a real problem, and that genuine finding reaches the score map on its own merits
regardless of layers_run. Read seo.page.unreachable as the signal that the whole run
is unreliable, regardless of what the other layers show.
Running the CLI against https://example.com with no config — real output, captured on
v0.4.0:
OmniRank 0.4.0 — https://example.com
overall 74/100 aeo 71 geo 47 perf 100 security 67 seo 88
1 URLs checked · 17 findings in 17 groups
-
seo 88: missingrel=canonical, missing meta description,seo.schema.absentandseo.sitemap.missing— four distinct error gates,10*4 = 40raw — plusog.missing(warning,3raw) on a fifth gate. Cost43overseo's 24-gate surface (denominator360):(100*43 + 180) // 360 = 12,100 - 12 = 88. -
aeo 71: missing answer block (error) and fewer than 3 FAQ pairs (warning) — two distinct gates,13cost, overaeo's 3-gate surface (denominator45):(100*13 + 22) // 45 = 29,100 - 29 = 71. -
geo 47:llms.txt,llms-full.txt,facts.json, androbots.txtall 404 — four distinct gates,40cost, overgeo's 5-gate surface (denominator75):(100*40 + 37) // 75 = 53,100 - 53 = 47. -
perf 100: a small response with no render-blocking scripts and a fast full-response time — zero findings,100 - 0 = 100. -
security 67: one error,security.https-redirect.missing—min(GATE_CAP, 10) = 10cost, oversecurity's 2-gate surface (denominator30):(100*10 + 15) // 30 = 33,100 - 33 = 67. The four header-missing findings are allinfoand change nothing. -
overall 74: the average is over five layers, not four —(88 + 71 + 47 + 100 + 67) // 5 = 373 // 5 = 74.
Work the list by fixing every [FAIL] (error) before any [WARN] (warning) — errors
carry roughly 3x the score weight of warnings per gate, and reference id, not just
gate, when tracking one specific finding across runs.
python3 -m omnirank.cli audit --config omnirank.config.json --fail-on h1 canonical schemaExit code becomes 1 if any of h1, canonical, or schema has an error-severity
finding. See CI-Recipes for full workflow examples and which gates are safe to gate a
build on.
Every finding also carries a fixTier (mechanical, templated, drafted, advisory
or infrastructure), a static property of the finding id declared in
scripts/py/omnirank/registry.py for all 67 ids. It replaced autoFixable, which
recorded which module a finding lived in rather than whether fixing it was safe. Only the
four mechanical ids can produce a diff today. See Fix-Tiers-and-Applicability and
Fix-Preview.
- Quick-Start — install and run your first audit
-
Security-Layer — the new
securitylayer in full, including its honest limits - Contradictions — the new indexability-contradiction gates in full
-
Configuration-Reference — every
omnirank.config.jsonfield, includingaudit.sampleSizeandaudit.failOn - GEO-Artifacts-Skill — generating the artifacts the GEO layer checks for
- Report-Schema — the full finding shape and id convention
- Finding-Reference — every finding id, generated from the registry
-
CI-Recipes — wiring
--fail-oninto a real pipeline
OmniRank · maintained by S M Moshiur Rahman at TICON System Limited, Dhaka · Code MIT, docs CC BY 4.0 · v0.4.0 ships two skills plus the fix diff preview; the rest is Roadmap
One page. Every engine.
Start here
Skills
Fix preview
Reference
Learn
Project