"""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)