Every page here is also a document. Use your browser's print or save-as-PDF and you get the same PDF that goes out by email, which is why these are laid out like sheets of paper.

EZO Content Pipeline

What the rule checker checks, and where every rule came from

For: Qasim Humayun

From: Brent Bartosch
Date: 2026-08-05

You asked three things in Tuesday's call: how many checks there are, whether they are weighted equally, and which ones carry the most importance. All three are answered below. The second one has an answer you will not expect, so it is first.

How the score is actually calculated

Nothing is weighted. The score is the number of checks that passed divided by the number that ran, as a percentage. Every check counts exactly once. There are no weights anywhere in the code - pipeline/qa_gate/core.py:539 is the whole calculation:

score = round(100 * passed / total)

This matters because the call notes recorded it as "weights were AI-generated from shared documents, not yet validated." That is not the case. There are no weights to validate. Flat scoring was a deliberate choice, and whether it stays flat is a decision for you, not an assumption for me. See the decisions at the end.

Severity does exactly two things, and neither is weighting:

  1. It orders the findings list so the worst thing is at the top.
  2. A CRITICAL finding fails the page regardless of score. A page needs score >= 80 and zero CRITICAL findings to pass (core.py:541).

So a page can score 95/100 and still not pass. That is the only place importance is expressed today, and it is binary rather than graded.

The 80 threshold is not arbitrary: it maps to the engagement's own "80% first-pass QA" target.

How many checks: 63 across four page types

Page type Checks Severity spread
Blog 8 3 CRITICAL, 2 HIGH, 3 MEDIUM
Feature page 21 10 HIGH, 10 MEDIUM, 1 LOW
Comparison page 12 7 CRITICAL, 3 HIGH, 2 MEDIUM
Solution page 22 1 CRITICAL, 11 HIGH, 9 MEDIUM, 1 LOW

A page is only ever checked against its own page type's list. The counts differ because the team's documents specify different amounts for each: the feature and solution page templates are fully specified in the sprint SOP, while the blog workflow is still undocumented, so the blog list is deliberately short.

How well each rule is sourced, stated up front

Of the 63 checks, 11 carry a line-level citation in the code - the document name and line you can go read. The rest are covered by a statement recorded once per page type rather than on each rule, quoted in full in each section below.

Page type Line-level citations
Blog 7 of 8
Feature page 0 of 21
Comparison page 4 of 12
Solution page 0 of 22

This is worth saying plainly because it is the weakest part of the answer to your question. Blog and comparison rules were written against your documents and each one names its line. The feature and solution rules were written against the SOP's template tables, and the line numbers were recorded once for the whole page type instead of per rule. No value in this document was invented, but the two halves are not equally easy for you to audit, and you should know which is which before you spend time on it.

Blog

8 checks. 3 CRITICAL, 2 HIGH, 3 MEDIUM. Source: pipeline/qa_gate/blog_page.py.

7 of 8 carry a line-level citation naming the team document and line they came from.

The remaining 1 do not carry their own line reference. They are not invented - the values were read off the template tables in the document quoted below - but the line is recorded once for the whole page type instead of on each rule, so you cannot spot-check those rows the way you can the rest. Flagged here rather than left for you to find.

CANON, and what it is: - audit/data/docs/blog-qa-process.md (+ the older revision doc-2.md) is the team's Blog QA Process. It is a PROCESS document - a 3-the AI reviewer (Qwen -> Kimi -> Claude consolidation) plus a manual final-review checklist. It specifies NO page structure and NO character or element counts for blogs. - audit/data/docs/marketing-team-sprint-sops.md specifies Feature/Solution, Use Case and Industry page structures in detail. It contains NO blog section. Its "Rules for standardization" chapter (line 1474 onward) is the one page-type-agnostic part, and CTA rules there apply to any asset. - audit/data/docs/rule-book-qa-rubric.md (Qasim's rulebook) is about topical clusters, LLM citations and server-log retrieval measurement. It states no on-page blog rules at all - no meta title or description lengths, for blogs or anything else. - audit/data/docs/pmm-automation-wishlist.md line 24 puts "Create asset-specific workflows ... for blogs" at P0 in the team's OWN backlog: the blog workflow is, by their admission, not documented yet.

# Group Check What it looks at Reads Limit If it fails Where the rule comes from
1 page-level metadata: presence only, no invented ranges blog-h1-present the field is filled in seo.h1 - CRITICAL blog-qa-process.md:176 "Headings are descriptive and logically organized" (the article title is the page's H1); pmm-automation-wishlist.md:43 "Automate pre-publishing checks: Verify links, metadata, heading hierarchy ...", whose scope list at :56 includes "Corporate and product blogs". The SOP's 20-40 char H1 rule is stated only for feature pages (marketing-team-sprint-sops.md:64), so it is NOT applied here.
2 page-level metadata: presence only, no invented ranges blog-meta-title-present the field is filled in seo.meta_title - CRITICAL pmm-automation-wishlist.md:41 "Draft meta titles, meta descriptions ..." and :43 (metadata verified pre-publish). Lengths: the 50-60 / 145-160 ranges appear ONLY inside the feature (sops:64-65), use-case (:546-547) and industry (:927-928) tables - never for blogs - so presence is all the rule checker can honestly assert.
3 page-level metadata: presence only, no invented ranges blog-meta-description-present the field is filled in seo.meta_description - CRITICAL pmm-automation-wishlist.md:41 "Draft meta titles, meta descriptions ..." and :43 (metadata verified pre-publish). Lengths: the 50-60 / 145-160 ranges appear ONLY inside the feature (sops:64-65), use-case (:546-547) and industry (:927-928) tables - never for blogs - so presence is all the rule checker can honestly assert.
4 page-level metadata: presence only, no invented ranges blog-slug-format URL formatting, path prefix allowed seo.slug - MEDIUM Live convention, read from their own published URLs: every EZO blog sits under a /blog/ path segment - corporate blogs at ezo.io/blog// (kb/ezo.io/inventory.json, 1098 entries) and product blogs at ezo.io//blog// (audit/data/refresh_candidates.csv:2-12, audit/data/docs/hardware-brief-gold.txt:245-261). So a blog/-prefixed slug is valid, unlike under FEATURE_RULESET. No word count: live blog slugs run from 2 words to 20+.
5 structure blog-section-order sections present and in publish order whole page 2 sections HIGH SOP overall, no line recorded
6 structure blog-body-headings every item carries its heading body.blocks - HIGH blog-qa-process.md:176 "Headings are descriptive and logically organized" + :29 "Review only the complete article" (an empty body is not a complete article). Descriptiveness is a judgment call for the AI reviewer; presence of a heading per section is machine-checkable.
7 structure blog-internal-links-recorded the field was answered, even if the answer is empty body.internal_links - MEDIUM blog-qa-process.md:178 "Internal linking opportunities have been considered" + pmm-automation-wishlist.md:42. "Considered" is not "N links", so the check is that the draft RECORDS the consideration; an empty list is a valid answer, a missing key is not. It hangs off the article body because that is where the links live.
8 CTAs (page-type agnostic canon) cta CTA wording: sentence case, 2-5 words, nothing vague whole page - MEDIUM marketing-team-sprint-sops.md:1474-1560 "Rules for standardization > CTAs" - sentence case, 2-5 words on buttons, no vague labels - stated outside any page-type chapter; blog-qa-process.md:180 requires a clear CTA.

What the blog check deliberately does NOT check

These are omissions on purpose. Where the team's own documents are silent, no number was invented, because a number invented here would look like the team's rule forever.

Feature page

21 checks. 10 HIGH, 10 MEDIUM, 1 LOW. Source: pipeline/qa_gate/feature_page.py.

0 of 21 carry a line-level citation naming the team document and line they came from.

The remaining 21 do not carry their own line reference. They are not invented - the values were read off the template tables in the document quoted below - but the line is recorded once for the whole page type instead of on each rule, so you cannot spot-check those rows the way you can the rest. Flagged here rather than left for you to find.

Feature / Solution page ruleset, encoded from the Marketing Team Sprint SOPs.

The feature and solution page templates share the SOP's own table values, and the line numbers are recorded against the solution page ruleset (solution_page.py, quoted in the solution section below). Same numbers, recorded once:

CANON, and what the rule checker takes from it: - Template publish order and tables: 13 sections, H1 20-40 (QA checklist line 588), meta title 50-60 (:589), meta description 145-160 (:590), hero supporting heading 40-50 (:91), hero description 200-230 (:92, :595), 3 stats (:168), stats H2 50-70 (:164), 6 benefit cards (:213), benefits H2 50-60 (:209), 5-8 logos (:263), 4-6 workflow blocks (:313), 2-4 trust badges (:136), 2-3 testimonials (:368), 3-5 resources (:425), 8-10 FAQs (:474, :619), final CTA H2 30-50 (:543). - Section 4.2 competitive context (pmm-page-brief-process-v1.md:219): "solution pages can carry a light wedge (why this category, why us) without becoming a comparison page. Keep it to one to two sentences maximum." Encoded twice: the wedge field is capped at two sentences (HIGH), and a head-to-head comparison_table section on a solution draft is CRITICAL - that is the comparison page's defining section, and a solution draft that carries one has become a comparison page.

# Group Check What it looks at Reads Limit If it fails Where the rule comes from
1 Page-level SEO h1-length character count within range seo.h1 20-40 chars HIGH SOP overall, no line recorded
2 Page-level SEO meta-title-length character count within range seo.meta_title 50-60 chars MEDIUM SOP overall, no line recorded
3 Page-level SEO meta-title-separator meta title separator convention seo.meta_title - LOW SOP overall, no line recorded
4 Page-level SEO meta-description-length character count within range seo.meta_description 145-160 chars MEDIUM SOP overall, no line recorded
5 Page-level SEO slug-format URL word count and formatting seo.slug 3-6 words MEDIUM SOP overall, no line recorded
6 Section presence + order section-order sections present and in publish order whole page 13 sections HIGH SOP overall, no line recorded
7 Hero hero-supporting-heading character count within range hero.supporting_heading 40-50 chars MEDIUM SOP overall, no line recorded
8 Hero hero-description character count within range hero.description 200-230 chars MEDIUM SOP overall, no line recorded
9 Stats: exactly 3 stats-count how many items in the section stats.items 3-3 chars HIGH SOP overall, no line recorded
10 Stats: exactly 3 stats-h2 character count within range stats.h2 50-70 chars MEDIUM SOP overall, no line recorded
11 Benefits: exactly 6 cards benefits-card-count how many items in the section benefits.cards 6-6 chars HIGH SOP overall, no line recorded
12 Benefits: exactly 6 cards benefits-h2 character count within range benefits.h2 50-60 chars MEDIUM SOP overall, no line recorded
13 Trusted-by logo strip: 5-8 logo-count how many items in the section logos.items 5-8 chars HIGH SOP overall, no line recorded
14 Feature-detail workflows: 4-6 feature-block-count how many items in the section feature_detail.blocks 4-6 chars HIGH SOP overall, no line recorded
15 Feature-detail workflows: 4-6 feature-block-body-length feature block body length whole page 350-360 chars MEDIUM SOP overall, no line recorded
16 Trust badges: 2-4 trust-badge-count how many items in the section trust_badges.items 2-4 chars HIGH SOP overall, no line recorded
17 Testimonials: 2-3 testimonial-count how many items in the section testimonials.items 2-3 chars HIGH SOP overall, no line recorded
18 Resources: 3-5 resource-count how many items in the section resources.items 3-5 chars HIGH SOP overall, no line recorded
19 FAQs: 8-10 faq-count how many items in the section faqs.items 8-10 chars HIGH SOP overall, no line recorded
20 Final CTA final-cta-h2 character count within range final_cta.h2 30-50 chars MEDIUM SOP overall, no line recorded
21 CTAs across the page: sentence case, no banned labels, 2-5 words cta CTA wording: sentence case, 2-5 words, nothing vague whole page - MEDIUM SOP overall, no line recorded

Comparison page

12 checks. 7 CRITICAL, 3 HIGH, 2 MEDIUM. Source: pipeline/qa_gate/comparison_page.py.

4 of 12 carry a line-level citation naming the team document and line they came from.

The remaining 8 do not carry their own line reference. They are not invented - the values were read off the template tables in the document quoted below - but the line is recorded once for the whole page type instead of on each rule, so you cannot spot-check those rows the way you can the rest. Flagged here rather than left for you to find.

CANON, and what the rule checker takes from it: - Section 4.6 core principles: source-first, claim-by-claim verification, evidence discipline, honesty over aggression, externally safe. - Phase 5 (external-safety pass): every negative/competitor-gap claim must carry one validated label - "Not publicly documented", "Requires configuration / paid tier" (only if citable), or "Via integration" with the integration named - and scope must be qualified ("via CMDB", "some Elite setups") over absolute "they can't do X". - Evidence discipline: no claim ships without a citable source. Competitor numbers are quantitative claims; the citation markers accepted here are brief_gate's own (CITATION_RE / CLAIM_RE are imported, not re-invented, so page check and brief check cannot drift). - The fairness pass (Phase 7 "fairness"; the exemplar opens with "Snipe-IT is a solid, open-source ITAM tool" and carries a whole "Who Snipe-IT Is Right For (Be Honest)" section): at least one competitor strength is acknowledged. - The feature matrix ("Head-to-Head Comparison", the exemplar's 13-row capability table): a comparison page without one is not a comparison.

