storycode / story.py
Codex (via opencode)
Codex: prompt tuning, _loads fix, GitHub ingestion, lint cleanup
7603aa2
Raw
History Blame Contribute Delete
7.34 kB
"""Prompt construction + a deterministic fallback story.
Design rule: the model only ever **narrates facts we extracted**. Every prompt
hands it the structured truth (roles, symbols, edges) and forbids inventing
files or connections. The per-file MAP summaries are style-neutral (so they
cache across styles); the project REDUCE applies the chosen style + difficulty.
"""
from __future__ import annotations
import posixpath
import config
from schema import FileInfo, FileSummary, ProjectModel, ProjectStory, StorySection
_GROUNDING = (
"You are StoryCode. You explain code to people who are NOT programmers. "
"Only describe what the FACTS below state. Never invent files, functions, or "
"connections that aren't listed. If something isn't in the facts, don't mention it. "
"Use the file's actual function/class names to explain what it does — don't just "
"repeat the role category."
)
# --- MAP: per-file factual summary ------------------------------------------
def file_digest(model: ProjectModel, f: FileInfo) -> str:
role = config.ROLE_LABEL.get(f.role, f.role)
syms = ", ".join(s.name for s in f.symbols[:12]) or "(no named functions)"
dep_names = [posixpath.basename(d) for d in f.depends_on]
deps = ", ".join(dep_names) or "(nothing else in this project)"
used_by = sum(1 for o in model.files if f.path in o.depends_on)
return (
f"FILE: {f.path}\n"
f"Language: {f.lang}\n"
f"Its job-category: {role}\n"
f"Defines: {syms}\n"
f"It uses (depends on): {deps}\n"
f"Number of other files that depend on it: {used_by}\n"
f"Lines of code: {f.loc}"
)
def map_prompt(model: ProjectModel, f: FileInfo) -> list[dict]:
facts = file_digest(model, f)
user = (
f"{facts}\n\n"
"Write a JSON object with:\n"
'- "one_liner": at most 8 words naming this file\'s actual job. '
"Use the function/class names listed above to be specific — "
'e.g. "Defines the PPO actor-critic neural network" not just "The engine".\n'
'- "summary": 2-3 sentences, no jargon, explaining:\n'
" 1) What this file actually does (based on its function/class names)\n"
" 2) Which specific files it connects to and why\n"
"Speak to a non-coder who wants to understand the project."
)
return [{"role": "system", "content": _GROUNDING},
{"role": "user", "content": user}]
# --- REDUCE: the whole-project story ----------------------------------------
def project_digest(model: ProjectModel, summaries: dict[str, FileSummary]) -> str:
lines = [f"PROJECT: {model.name}",
f"Languages: {', '.join(model.languages) or 'mixed'}",
f"Front door (starts here): {', '.join(model.entry_points) or 'unknown'}",
""]
# Group files by role for better context
by_role: dict[str, list[FileInfo]] = {}
for f in model.files:
by_role.setdefault(f.role, []).append(f)
role_order = ["entry", "backend", "data", "config", "util", "frontend", "test", "other"]
for role in role_order:
group = by_role.get(role, [])
if not group:
continue
label = config.ROLE_LABEL.get(role, role)
lines.append(f"--- {label} ---")
for f in group:
base = posixpath.basename(f.path)
one = summaries.get(f.path)
job = one.one_liner if one else config.ROLE_LABEL.get(f.role, f.role)
deps = ", ".join(posixpath.basename(d) for d in f.depends_on) or "nothing internal"
lines.append(f"- {base}{job}. Uses: {deps}.")
lines.append("")
return "\n".join(lines)
def reduce_prompt(model: ProjectModel, summaries: dict[str, FileSummary],
style_key: str, difficulty_key: str) -> list[dict]:
style = config.STYLE_BY_KEY.get(style_key, config.STYLE_BY_KEY[config.DEFAULT_STYLE])
diff = config.DIFFICULTY_BY_KEY.get(difficulty_key,
config.DIFFICULTY_BY_KEY[config.DEFAULT_DIFFICULTY])
digest = project_digest(model, summaries)
user = (
f"{digest}\n\n"
f"STYLE: {style.voice}\n"
f"AUDIENCE: {diff.voice}\n\n"
"Write a JSON object with:\n"
'- "title": a short, inviting title for this project\'s story.\n'
'- "overview": 2-4 sentences explaining what this project IS (its purpose), '
"WHAT it does (the main capability), and HOW it works (the key architectural "
"flow from entry point through the files it uses). Be specific — use the "
"project name, the key files, and the real data flow.\n"
'- "steps": exactly 3-6 {"heading","body"} sections that follow the REAL '
"execution flow: start at the front door (entry point), then follow the "
"import chain through the files it uses. Each step should explain:\n"
" 1) What happens at this stage\n"
" 2) Which files are involved and what they do\n"
" 3) Why this step matters for the overall project\n"
"Apply the STYLE here. Don't skip intermediate files — the reader should "
"understand the complete journey from start to finish.\n"
'- "plain_overview": the SAME walkthrough but in flat, plain English with NO '
"metaphors (so a nervous user always has a literal version). 3-5 sentences. "
"This should be simple enough for a 12-year-old to understand.\n"
"Stay 100% true to the FILES facts above. Never invent connections."
)
return [{"role": "system", "content": _GROUNDING},
{"role": "user", "content": user}]
# --- Deterministic fallback (no model needed) -------------------------------
def plain_fallback_story(model: ProjectModel,
summaries: dict[str, FileSummary] | None = None) -> ProjectStory:
"""A truthful, model-free story built straight from the facts.
Used if the model endpoint is unavailable, so the app degrades gracefully
instead of showing nothing.
"""
summaries = summaries or {}
entry = model.entry_points[0] if model.entry_points else (
model.files[0].path if model.files else "")
steps: list[StorySection] = []
seen: set[str] = set()
def walk(path: str, depth: int):
f = model.by_path(path)
if f is None or path in seen or depth > 4:
return
seen.add(path)
base = posixpath.basename(path)
one = summaries.get(path)
job = one.summary if one else (
f"This is the {config.ROLE_LABEL.get(f.role, f.role).lower()} of the project.")
steps.append(StorySection(heading=base, body=job))
for dep in f.depends_on:
walk(dep, depth + 1)
if entry:
walk(entry, 0)
for f in model.files: # include anything not reachable from the entry
walk(f.path, 0)
langs = ", ".join(model.languages) or "code"
overview = (f"{model.name} is a {langs} project made of {len(model.files)} files. "
f"It starts at {posixpath.basename(entry) if entry else 'its main file'}, "
"which then uses the other files to do its work.")
return ProjectStory(title=f"The story of {model.name}", overview=overview,
steps=steps[:8], plain_overview=overview)