storycode / config.py
Codex (via opencode)
Codex: prompt tuning, _loads fix, GitHub ingestion, lint cleanup
7603aa2
Raw
History Blame Contribute Delete
7.92 kB
"""Central configuration: env, story styles, difficulty levels, file filters, palette.
StoryCode reads a non-coder's project and explains it as a story. The values here
are deliberately plain-language: labels a non-technical person sees should never
contain jargon unless we explain it.
"""
from __future__ import annotations
import os
from dataclasses import dataclass
from dotenv import load_dotenv
load_dotenv()
# --- Runtime / services -----------------------------------------------------
# The model runs on Modal (vLLM, OpenAI-compatible API). The Space is CPU-only.
MODAL_ENDPOINT_URL = os.getenv("MODAL_ENDPOINT_URL", "").rstrip("/")
MODAL_API_KEY = os.getenv("MODAL_API_KEY", "")
MODEL_ID = os.getenv("MODEL_ID", "openbmb/MiniCPM4.1-8B")
DB_PATH = os.getenv("STORYCODE_DB_PATH", "data/storycode.db")
USE_ZEROGPU_FALLBACK = os.getenv("USE_ZEROGPU_FALLBACK", "0") == "1"
# Generation limits (keep the Space responsive; map-reduce stays well under 32k ctx).
LLM_TIMEOUT_S = int(os.getenv("LLM_TIMEOUT_S", "90"))
MAX_FILES = int(os.getenv("STORYCODE_MAX_FILES", "60")) # analysed per project
MAX_FILE_BYTES = int(os.getenv("STORYCODE_MAX_FILE_BYTES", str(200_000)))
MAX_TOTAL_BYTES = int(os.getenv("STORYCODE_MAX_TOTAL_BYTES", str(5_000_000)))
# --- Languages we understand (stated honestly in the UI) --------------------
# Extension -> language key. Anything else is listed but not deeply parsed.
LANG_BY_EXT: dict[str, str] = {
".py": "python",
".js": "javascript", ".jsx": "javascript",
".ts": "typescript", ".tsx": "typescript", ".mjs": "javascript", ".cjs": "javascript",
".html": "html", ".htm": "html",
".css": "css", ".scss": "css",
".json": "json",
".yaml": "yaml", ".yml": "yaml",
".md": "markdown",
}
# Directories and files we never analyse (noise / not the user's own code).
IGNORE_DIRS: frozenset[str] = frozenset({
"node_modules", ".git", ".svn", "__pycache__", ".venv", "venv", "env",
"dist", "build", ".next", ".nuxt", "out", "target", ".idea", ".vscode",
"coverage", ".pytest_cache", ".mypy_cache", "site-packages", ".cache",
"vendor", "bower_components", ".gradio",
})
IGNORE_FILES: frozenset[str] = frozenset({
"package-lock.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock",
".ds_store", "thumbs.db",
})
# Binary-ish extensions to skip outright.
BINARY_EXTS: frozenset[str] = frozenset({
".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".svg", ".pdf", ".zip",
".gz", ".tar", ".mp3", ".mp4", ".wav", ".woff", ".woff2", ".ttf", ".otf",
".pyc", ".so", ".dll", ".exe", ".bin", ".db", ".sqlite", ".lock",
})
# Manifest files the dependency explainer reads.
DEP_MANIFESTS: frozenset[str] = frozenset({
"requirements.txt", "package.json", "pyproject.toml", "pipfile",
})
# --- Roles -------------------------------------------------------------------
# A file's "role" drives the architecture-map grouping and the safe-to-edit
# verdict. These are plain-English buckets, not technical layers.
@dataclass(frozen=True)
class Role:
key: str
label: str
color: str # used in the diagram + chips
ROLES: tuple[Role, ...] = (
Role("entry", "Front door", "#4b5bd6"), # what runs first / users hit
Role("frontend", "What you see", "#3a7ca5"), # UI / pages / styles
Role("backend", "The engine", "#6b4fb0"), # logic / services / API calls
Role("data", "Memory", "#3f9b6b"), # db / storage / models / embeddings
Role("config", "Settings", "#8a7a66"), # config / env / manifests
Role("test", "Safety checks", "#9aa0a6"), # tests
Role("util", "Helpers", "#b0793a"), # shared utilities
Role("other", "Other", "#9aa0a6"),
)
ROLE_KEYS: tuple[str, ...] = tuple(r.key for r in ROLES)
ROLE_LABEL = {r.key: r.label for r in ROLES}
ROLE_COLOR = {r.key: r.color for r in ROLES}
# Filename / path hints used by analyzer.graph.classify_role (first match wins).
ENTRY_NAMES: frozenset[str] = frozenset({
"app.py", "main.py", "manage.py", "server.py", "run.py", "wsgi.py", "asgi.py",
"index.js", "index.ts", "index.jsx", "index.tsx", "main.js", "main.ts",
"index.html", "app.js", "app.ts",
"train.py", "play.py", "demo.py", "infer.py", "evaluate.py", "test.py",
})
CONFIG_HINTS: tuple[str, ...] = (
"config", "settings", ".env", "requirements", "package.json", "pyproject",
"dockerfile", "makefile", ".yaml", ".yml", ".toml", ".ini",
)
DATA_HINTS: tuple[str, ...] = (
"db", "database", "model", "schema", "store", "embed", "vector",
"index", "ingest", "migration", "seed",
)
TEST_HINTS: tuple[str, ...] = ("test", "spec", "__tests__")
UTIL_HINTS: tuple[str, ...] = ("util", "helper", "common", "lib", "shared", "tools")
# --- Story styles ------------------------------------------------------------
# Simple Walkthrough is the DEFAULT (per the brief: creative styles can feel
# patronising; they're opt-in). Each carries a short voice instruction handed to
# the model, but the model only ever narrates facts we extracted.
@dataclass(frozen=True)
class Style:
key: str
label: str
emoji: str
voice: str # instruction injected into the narration prompt
STYLES: tuple[Style, ...] = (
Style("plain", "Simple Walkthrough", "πŸ“‹",
"Explain plainly and warmly, step by step, no metaphors, no jargon. "
"Like a patient friend who read the whole project for them."),
Style("kids", "Kids Book", "🧸",
"Tell it like a children's picture book. Each file is a friendly "
"character with a job. Keep sentences short and gentle."),
Style("thriller", "Thriller", "πŸ•΅οΈ",
"Tell it like a suspense thriller β€” the request arrives, tension "
"builds as it moves through the code β€” but every claim must stay true."),
Style("news", "News Report", "πŸ“°",
"Report it like a breaking-news anchor: who did what, in what order, "
"with punchy headlines. Accurate, never sensational about facts."),
Style("recipe", "Recipe", "🍳",
"Explain it like a cooking recipe: ingredients (the pieces) and steps "
"(the flow), in order."),
)
STYLE_KEYS: tuple[str, ...] = tuple(s.key for s in STYLES)
STYLE_BY_KEY = {s.key: s for s in STYLES}
DEFAULT_STYLE = "plain"
@dataclass(frozen=True)
class Difficulty:
key: str
label: str
voice: str
DIFFICULTIES: tuple[Difficulty, ...] = (
Difficulty("kid", "Explain like I'm 5",
"Use no technical terms at all. Everyday words a child knows."),
Difficulty("teen", "Teenager",
"Light technical terms are fine if you explain each one in the "
"same sentence."),
Difficulty("adult", "Adult / curious beginner",
"Full clarity. Technical terms are okay but define them the first "
"time they appear."),
)
DIFFICULTY_KEYS: tuple[str, ...] = tuple(d.key for d in DIFFICULTIES)
DIFFICULTY_BY_KEY = {d.key: d for d in DIFFICULTIES}
DEFAULT_DIFFICULTY = "teen"
# --- Where did your code come from? (onboarding) ----------------------------
SOURCE_HINTS: tuple[str, ...] = (
"Claude / Claude Code", "Cursor", "ChatGPT", "Bolt / v0 / Lovable",
"A ZIP someone sent me", "Not sure",
)
# --- Visual palette (mirrors ui/styles.css; kept here for diagram + chips) ---
PALETTE = {
"paper": "#f7f4ef",
"ink": "#1d2430",
"accent": "#4b5bd6",
"safe": "#3f9b6b",
"careful": "#d8932f",
"danger": "#c2503f",
"muted": "#6b7280",
"line": "#e7e3da",
}
# Safe-to-edit verdicts (computed in analyzer.graph, never by the model).
SAFE = "safe"
CAREFUL = "careful"
DANGER = "danger"
SAFETY_COLOR = {SAFE: PALETTE["safe"], CAREFUL: PALETTE["careful"], DANGER: PALETTE["danger"]}
SAFETY_EMOJI = {SAFE: "🟒", CAREFUL: "🟠", DANGER: "πŸ”΄"}
SAFETY_LABEL = {SAFE: "Safe to change", CAREFUL: "Change carefully", DANGER: "Don't touch"}