# Group Check What it looks at Reads Limit If it fails Where the rule comes from
1 page-level metadata: presence only, no invented ranges comparison-h1-present the field is filled in seo.h1 - CRITICAL Section 4.6 states no character ranges for comparison pages; the 20-40 / 50-60 / 145-160 ranges are feature/use-case/industry table values (marketing-team-sprint-sops.md) and are NOT applied here.
2 page-level metadata: presence only, no invented ranges comparison-meta-title-present the field is filled in seo.meta_title - CRITICAL Section 4.6 states no character ranges for comparison pages; the 20-40 / 50-60 / 145-160 ranges are feature/use-case/industry table values (marketing-team-sprint-sops.md) and are NOT applied here.
3 page-level metadata: presence only, no invented ranges comparison-meta-description-present the field is filled in seo.meta_description - CRITICAL Section 4.6 states no character ranges for comparison pages; the 20-40 / 50-60 / 145-160 ranges are feature/use-case/industry table values (marketing-team-sprint-sops.md) and are NOT applied here.
4 No live slug convention is documented for comparison pages; same honest treatment as blogs - lowercase hyphenated segments, path prefixes allowed, no invented word count. comparison-slug-format URL formatting, path prefix allowed seo.slug - MEDIUM SOP overall, no line recorded
5 Section 4.6 Phase 2 maps THE competitor; every safety scan below needs to know who the page is about. comparison-competitor-named the field is filled in competitor - CRITICAL SOP overall, no line recorded
6 structure comparison-section-order sections present and in publish order whole page 3 sections HIGH SOP overall, no line recorded
7 structure comparison-table ComparisonTable whole page - CRITICAL SOP overall, no line recorded
8 Section 4.6 safety and evidence rules comparison-negative-claim-labels NegativeClaimLabels whole page - CRITICAL SOP overall, no line recorded
9 Section 4.6 safety and evidence rules comparison-competitor-numbers-cited CompetitorNumbersCited whole page - CRITICAL SOP overall, no line recorded
10 Section 4.6 safety and evidence rules comparison-honesty-over-aggression HonestyOverAggression whole page - HIGH SOP overall, no line recorded
11 Section 4.6 safety and evidence rules comparison-competitor-strength CompetitorStrength whole page - HIGH SOP overall, no line recorded
12 CTAs (page-type agnostic canon) cta CTA wording: sentence case, 2-5 words, nothing vague whole page - MEDIUM marketing-team-sprint-sops.md "Rules for standardization > CTAs".

