BUILD GATES - SOURCE EXTRACTS ================================================================== Captured (UTC): 2026-09-26T02:31:33Z Repository: allspark-seo Commit: 23072668d9862ae4dbb3af88c0c0502b8dec0d09 Source: build.py These are the gates as they exist in the generator, copied from source rather than described. Failures stop the build and leave the previous output directory in place. Warnings are printed and the build continues. ------------------------------------------------------------------ 1. PRICING (build.py, run_gates) ------------------------------------------------------------------ # Vague promotional pricing language: always a hard failure. BANNED = re.compile(r"\baffordable\b|\bcheap(est)?\b|\bbudget-friendly\b" r"|\bbargain\b|\blow-cost\b|\bbudget\b", re.I) # A concrete figure. Allowed, but only when verified and dated. PRICE = re.compile(r"\$\s?\d[\d,]*(?:\.\d{2})?|\b\d+\s+dollars\b", re.I) PRICE_VERIFIED = re.compile(r"") PRICE_STALE_DAYS = 180 text = re.sub(r"", " ", body_html, flags=re.S) text = re.sub(r"<[^>]+>", " ", text) m = BANNED.search(text) if m: fail(f"cost rule violation - '{m.group(0)}'") pm = PRICE.search(text) if pm: stamp = PRICE_VERIFIED.search(body_html) if not stamp: fail(f"price '{pm.group(0)}' with no price-verified marker") elif age > PRICE_STALE_DAYS: warn(f"price verified {age} days ago, over the refresh window") Vague adjectives fail the build outright: they say nothing a reader can act on. A specific scoped figure ("the diagnostic visit is $99") is useful and is permitted, but it must carry an inline marker, and the marker stays in the rendered page source so anyone can see when a price was last checked. Verifications older than 180 days warn. An earlier version of this rule blocked every currency figure. That was too crude - it also blocked legitimate flat fees - but it is the version that stopped one of our own writers, and the log records it. See gate-log-cost-rule.txt for all four outcomes. ------------------------------------------------------------------ 2. CONTENT FLOOR (build.py, body_words + run_gates) ------------------------------------------------------------------ def body_words(html): """Word count of the ARTICLE, not the page.""" m = re.search(r"]*>(.*?)", html, re.S) seg = m.group(1) if m else html seg = re.sub(r'
]*>.*?
', " ", seg, flags=re.S) seg = re.sub(r'
]*>.*?
', " ", seg, flags=re.S) seg = re.sub(r"<(nav|footer|form)[^>]*>.*?", " ", seg, flags=re.S) return words(seg) if meta.get("type") in ("pillar", "problem", "city"): w = body_words(body_html) stub = meta.get("status") == "wireframe" if w < 350 and not stub: fail("N words, under the 350 absolute floor") elif w < 350: warn("wireframe stub - copy not written yet") elif w < 500: warn("under the tier3 target of 500") CORRECTION, and the reason this extract changed. Until commit 2307266 this gate called words(body_html), which counted the whole rendered document. Navigation, footer, CTA band, trust row and placeholder copy carried every page over the floor by themselves: a 64-word draft counted 2,355 words and passed. body_words() scopes the count to the article region and the same draft now counts 159 and fails. The gate did not work before that commit. See gate-log-thin-content.txt, which records both the failure and the defect that producing it exposed. Note the floor applies to pillar, symptom and city pages only. Technician bios (100-300) and project write-ups (150-400) have their own type minimums and are not measured against 350. ------------------------------------------------------------------ 3. TITLE DISCIPLINE (build.py, run_gates) ------------------------------------------------------------------ if not title: fail("empty title") elif len(title) > 70: fail("title N chars, over 70") elif len(title) > 60: warn("over the 60 target") if title in seen_titles: fail("duplicate title, also on {other}") Hard stop at 70, warning at 60, and no two pages may share a title. ------------------------------------------------------------------ 4. RETRIEVABILITY WITHOUT JAVASCRIPT (build.py, run_gates) ------------------------------------------------------------------ if len(text.encode("utf-8")) < 400: fail("under 400 bytes of body text without JS") if f"tel:{TEL}" not in body_html: fail("no tel: link") if "application/ld+json" not in body_html: fail("JSON-LD not in raw HTML") Most assistant crawlers do not execute JavaScript. A page that needs it is an empty page to them, so emptiness is a build error. ------------------------------------------------------------------ 5. SELF-COMPETITION (build.py, run_gates) ------------------------------------------------------------------ k = (meta.get("type"), tk) if k in seen_topics: fail(f"topicKey '{tk}' collides with {seen_topics[k]}") Two pages of the same type may not claim the same topic. This is the mechanical half of the service-vs-city cannibalisation fix. ------------------------------------------------------------------ 6. INTERNAL LINK INTEGRITY (build.py, link_gate) ------------------------------------------------------------------ for href in set(re.findall(r'href="(/[^"#]*)"', body_html)): if href.startswith("/") and href not in emitted: fail(f"internal link 404 -> {href}") Every internal href must resolve to a page this build emits. Caught 41 broken links on first run. ------------------------------------------------------------------ 7. ORPHAN DETECTION (build.py, v51_gates) ------------------------------------------------------------------ if n == 0: fail(f"ORPHAN: {path} has no inbound content links") elif n < 3: warn(f"{path}: {n} inbound content link(s), under 3") Zero inbound links from CONTENT is a failure; one or two is a warning. Nav and footer links are excluded from the count, or every page would pass trivially. ------------------------------------------------------------------ 8. ALT ATTRIBUTES (build.py, run_gates) ------------------------------------------------------------------ for img in re.findall(r"]*>", body_html): if "alt=" not in img: fail(" without alt attribute") Empty alt is valid on a decorative image. A missing alt attribute never is, so the check is for the attribute, not its contents. ------------------------------------------------------------------ NOT SHOWN ------------------------------------------------------------------ Placeholder-token lint, unfilled-slot detection (blocks production, warns on staging), DOM node and internal-anchor budgets, and page weight. Full set in build.py, function v51_gates().