| """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." |
| ) |
|
|
|
|
| |
| 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}] |
|
|
|
|
| |
| 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'}", |
| ""] |
| |
| 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}] |
|
|
|
|
| |
| 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: |
| 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) |
|
|