You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit e9a1378
Browse filesBrowse the repository at this point in the historyBrowse files
feat(autoblog): gate drafts on quality, repair instead of discard, ship E-E-A-T fields (#196)
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>
// How many times we ask the model for a usable draft. Attempt 2 is a repair
929
+
// pass that receives the previous attempt's violations. Two is deliberate:
930
+
// the common failure is the model dropping one link or leaning on a filler
931
+
// phrase, which one round of specific feedback fixes. Beyond that the draft
932
+
// is usually wrong in a way more sampling will not fix, and each attempt
933
+
// costs a full long-form generation.
934
+
constMAX_GENERATION_ATTEMPTS=2;
935
+
936
+
/**
937
+
* Link-placement violations, phrased for the model rather than for a log line.
938
+
*
939
+
* Previously each of these called failKeyword() directly, which threw away a
940
+
* finished 4,000-word draft because one URL was missing from the body. They
941
+
* are now collected so the repair pass can fix them in place.
942
+
*/
943
+
exportfunctioncollectLinkViolations(
944
+
article: ArticleOutput,
945
+
offeredExchangeUrls: Set<string>,
946
+
): string[]{
947
+
constviolations: string[]=[];
948
+
949
+
constinternal=validateInternalLinks(
950
+
article.markdown_body,
951
+
article.used_internal_link_urls,
952
+
);
953
+
if(!internal.ok){
954
+
violations.push(
955
+
`You listed these URLs in used_internal_link_urls but did not actually place them in markdown_body: ${internal.missing.join(", ")}. Insert each one inline as [anchor](url) in a sentence genuinely about that page, or drop it from used_internal_link_urls.`,
`You listed these URLs in used_exchange_link_urls but did not place them in markdown_body: ${exchange.missing.join(", ")}. Insert each inline, or drop it from used_exchange_link_urls.`,
971
+
);
972
+
}
973
+
974
+
returnviolations;
975
+
}
976
+
977
+
/** Recent article bodies on this site, for the gate's duplication checks. */
"IMPORTANT — your previous attempt at this article was rejected by the pre-publish quality gate. Write the article again from scratch, keeping everything that was working, and fix every item below. Do not acknowledge this instruction in the output.",
1002
+
"",
1003
+
...violations.map((v,i)=>`${i+1}. ${v}`),
1004
+
].join("\n");
1005
+
}
1006
+
923
1007
exporttypeGenerateArticleResult={
924
1008
ok: boolean;
925
1009
articleId?: string;
@@ -1103,111 +1187,144 @@ export async function generateArticle(
1103
1187
);
1104
1188
}
1105
1189
1106
-
// Generate the article body.
1107
-
letarticle: ArticleOutput;
1108
-
try{
1109
-
constgenerated=awaitgenerateStructuredOutput({
1110
-
name: "lx_article",
1111
-
schema: ArticleSchema,
1112
-
system: buildSystemPrompt(),
1113
-
user: buildUserPrompt({
1114
-
site: typedSite,
1115
-
keyword: keyword.keyword,
1116
-
keywordMeta: {
1117
-
articleType: keyword.article_type,
1118
-
customInstructions: keyword.custom_instructions,
1119
-
},
1120
-
candidates,
1121
-
linkSlots,
1122
-
exchangeCandidates,
1123
-
exchangeSlots,
1124
-
exchangeRelaxed,
1125
-
}),
1126
-
anthropic,
1127
-
openai,
1128
-
anthropicModel: CLAUDE_MODEL,
1129
-
// 3,200–4,500 words ≈ ~18k–25k output tokens. JSON escape overhead
1130
-
// can push that another 30%. 48k gives meaningful headroom.
0 commit comments