feat(autoblog): quality gate, repair-retry, and E-E-A-T delivery fields - #196
Merged
Merged
Conversation
…ip E-E-A-T fields generateArticle() validated exactly one thing — that the internal links the model claimed to place were physically present — and any failure marked the keyword 'failed', throwing away a finished 4,000-word draft. Everything else about the post shipped unread. Meanwhile this repo already ships a slop detector we point at other people's sites, and the autoblog SDK ships the heuristic gate a receiver applies to posts we deliver. Neither was ever aimed at our own output. - lib/lx/qualityGate.ts: runs the in-repo slop checks (filler, placeholders, misspellings, first-party evidence) plus the SDK's receiver heuristics against a draft, and adds the cross-article checks a single-page scan structurally cannot do — near-duplicate and repeated-opening detection against the site's recent posts. Deterministic, no LLM call. - articleGen: link validation and the gate now produce model-readable violations instead of terminal failures, and a rejected draft is regenerated once with those violations appended to the brief. Gating runs before image generation, so a rejected draft never pays for four gpt-image-2 calls. The accepted draft's score is stored on the row. - webhookDeliver: posts went out with author: null, no structured data, and both dates stamped with the delivery time — so a retry moved the article's publication date, and our own audit would flag the content we deliver for missing attribution. Now carries a configurable byline, BlogPosting JSON-LD, and dates from the row. Only the near-duplicate threshold differs from the audit's (0.55 vs 0.70): we are judging our own generator, where that much overlap already means the post competes with one we published last week. Typecheck clean; 1451 tests pass, 15 of them new. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ThreatCrush Security Scan35 finding(s) HIGH/CRITICAL: 5 | MEDIUM: 27 | LOW: 3
Snippets are redacted; ThreatCrush never prints matched credential material. |
ralyodio
marked this pull request as ready for review
August 13, 2026 13:40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Batch 1 of the autoblog quality work. Three self-contained changes to the generate → deliver path.
Why
generateArticle()validated exactly one thing — that the internal links the model claimed to place were physically present — then wrote the row atstatus='ready'. Everything else about the draft shipped unread. Any failure calledfailKeyword, permanently consuming the keyword and discarding a finished 4,000-word draft over one missing URL.Meanwhile the repo already ships a slop detector we point at other people's sites (
lib/audit/checks/slop.ts), and the autoblog SDK ships the heuristic gate a receiver applies to posts we deliver (@profullstack/autoblog/quality). Neither was ever aimed at our own output.What changed
lib/lx/qualityGate.ts(new) — runs both existing gates against a draft, plus the cross-article checks a single-page scan structurally cannot do:Deterministic — no LLM call. The SDK's
scoreQuality()would add one, but every signal here is free and instant.lib/lx/articleGen.ts— link validation and the gate now produce model-readable violations rather than terminal failures, and a rejected draft is regenerated once with those violations appended to the brief. Gating runs before image generation, so a rejected draft never pays for fourgpt-image-2"high"-tier calls. The accepted draft's score is stored on the row.lib/lx/webhookDeliver.ts— posts went out withauthor: null, no structured data, and both dates stamped withnow()at delivery time. Two consequences: a retry silently moved the article's publication date, and our own audit (content.author,content.date_signal) would flag the content we deliver on the customer's own domain. Now carries a configurable byline,BlogPostingJSON-LD, and dates read from the row.Notes for review
htmlrather than as a newPostfield — that shape is shared by four Profullstack consumers, and a receiver that already rendershtmlpicks this up with no change.Verification
npx tsc --noEmit— cleannpx vitest run— 1451 passed, 0 failures (15 new)The repo has no working lint setup: the
lintscript callsnext lint, which Next 15 removed, and there's noeslint.config.*. Not addressed here.Not in this PR
The larger items from the analysis: SERP-grounded research so articles carry real citations (the system prompt currently instructs the model to hedge instead of cite, and nothing in
lib/lx/ever supplies sources), and breaking the structural/phrase sameness the system prompt hard-codes into every post on a site.Incidental finding:
stripInPageAnchorLinksinarticleGen.tsis dead code — never called. Its premise is also stale; the SDK's link counter already skipshref="#…", so the table of contents was never counting against link density. Left in place to keep this diff focused.🤖 Generated with Claude Code