From 2ba61f1aba22d338f611bf14782a2acf414cbc0c Mon Sep 17 00:00:00 2001 From: shilpijc Date: Sun, 2 Aug 2026 22:22:58 +0530 Subject: [PATCH 1/2] Add Graphify-backed repo explainer mode. Wire `pr-explainer repo` to map a local checkout via Graphify and write a profile-calibrated plain-language summary with the same quiz flow as PR mode. Co-authored-by: Cursor --- .gitignore | 1 + README.md | 26 +++++++ scripts/test-repo-prompt.js | 105 +++++++++++++++++++++++++ src/cli.js | 145 ++++++++++++++++++++++++++-------- src/display.js | 49 ++++++++---- src/github.js | 51 ++++++++++++ src/graphify.js | 102 ++++++++++++++++++++++++ src/prompt.js | 151 ++++++++++++++++++++++++++++++++++++ src/repo-context.js | 108 ++++++++++++++++++++++++++ 9 files changed, 692 insertions(+), 46 deletions(-) create mode 100644 scripts/test-repo-prompt.js create mode 100644 src/graphify.js create mode 100644 src/repo-context.js diff --git a/.gitignore b/.gitignore index 65c5ca0..62f9bf8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ learning-profile.md .DS_Store LAUNCH.md DESIGN_BRIEF.md +graphify-out/ diff --git a/README.md b/README.md index 52798b0..4ff2f71 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,31 @@ pr-explainer 42 # current repo only pr-explainer https://github.com/some-org/some-repo/pull/42 # any repo pr-explainer some-org/some-repo#42 pr-explainer https://github.com/some-org/some-repo/pull/42 --no-quiz + +# Explain a whole local checkout (needs Graphify — see below) +pr-explainer repo +pr-explainer repo /path/to/checkout --no-quiz ``` +### Repo mode + +`pr-explainer repo` orients you to what a **repository** does — for non-engineers +or engineers outside that domain — using the same learning profile as PR mode. + +It builds a local [Graphify](https://graphify.com/) knowledge graph of the +checkout (structure, hubs, communities), combines that with recent merged PRs +and your profile, and writes a concise plain-language explainer. Output lands +in `~/.pr-explainer/explainers/repos/`. + +Requires a **local git checkout** with a GitHub remote (not a bare `owner/repo` +URL yet), plus Graphify: + +```bash +uv tool install graphifyy # or: pipx install graphifyy +``` + +If `graphify` is missing, repo mode exits with install instructions. + > **Note:** a bare number resolves against the GitHub repo of your current > directory. To explain a PR elsewhere, pass the full URL or `owner/repo#N`. > Only **merged** PRs are supported. @@ -67,6 +90,8 @@ once to log in if you haven't. Also requires the [GitHub CLI](https://cli.github.com/) (`gh`), authenticated (`gh auth login`). +**Repo mode** additionally requires [Graphify](https://graphify.com/docs) +(`uv tool install graphifyy`). ## GitHub Action (optional) The CLI is the main way to use this — point it at any PR, any time. The @@ -147,6 +172,7 @@ machine except what `claude` itself sends. | Claude Code CLI not found | Install from https://claude.com/claude-code and run `claude` once to log in | | `gh` auth / forbidden errors | Run `gh auth login` | | GitHub CLI not found | Install from https://cli.github.com/ | +| Graphify CLI not found / repo mode | Install with `uv tool install graphifyy`, then retry `pr-explainer repo` | Quick sanity checks: diff --git a/scripts/test-repo-prompt.js b/scripts/test-repo-prompt.js new file mode 100644 index 0000000..373d234 --- /dev/null +++ b/scripts/test-repo-prompt.js @@ -0,0 +1,105 @@ +#!/usr/bin/env node +/** + * Dev helper for iterating on buildRepoPrompt without the full CLI. + * Prefer: node src/cli.js repo --no-quiz + * Kept for prompt experiments (see issue #13). + */ +import { readFile, writeFile, mkdir } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import path from "node:path"; +import os from "node:os"; +import { fileURLToPath } from "node:url"; +import { buildRepoPrompt } from "../src/prompt.js"; +import { getRepoIdentity, getRecentMergedPRs } from "../src/github.js"; +import { runClaude } from "../src/claude.js"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.join(__dirname, ".."); + +function truncate(text, max, label) { + if (!text || text.length <= max) return text || ""; + return text.slice(0, max) + `\n\n... (${label} truncated at ${max} chars)`; +} + +function formatRecentPrs(prs, budget = 7000) { + if (!prs.length) return "(no recent merged PRs found)"; + const parts = []; + let used = 0; + for (const pr of prs) { + const body = truncate((pr.body || "").trim() || "(no description)", 600, `PR #${pr.number}`); + const block = `#${pr.number} ${pr.title}\n${body}`; + if (used + block.length > budget && parts.length) break; + parts.push(block); + used += block.length + 2; + } + return parts.join("\n\n"); +} + +async function godNodesSummary(repoRoot) { + try { + const { stdout } = await execFileAsync( + "graphify", + ["god-nodes", "--top", "10", "--graph", path.join(repoRoot, "graphify-out/graph.json")], + { cwd: repoRoot } + ); + return stdout.trim(); + } catch (err) { + return `(god-nodes unavailable: ${err.message})`; + } +} + +async function loadProfile() { + const candidates = [ + process.env.LEARNING_PROFILE, + path.join(ROOT, "learning-profile.md"), + path.join(os.homedir(), ".pr-explainer/learning-profile.md"), + path.join(ROOT, "templates/learning-profile.example.md"), + ].filter(Boolean); + for (const p of candidates) { + if (existsSync(p)) return { path: p, text: await readFile(p, "utf8") }; + } + throw new Error("No learning profile found"); +} + +async function main() { + const reportPath = path.join(ROOT, "graphify-out/GRAPH_REPORT.md"); + if (!existsSync(reportPath)) { + throw new Error(`Missing ${reportPath}. Run: graphify update .`); + } + + const profile = await loadProfile(); + const identity = await getRepoIdentity(ROOT); + const prs = await getRecentMergedPRs(ROOT, 8); + const report = truncate(await readFile(reportPath, "utf8"), 20_000, "GRAPH_REPORT"); + const graphSummary = truncate(await godNodesSummary(ROOT), 4_000, "god-nodes"); + + const prompt = buildRepoPrompt({ + profile: profile.text, + identity, + graphifyReport: report, + graphSummary, + recentPrs: formatRecentPrs(prs), + }); + + const outDir = path.join(ROOT, "graphify-out"); + await mkdir(outDir, { recursive: true }); + const promptPath = path.join(outDir, "repo-prompt-test.txt"); + await writeFile(promptPath, prompt, "utf8"); + console.error(`Wrote prompt (${prompt.length} chars) → ${promptPath}`); + console.error(`Profile: ${profile.path}`); + console.error("Calling claude…"); + + const entry = await runClaude(prompt); + const resultPath = path.join(outDir, "repo-explainer-sample.md"); + await writeFile(resultPath, entry + "\n", "utf8"); + console.error(`Wrote explainer → ${resultPath}`); + console.log(entry); +} + +main().catch((err) => { + console.error(`Error: ${err.message}`); + process.exitCode = 1; +}); diff --git a/src/cli.js b/src/cli.js index 98448cd..13ff873 100755 --- a/src/cli.js +++ b/src/cli.js @@ -5,10 +5,11 @@ import path from "node:path"; import os from "node:os"; import { fileURLToPath } from "node:url"; import { getPR, getPRDiff } from "./github.js"; -import { buildPrompt } from "./prompt.js"; +import { buildPrompt, buildRepoPrompt } from "./prompt.js"; import { runClaude } from "./claude.js"; import { printExplainerSummary } from "./display.js"; import { runInteractiveQuiz } from "./quiz.js"; +import { gatherRepoContext } from "./repo-context.js"; const MAX_DIFF_CHARS = 60_000; const CONFIG_DIR = path.join(os.homedir(), ".pr-explainer"); @@ -27,28 +28,30 @@ const TEMPLATE_PATH = path.join( function usage() { console.error( `Usage: pr-explainer [--no-quiz] + pr-explainer repo [path] [--no-quiz] pr-explainer init [--force] -Explains a merged pull request in plain language, tailored to your -learning profile — however technical or non-technical you are, and -whether or not you wrote the PR yourself. Ends with a multiple-choice -Quick check so it's something you retain, not just read. - -After the explainer is saved, a readable summary (title, Ships, What -changed, Why it was done this way, Why it matters) prints to stderr. -In an interactive terminal: Press Enter, then CHECK IT STUCK quiz -(a/b/c or 1/2/3, Enter to skip, q to quit), then optionally open the -saved file. Pass --no-quiz to skip the quiz (also skipped in CI / -non-TTY). Summary still prints unless PR_EXPLAINER_QUIET=1. +Explains merged pull requests — or a whole repository — in plain language, +tailored to your learning profile. Ends with a multiple-choice Quick check +so it's something you retain, not just read. PR a PR number ("42"), a PR URL, or "owner/repo#42" (a bare number resolves against the repo in your current directory) + repo explain what a local git checkout does (cwd, or path to a checkout). + Uses Graphify to map the code, then writes a reader-pitched summary. init create ~/.pr-explainer/learning-profile.md from the template (use --force to overwrite an existing profile) -Requires the Claude Code CLI ("claude") installed and logged in -(subscription or API key — whatever you already use for \`claude\`), and -the GitHub CLI ("gh") authenticated. +After the explainer is saved, a readable summary prints to stderr. +In an interactive terminal: Press Enter, then CHECK IT STUCK quiz +(a/b/c or 1/2/3, Enter to skip, q to quit), then optionally open the +saved file. Pass --no-quiz to skip the quiz (also skipped in CI / +non-TTY). Summary still prints unless PR_EXPLAINER_QUIET=1. + +Requires: + - Claude Code CLI ("claude") installed and logged in + - GitHub CLI ("gh") authenticated + - For repo mode: Graphify CLI ("graphify") — uv tool install graphifyy Profile lookup (first hit wins): 1. LEARNING_PROFILE env @@ -144,6 +147,14 @@ function slugify(title) { .slice(0, 60); } +function repoSlug(identity, repoRoot) { + const raw = + identity?.nameWithOwner || + identity?.name || + path.basename(repoRoot); + return slugify(String(raw).replace(/\//g, "-")) || "repo"; +} + async function appendToIndex(outDir, { filename, title, pr }) { const indexPath = path.join(outDir, "index.md"); if (!existsSync(indexPath)) { @@ -160,25 +171,27 @@ async function appendToIndex(outDir, { filename, title, pr }) { await appendFile(indexPath, row, "utf8"); } -async function main() { - const { flags, positionals } = parseArgs(process.argv.slice(2)); - const command = positionals[0]; - - if (flags.help || !command) { - usage(); - process.exitCode = flags.help ? 0 : 1; - return; - } - - if (command === "init") { - await initProfile(flags.force); - return; +async function appendToRepoIndex(outDir, { filename, title, identity }) { + const indexPath = path.join(outDir, "index.md"); + if (!existsSync(indexPath)) { + await writeFile( + indexPath, + "# Repo explainers\n\nRepositories explained so far, most recent first.\n\n" + + "| Date | Repo | Title | Entry |\n|---|---|---|---|\n", + "utf8" + ); } + const date = new Date().toISOString().slice(0, 10); + const repoLabel = identity?.nameWithOwner || identity?.name || "repo"; + const repoLink = identity?.url || ""; + const repoCell = repoLink ? `[${repoLabel}](${repoLink})` : repoLabel; + const row = `| ${date} | ${repoCell} | ${title} | [${filename}](${filename}) |\n`; + await appendFile(indexPath, row, "utf8"); +} - const profile = await loadProfile(); - - const pr = await getPR(command); - let diff = await getPRDiff(command); +async function explainPR(prRef, flags, profile) { + const pr = await getPR(prRef); + let diff = await getPRDiff(prRef); if (diff.length > MAX_DIFF_CHARS) { diff = diff.slice(0, MAX_DIFF_CHARS) + @@ -207,6 +220,74 @@ async function main() { await runInteractiveQuiz(entry, flags, outPath); } +async function explainRepo(repoPath, flags, profile) { + const root = path.resolve(repoPath || process.cwd()); + if (!existsSync(root)) { + throw new Error(`Path not found: ${root}`); + } + + console.error(`Gathering repo context for ${root}…`); + const ctx = await gatherRepoContext(root); + const label = ctx.identity?.nameWithOwner || ctx.identity?.name || path.basename(root); + console.error(`Generating repo explainer for ${label}…`); + + const prompt = buildRepoPrompt({ + profile, + identity: ctx.identity, + graphifyReport: ctx.report, + graphSummary: ctx.graphSummary, + recentPrs: ctx.recentPrs, + }); + const entry = await runClaude(prompt); + + const titleMatch = entry.match(/^#\s+(.+)$/m); + const title = titleMatch ? titleMatch[1] : label; + + const baseDir = process.env.EXPLAINER_DIR || GLOBAL_EXPLAINERS; + const outDir = path.join(baseDir, "repos"); + await mkdir(outDir, { recursive: true }); + const num = nextEntryNumber(outDir); + const filename = `${num}-${repoSlug(ctx.identity, root)}.md`; + const outPath = path.join(outDir, filename); + + await writeFile(outPath, entry + "\n", "utf8"); + await appendToRepoIndex(outDir, { + filename, + title, + identity: ctx.identity, + }); + console.log(outPath); + + printExplainerSummary(entry); + await runInteractiveQuiz(entry, flags, outPath); +} + +async function main() { + const { flags, positionals } = parseArgs(process.argv.slice(2)); + const command = positionals[0]; + + if (flags.help || !command) { + usage(); + process.exitCode = flags.help ? 0 : 1; + return; + } + + if (command === "init") { + await initProfile(flags.force); + return; + } + + const profile = await loadProfile(); + + if (command === "repo") { + const repoPath = positionals[1] || process.cwd(); + await explainRepo(repoPath, flags, profile); + return; + } + + await explainPR(command, flags, profile); +} + main().catch((err) => { console.error(`Error: ${err.message}`); process.exitCode = 1; diff --git a/src/display.js b/src/display.js index 4844109..36c3802 100644 --- a/src/display.js +++ b/src/display.js @@ -88,9 +88,9 @@ function writeSection(heading, body) { } /** - * Print title, Ships, and the three core sections to stderr so the user - * can read before the quiz. Skips Quick check (quiz covers it). - * Honors PR_EXPLAINER_QUIET=1. Always prints (TTY or not) unless quiet. + * Print a readable summary to stderr before the quiz. + * Auto-detects PR vs repo explainer from headings. + * Honors PR_EXPLAINER_QUIET=1. */ export function printExplainerSummary(markdown) { if (process.env.PR_EXPLAINER_QUIET === "1") return; @@ -98,24 +98,45 @@ export function printExplainerSummary(markdown) { const titleMatch = markdown.match(/^#\s+(.+)$/m); const title = titleMatch ? stripMdLite(titleMatch[1]) : null; - const shipsMatch = markdown.match(/\*\*Ships:\*\*\s*(.+)/i); - const ships = shipsMatch ? stripMdLite(shipsMatch[1]) : null; - const what = extractSection(markdown, "What changed"); - const whyWay = extractSection(markdown, "Why it was done this way"); - const whyMatters = extractSection(markdown, "Why it matters"); + const isRepo = + /##\s+What it does\b/i.test(markdown) || + /\*\*In short:\*\*/i.test(markdown); stderr.write("\n"); if (title) { stderr.write(bold(title) + "\n"); } - if (ships) { - stderr.write(dim("Ships: ") + ships + "\n"); - } - writeSection("What changed", what); - writeSection("Why it was done this way", whyWay); - writeSection("Why it matters", whyMatters); + if (isRepo) { + const inShortMatch = markdown.match(/\*\*In short:\*\*\s*(.+)/i); + const inShort = inShortMatch ? stripMdLite(inShortMatch[1]) : null; + if (inShort) { + stderr.write(dim("In short: ") + inShort + "\n"); + } + writeSection("What it does", extractSection(markdown, "What it does")); + writeSection( + "How it's put together", + extractSection(markdown, "How it's put together") + ); + writeSection( + "What the team has been working on", + extractSection(markdown, "What the team has been working on") + ); + writeSection("Why it matters", extractSection(markdown, "Why it matters")); + } else { + const shipsMatch = markdown.match(/\*\*Ships:\*\*\s*(.+)/i); + const ships = shipsMatch ? stripMdLite(shipsMatch[1]) : null; + if (ships) { + stderr.write(dim("Ships: ") + ships + "\n"); + } + writeSection("What changed", extractSection(markdown, "What changed")); + writeSection( + "Why it was done this way", + extractSection(markdown, "Why it was done this way") + ); + writeSection("Why it matters", extractSection(markdown, "Why it matters")); + } const quizSection = extractSection(markdown, "Quick check"); if (quizSection) { diff --git a/src/github.js b/src/github.js index e5d3976..94585ae 100644 --- a/src/github.js +++ b/src/github.js @@ -78,3 +78,54 @@ export async function getPR(prRef) { export async function getPRDiff(prRef) { return gh(["pr", "diff", String(prRef)]); } + +/** + * Repo metadata for the GitHub remote of a local checkout (or cwd). + * Runs `gh` with cwd set so bare invocations resolve the right repo. + */ +export async function getRepoIdentity(repoRoot = process.cwd()) { + const json = await ghIn(repoRoot, [ + "repo", + "view", + "--json", + "name,description,url,repositoryTopics,nameWithOwner,defaultBranchRef", + ]); + const data = JSON.parse(json); + const topics = Array.isArray(data.repositoryTopics) + ? data.repositoryTopics + .map((t) => (typeof t === "string" ? t : t?.name)) + .filter(Boolean) + : []; + return { + name: data.name || null, + nameWithOwner: data.nameWithOwner || null, + description: data.description || null, + url: data.url || null, + topics, + defaultBranch: data.defaultBranchRef?.name || null, + }; +} + +/** Recent merged PRs for orientation — titles + bodies, not diffs. */ +export async function getRecentMergedPRs(repoRoot = process.cwd(), limit = 8) { + const json = await ghIn(repoRoot, [ + "pr", + "list", + "--state", + "merged", + "--limit", + String(limit), + "--json", + "number,title,body,url,mergedAt", + ]); + return JSON.parse(json); +} + +async function ghIn(cwd, args) { + try { + const { stdout } = await execFileAsync("gh", args, { cwd }); + return stdout; + } catch (err) { + throw new Error(friendlyGhError(args, err)); + } +} diff --git a/src/graphify.js b/src/graphify.js new file mode 100644 index 0000000..5ee964a --- /dev/null +++ b/src/graphify.js @@ -0,0 +1,102 @@ +import { readFile, access } from "node:fs/promises"; +import { existsSync, constants as fsConstants } from "node:fs"; +import path from "node:path"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); + +const REPORT_BUDGET = 20_000; +const SUMMARY_BUDGET = 4_000; + +function truncate(text, max, label) { + if (!text || text.length <= max) return text || ""; + return text.slice(0, max) + `\n\n... (${label} truncated at ${max} chars)`; +} + +function friendlyGraphifyMissing() { + return ( + "Graphify CLI (`graphify`) not found.\n" + + "Repo mode needs Graphify to build a knowledge graph of the checkout.\n" + + "Install it with:\n" + + " uv tool install graphifyy\n" + + " # or: pipx install graphifyy\n" + + "Then retry. Docs: https://graphify.com/docs" + ); +} + +async function runGraphify(args, { cwd } = {}) { + try { + const { stdout, stderr } = await execFileAsync("graphify", args, { + cwd, + maxBuffer: 10 * 1024 * 1024, + }); + return { stdout: stdout || "", stderr: stderr || "" }; + } catch (err) { + if (err.code === "ENOENT") { + throw new Error(friendlyGraphifyMissing()); + } + const detail = [err.stderr, err.stdout, err.message] + .map((s) => (s || "").trim()) + .filter(Boolean) + .join("\n"); + throw new Error(`graphify ${args.join(" ")} failed:\n${detail}`); + } +} + +/** + * Ensure Graphify has produced graphify-out/ for this checkout, then + * return truncated report + compact god-nodes summary for the prompt. + * Never returns full graph.json. + */ +export async function ensureGraphifyContext(repoRoot, { forceUpdate = false } = {}) { + const root = path.resolve(repoRoot); + const outDir = path.join(root, "graphify-out"); + const reportPath = path.join(outDir, "GRAPH_REPORT.md"); + const graphPath = path.join(outDir, "graph.json"); + + const hasReport = existsSync(reportPath); + const hasGraph = existsSync(graphPath); + + if (!hasReport || !hasGraph || forceUpdate) { + process.stderr.write("Building knowledge graph with Graphify…\n"); + await runGraphify(["update", "."], { cwd: root }); + } + + if (!existsSync(reportPath)) { + throw new Error( + `Graphify finished but ${reportPath} is missing.\n` + + `Try: cd ${root} && graphify update .` + ); + } + + const report = truncate( + await readFile(reportPath, "utf8"), + REPORT_BUDGET, + "GRAPH_REPORT" + ); + + let graphSummary = ""; + try { + await access(graphPath, fsConstants.R_OK); + const { stdout } = await runGraphify( + ["god-nodes", "--top", "10", "--graph", graphPath], + { cwd: root } + ); + graphSummary = truncate(stdout.trim(), SUMMARY_BUDGET, "god-nodes"); + } catch (err) { + if (err.message?.includes("graphify") && err.message?.includes("not found")) { + throw err; + } + graphSummary = `(god-nodes unavailable: ${err.message})`; + } + + return { + outDir, + reportPath, + report, + graphSummary, + }; +} + +export { REPORT_BUDGET, SUMMARY_BUDGET }; diff --git a/src/prompt.js b/src/prompt.js index 5a04337..fada6d1 100644 --- a/src/prompt.js +++ b/src/prompt.js @@ -74,3 +74,154 @@ Correct: . `; } + +/** + * Repo explainer prompt: Graphify structure + recent merges + profile. + * Actor is "this repo" / "the team" — orientation, not a changelog. + */ +export function buildRepoPrompt({ + profile, + identity, + graphifyReport, + graphSummary, + recentPrs, +}) { + const identityBlock = [ + identity?.nameWithOwner || identity?.name || "(unknown repo)", + identity?.description ? `Description: ${identity.description}` : null, + identity?.url ? `URL: ${identity.url}` : null, + identity?.topics?.length ? `Topics: ${identity.topics.join(", ")}` : null, + ] + .filter(Boolean) + .join("\n"); + + return `You write explainers of software repositories for a specific \ +reader, who may or may not be technical and may be new to this codebase \ +or working in a different domain. You will be given that reader's profile, \ +repo identity, a knowledge-graph report of how the code is structured \ +(from Graphify: communities, god nodes, connections), an optional compact \ +graph summary, and recent merged pull requests. Write ONE explainer in \ +Markdown, in plain language pitched precisely at this reader. + +The profile describes the reader's role and what they're currently trying \ +to understand better — it is not an exhaustive skill checklist. Use your \ +own judgment about what someone in that role would already know or care \ +about, and skip over that. Slow down specifically on the areas they said \ +they're trying to understand, and translate anything that assumes \ +technical background they don't have (e.g. explain what a CLI *is* in \ +plain terms if the reader is non-technical). + +Calibrate depth to the reader: +- If they are non-technical (product, ops, support, founder outside the \ +IDE, analyst): write a *product orientation*, not an architecture tour. \ +Lead with who it's for, what problem it solves, what you get when you \ +use it, and why the team's recent work matters for stakeholders. Use the \ +Graphify graph only as *private grounding* — do NOT surface file paths \ +(\`src/…\`), function names (\`main()\`, \`getPR()\`), module maps, \ +"god nodes," communities, or import/call relationships unless a single \ +plain-language capability absolutely needs a one-word name (e.g. "the \ +GitHub CLI"). Prefer verbs and outcomes ("fetches the pull request", \ +"writes a plain-English summary", "asks a short quiz") over structure. +- If they are an engineer in another domain: light structure is fine \ +(major parts and how data flows), still skip trivia and dense symbol lists. +- If they are deep in this stack: you may cite a few real paths/symbols \ +from the graph when it helps; still do not dump every node. + +Never assume the reader built this repo — you are orienting them to \ +someone else's system. Write "this repo" / "the team" as the actor, \ +never "you." + +Treat the Graphify report and graph summary as the grounded skeleton of \ +what the system *is*. Do not invent capabilities, modules, or connections \ +that are not present. Prefer graph-backed facts over marketing language \ +if they disagree. Focus on product purpose and load-bearing *behaviors*, \ +not an inventory of files. + +For "How it's put together" for non-technical readers: describe 3–5 \ +capabilities as a simple flow (e.g. "takes a PR link → reads what \ +changed → writes an explainer in your voice → optional quiz"). No \ +module-by-module breakdown. + +Recent merged PRs show what the team has been shipping *now*. Extract \ +2–3 themes max — not a changelog dump. Frame themes in product terms \ +(install experience, clarity for first-time users, etc.), not commit \ +hygiene. If there are no recent PRs, say so briefly and skip inventing \ +momentum. + +If the repo is small or routine for this reader, say so briefly instead of \ +inventing significance. + +Be concise. Prefer short sentences and tight bullets over long paragraphs. \ +Each section should make its point in a few lines — lead with the takeaway, \ +then one or two supporting facts. Cut throat-clearing, repetition, and \ +restating the same idea in softer words. Aim for a scannable brief, not an \ +essay. Rough budget: "In short" one sentence; each body section about \ +3–6 short lines or bullets; "Why it matters" two short paragraphs max \ +(or a short bullet list). + +Default to bullets for "What it does", "How it's put together", and \ +"What the team has been working on". Keep "Why it matters" to 2–4 short \ +bullets (or two sentences). No multi-paragraph walls. If a sentence does \ +not add new information, delete it. + +End with 2-3 short multiple-choice recall questions that test whether the \ +reader absorbed the explainer. Questions must match the reader's depth: \ +for non-technical readers, ask about purpose, audience, and stakeholder \ +takeaways — never filenames, function names, or which source file does \ +what. Examples: "what problem does this repo solve?", "what do you get \ +after running it?", "what have recent changes been optimizing for?". \ +Each question has exactly three short options (A/B/C) grounded in this \ +repo: one correct, two plausible distractors. Also write a one- or \ +two-sentence answer explanation inside the details block. + + +${profile} + + + +${identityBlock} + + + +${graphifyReport || "(no Graphify report provided)"} + + + +${graphSummary || "(none)"} + + + +${recentPrs || "(no recent merged PRs found)"} + + +Output only the Markdown explainer, structured as: +# + +**In short:** + +## What it does +... + +## How it's put together +... + +## What the team has been working on +... + +## Why it matters +... + +## Quick check +<2-3 multiple-choice questions. Use this exact shape so tools can parse it:> + +**Q1. ** +- A)