acorn / examples /hn_production_check.py
GitHub Actions
Deploy from GitHub 7caed70
ad6d452
"""Example: HN Production Readiness Checker
Analyzes a Hacker News story URL to determine whether the linked GitHub project
is production-ready or just a "cool demo." Checks maturity markers and mines
community comments for technical concerns.
"""
import base64
import os
import requests
from pydantic import BaseModel, Field
from acorn import Module, tool
from typing import Optional
# ---------------------------------------------------------------------------
# Schemas
# ---------------------------------------------------------------------------
class HNStoryInput(BaseModel):
story_url: str = Field(description="Show HN: story URL")
class ProductionReadinessReport(BaseModel):
grade: Optional[str] = Field(description="Production readiness grade: A, B, C, D, or F")
markers: Optional[str] = Field(description="Which maturity markers were found (LICENSE, CONTRIBUTING, tests, CI/CD) and which were missing")
community_sentiment: Optional[str] = Field(description="Brief characterization of community sentiment (e.g. 'cautiously optimistic') including the biggest technical concern surfaced in comments (or 'None surfaced' if positive)")
summary: str = Field(description="2-3 sentence narrative verdict covering the project maturity, and overall production readiness")
# ---------------------------------------------------------------------------
# Module
# ---------------------------------------------------------------------------
SYSTEM_PROMPT = """You are a software due-diligence analyst evaluating whether a Hacker News project is production-ready.
Workflow:
1. Parse the HN story ID from the URL (?id=...)
2. Call fetch_hn_story to get the story details and extract the GitHub URL
- If the story does not have a github project attached to it, exit early and mention that in summary
3. Parse owner/repo from the GitHub URL (handle trailing slashes and .git suffixes)
4. Explore the repository freely using list_folder and read_file. Use your judgement — good signals to look for:
- Licensing: LICENSE, LICENSE.md, COPYING, etc. in the root
- Contribution culture: CONTRIBUTING.md, CODE_OF_CONDUCT.md, detailed README
- Test discipline: test/, tests/, spec/, __tests__/ directories; look inside to gauge depth
- CI/CD: .github/workflows/, .circleci/, .travis.yml, Makefile with test targets
- Code quality: pyproject.toml / package.json / Cargo.toml for dependency pinning,
CHANGELOG or release tags, security policy (SECURITY.md)
- Red flags: TODO/FIXME density in source files, empty test files, no recent activity
You are not limited to these — read any file that helps you form a more accurate verdict.
5. Call fetch_hn_comments to gather community discussion
6. Scan comments for technical keywords: performance, security, bug, crash, memory, scale,
production, unstable, slow, CVE — surface the biggest concern (or "None surfaced")
7. Assign a Production Readiness Grade (A-F):
- A: strong maturity signals across the board, no critical concerns
- B: mostly solid, minor gaps or concerns
- C: notable gaps in quality signals, or community concerns worth flagging
- D: serious red flags in code quality, missing fundamentals, or critical community concerns
- F: no maturity signals whatsoever, or critical security/stability issues
8. Return the structured report:
- grade: letter grade
- markers: what you found and what was missing across licensing, tests, CI, contribution culture
- community_sentiment: overall tone plus the biggest technical concern (or "None surfaced")
- summary: 2-3 sentences covering the project's maturity, and production readiness verdict
"""
class HNProductionChecker(Module):
"""Determines if a trending HN project is production-ready or just a cool demo."""
system_prompt = SYSTEM_PROMPT
model = "anthropic/claude-sonnet-4-6"
temperature = 0.3
max_steps = 25
initial_input = HNStoryInput
final_output = ProductionReadinessReport
def __init__(self, github_token: str | None = None):
super().__init__()
self.github_token = github_token or os.environ.get("GITHUB_TOKEN")
def _github_headers(self) -> dict:
headers = {"Accept": "application/vnd.github+json"}
if self.github_token:
headers["Authorization"] = f"Bearer {self.github_token}"
return headers
@tool
def fetch_hn_story(self, story_id: int) -> dict:
"""Fetch details for a Hacker News story using the Algolia HN API.
Args:
story_id: The HN story ID parsed from the ?id= query parameter
Returns:
Dict with title, url (the GitHub URL), author, points, num_comments
"""
url = f"https://hn.algolia.com/api/v1/items/{story_id}"
response = requests.get(url, timeout=15)
response.raise_for_status()
data = response.json()
return {
"title": data.get("title", ""),
"url": data.get("url", ""),
"author": data.get("author", ""),
"points": data.get("points", 0),
"num_comments": data.get("num_comments", 0),
}
@tool
def list_folder(self, repo_path: str, folder_path: str | None = None) -> list[dict]:
"""List the contents of a folder in a GitHub repository.
Args:
repo_path: Repository in 'owner/repo' format (e.g. 'pallets/flask')
folder_path: Path within the repo to list (e.g. 'src/utils'). Pass None to list the root.
Returns:
List of dicts with 'name', 'type' ('file' or 'dir'), and 'size' (bytes, 0 for dirs)
"""
path = folder_path.strip("/") if folder_path else ""
url = f"https://api.github.com/repos/{repo_path}/contents/{path}"
r = requests.get(url, headers=self._github_headers(), timeout=15)
r.raise_for_status()
return [
{"name": entry["name"], "type": entry["type"], "size": entry.get("size", 0)}
for entry in r.json()
]
@tool
def read_file(self, repo_path: str, file_path: str) -> str:
"""Read the contents of a file in a GitHub repository.
Args:
repo_path: Repository in 'owner/repo' format (e.g. 'pallets/flask')
file_path: Path to the file within the repo (e.g. 'README.md' or 'src/main.py')
Returns:
Decoded text content of the file
"""
url = f"https://api.github.com/repos/{repo_path}/contents/{file_path.strip('/')}"
r = requests.get(url, headers=self._github_headers(), timeout=15)
r.raise_for_status()
data = r.json()
return base64.b64decode(data["content"]).decode("utf-8", errors="replace")
@tool
def fetch_hn_comments(self, story_id: int, max_comments: int = 50) -> list[dict]:
"""Fetch top comments for a Hacker News story via the Algolia search API.
Args:
story_id: The HN story ID
max_comments: Maximum number of comments to return (default 50)
Returns:
List of dicts with 'author' and 'text' keys, filtered to non-empty comments
"""
url = "https://hn.algolia.com/api/v1/search"
params = {
"tags": f"comment,story_{story_id}",
"hitsPerPage": max_comments,
}
response = requests.get(url, params=params, timeout=15)
response.raise_for_status()
hits = response.json().get("hits", [])
return [
{"author": h.get("author", ""), "text": h.get("comment_text", "")}
for h in hits
if h.get("comment_text")
]