What the comparison page check deliberately does NOT check

These are omissions on purpose. Where the team's own documents are silent, no number was invented, because a number invented here would look like the team's rule forever.

Solution page

22 checks. 1 CRITICAL, 11 HIGH, 9 MEDIUM, 1 LOW. Source: pipeline/qa_gate/solution_page.py.

0 of 22 carry a line-level citation naming the team document and line they came from.

The remaining 22 do not carry their own line reference. They are not invented - the values were read off the template tables in the document quoted below - but the line is recorded once for the whole page type instead of on each rule, so you cannot spot-check those rows the way you can the rest. Flagged here rather than left for you to find.

CANON, and what the rule checker takes from it: - Template publish order and tables: 13 sections, H1 20-40 (QA checklist line 588), meta title 50-60 (:589), meta description 145-160 (:590), hero supporting heading 40-50 (:91), hero description 200-230 (:92, :595), 3 stats (:168), stats H2 50-70 (:164), 6 benefit cards (:213), benefits H2 50-60 (:209), 5-8 logos (:263), 4-6 workflow blocks (:313), 2-4 trust badges (:136), 2-3 testimonials (:368), 3-5 resources (:425), 8-10 FAQs (:474, :619), final CTA H2 30-50 (:543). - Section 4.2 competitive context (pmm-page-brief-process-v1.md:219): "solution pages can carry a light wedge (why this category, why us) without becoming a comparison page. Keep it to one to two sentences maximum." Encoded twice: the wedge field is capped at two sentences (HIGH), and a head-to-head comparison_table section on a solution draft is CRITICAL - that is the comparison page's defining section, and a solution draft that carries one has become a comparison page.

# Group Check What it looks at Reads Limit If it fails Where the rule comes from
1 page-level SEO: the shared Feature/Solution table values solution-h1-length character count within range seo.h1 20-40 chars HIGH SOP overall, no line recorded
2 page-level SEO: the shared Feature/Solution table values solution-meta-title-length character count within range seo.meta_title 50-60 chars MEDIUM SOP overall, no line recorded
3 page-level SEO: the shared Feature/Solution table values solution-meta-title-separator meta title separator convention seo.meta_title - LOW SOP overall, no line recorded
4 page-level SEO: the shared Feature/Solution table values solution-meta-description-length character count within range seo.meta_description 145-160 chars MEDIUM SOP overall, no line recorded
5 page-level SEO: the shared Feature/Solution table values solution-slug-format URL word count and formatting seo.slug 3-6 words MEDIUM SOP overall, no line recorded
6 section presence + publish order (template lines 5-29) solution-section-order sections present and in publish order whole page 13 sections HIGH SOP overall, no line recorded
7 hero (template hero table) solution-hero-supporting-heading character count within range hero.supporting_heading 40-50 chars MEDIUM SOP overall, no line recorded
8 hero (template hero table) solution-hero-description character count within range hero.description 200-230 chars MEDIUM SOP overall, no line recorded
9 stats: exactly 3 solution-stats-count how many items in the section stats.items 3-3 chars HIGH SOP overall, no line recorded
10 stats: exactly 3 solution-stats-h2 character count within range stats.h2 50-70 chars MEDIUM SOP overall, no line recorded
11 benefits: exactly 6 cards solution-benefit-card-count how many items in the section benefits.cards 6-6 chars HIGH SOP overall, no line recorded
12 benefits: exactly 6 cards solution-benefits-h2 character count within range benefits.h2 50-60 chars MEDIUM SOP overall, no line recorded
13 trusted-by logo strip: 5-8 solution-logo-count how many items in the section logos.items 5-8 chars HIGH SOP overall, no line recorded
14 workflow blocks (the template's feature-detail section): 4-6 solution-workflow-block-count how many items in the section feature_detail.blocks 4-6 chars HIGH SOP overall, no line recorded
15 trust badges: 2-4 solution-trust-badge-count how many items in the section trust_badges.items 2-4 chars HIGH SOP overall, no line recorded
16 testimonials: 2-3 solution-testimonial-count how many items in the section testimonials.items 2-3 chars HIGH SOP overall, no line recorded
17 resources: 3-5 solution-resource-count how many items in the section resources.items 3-5 chars HIGH SOP overall, no line recorded
18 FAQs: 8-10 solution-faq-count how many items in the section faqs.items 8-10 chars HIGH SOP overall, no line recorded
19 final CTA solution-final-cta-h2 character count within range final_cta.h2 30-50 chars MEDIUM SOP overall, no line recorded
20 Section 4.2 solution-specific rules solution-wedge-limit SolutionWedgeLimit whole page - HIGH SOP overall, no line recorded
21 Section 4.2 solution-specific rules solution-not-a-comparison SolutionNotAComparison whole page - CRITICAL SOP overall, no line recorded
22 CTAs (page-type agnostic canon) cta CTA wording: sentence case, 2-5 words, nothing vague whole page - MEDIUM SOP overall, no line recorded

What the solution page check deliberately does NOT check

These are omissions on purpose. Where the team's own documents are silent, no number was invented, because a number invented here would look like the team's rule forever.

The meta description question, worked through

You flagged a meta description at 162 characters showing as a "critical issue" against a 145-160 range, said it is not actually a critical SEO problem, and that you could not find the source of the range in your own documents. Three separate things there, and they have three different answers.

1. The range is real and cited, but it is not from your documents. It comes from the sprint SOP, at marketing-team-sprint-sops.md:64-65 for feature pages, :546-547 for use-case pages and :927-928 for industry pages, plus the solution page QA checklist at :590. That is why it is not in your files. It is part of the website standardisation canon, so whether it should apply at all is a conversation with Rubab rather than a code change.

2. It was never marked critical. A length overage produces a MEDIUM finding. The default severity for a character range check is MEDIUM (core.py:155), and CRITICAL fires only when the field is missing entirely. At 162 characters you would have seen MEDIUM. If something showed you the word critical, that is a presentation bug and I need to know which document or comment you were reading, because the check itself is behaving correctly.

3. Blogs already have no range at all. Blog meta descriptions are checked for presence only, deliberately, because no blog character counts exist in any team document. That is recorded in the blog omissions above.

Decisions I need from you

These are yours, not mine, and I would rather have them on the record than guess:

  1. Flat or weighted? Every check currently counts once. If you want some checks to matter more, that is a real change to how the score is calculated and I would want to scope it with you rather than pick weights myself. Picking them myself is how the call notes came to say the weights were AI-generated.
  2. Meta description range: keep, widen, or drop it? It is Rubab's canon, so this needs her too. If minor overages should not count against a page, the honest fix is to widen the range or lower the severity, not to leave a rule in place that everyone ignores.
  3. Which of these checks should be CRITICAL? Today CRITICAL is the only way a check can outrank the others, and the current assignment came from reading your documents, not from you. The comparison page list in particular has 7 of 12 as CRITICAL, which is worth your eyes.

One thing that is already answered

You asked which model runs the fan-out query and retrieval tree prompt, since the prompt was written for GPT. It runs on openai/gpt-5.2 (pipeline/fanout.py:71, pipeline/config.py:24) - the same lineage you wrote it for, routed through EZO's OpenRouter account rather than a separate OpenAI key.


Generated by scripts/build_rule_inventory.py on 2026-08-05. Every count, threshold, severity and citation in this document is read out of the live code at build time, so re-running it after a rule change produces a correct document rather than a stale one. Nothing here was typed by hand.