Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ctaio-reads

A rubric-based curation pipeline for agentic-AI-tooling links. The model scores and summarises. Deterministic code owns fetching, validation, filtering, and publication.

Node.js, ES modules, zero dependencies. Anthropic Messages API over fetch.

Live output: ctaio.dev


The idea

Most LLM content pipelines let the model decide what ships. This one doesn't.

RUBRIC.md is loaded as the system prompt. The model applies that rubric to a fixed candidate set and returns a score, a category, and a one-line summary. That is the entirety of its authority. Every other step — what gets fetched, what counts as valid, what clears the threshold, what order it publishes in — is ordinary code with no model in the loop.

The model informs. The code decides.

Fail-closed, never repair

A response that fails validation is rejected and logged with a reason. It is never coerced into shape.

Repairing malformed model output is tempting and almost always wrong: once the parser has been loosened to accept bad output, you can no longer tell whether the model understood the task or whether your code hid the fact that it didn't. The failure gets buried exactly where you needed to see it.

Two layers enforce this.

Pipeline-level — hard exit. The run aborts rather than degrading:

  • ANTHROPIC_API_KEY unset
  • Any HN Algolia request returns non-2xx
  • Anthropic API returns non-2xx
  • stop_reason === "max_tokens" — a truncated response cannot be trusted, even if the prefix parses
  • Response body is not parseable JSON
  • Response parses but is not an array

Item-level — reject with a reason. Each element is checked independently; failures are collected, not thrown:

Check Rejection reason
Element is an object element is not an object
id appears in the candidate set id "…" is not in the candidate set (hallucinated item)
score is an integer 0–10 score … is not an integer between 0 and 10
category is in the allowlist category … is not in the allowed list
summary is non-empty summary is empty
summary ≤ 160 chars summary is N chars (max 160)
summary contains no URL summary contains "http"

The id check is the one that matters most. The model is handed a candidate set and asked to return one element per story using the id verbatim. Any id it returns that wasn't in that set is a fabricated item, and it is caught by set membership rather than by a judgement call.

Rejections are not discarded. They are written into reads.json alongside the published items, with the reason attached — so a run that is quietly degrading is visible in the output rather than silently thinner.

Pipeline

fetch candidates          HN Algolia · 8 queries · last 14 days · >20 points
   ↓                      dedup by objectID · cap 40
score                     ← model · one batched Anthropic call · RUBRIC.md as system prompt
   ↓
validate                  ← code · reject, never repair
   ↓
filter                    ← code · score >= 6
sort + cap                ← code · score desc · max 12
   ↓
write reads.json          published items + rejections + run counts
   ↓
render index.html

Fetch

Hacker News via the Algolia search_by_date endpoint, across eight queries — Claude Code, Cursor, Aider, MCP, agent SDK, AI coding agent, LLM evals, agentic workflow. Stories only, last 14 days, more than 20 points. Deduplicated by objectID, capped at 40 candidates.

Score

A single batched call to claude-sonnet-4-6. RUBRIC.md becomes the system prompt, followed by an explicit output contract: a bare JSON array, no prose, no markdown fences, first character [ and last character ]. Category must match the allowlist exactly. Summary one sentence, factual, no links. Score 0 if the story isn't about agentic AI coding tools, workflows, or infrastructure — however good it is otherwise.

The prompt asks for summaries under 140 characters; the validator rejects above 160. The tolerance is deliberate — instruct tighter than you enforce, so a marginal overrun doesn't cost an otherwise valid item.

Publish

Valid items scoring 6 or above, sorted by score descending, capped at 12. reads.json carries the run's counts — candidates fetched, passed validation, published — so the shape of each run is inspectable without re-running it.

Files

File Purpose
curate.mjs The pipeline. Fetch, score, validate, filter, publish.
RUBRIC.md Editorial rubric, loaded verbatim as the system prompt. Change this to change what ships.
reads.json Output: published items, rejections with reasons, and run counts.
index.html Renders the output.

Running it

Node 18+ (uses global fetch and top-level await). Nothing to install.

export ANTHROPIC_API_KEY=sk-ant-...
node curate.mjs

Writes reads.json in place. Open index.html to view.

Origin

Built and deployed inside a 45-minute timed assessment.

Partway through the build a malformed model response hit the validation path for real. The fast fix would have been to loosen the parser and accept the output. Instead I fixed it at the prompt layer — tightening the output contract so the model produced valid JSON, rather than teaching the code to tolerate invalid JSON.

That decision is the reason the repo is worth reading. Under time pressure, the fail-closed rule held.

About

Rubric-based LLM evaluation pipeline. Model scores and drafts; deterministic code owns validation, filtering, and publication. Fail-closed — rejections logged, never repaired.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages