diff --git a/README-zh.md b/README-zh.md index 2c2de0e..6dd0510 100644 --- a/README-zh.md +++ b/README-zh.md @@ -138,6 +138,7 @@ - [changelog-generator](./plugins/changelog-generator) - [codebase-documenter](./plugins/codebase-documenter) - [context7-docs-fetcher](./plugins/context7-docs-fetcher) +- [dev-report](./plugins/dev-report) - [documentation-generator](./plugins/documentation-generator) - [generate-api-docs](./plugins/generate-api-docs) - [openapi-expert](./plugins/openapi-expert) diff --git a/README.md b/README.md index e4de615..e2e05f4 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ Install or disable them dynamically with the `/plugin` command — enabling you - [changelog-generator](./plugins/changelog-generator) - [codebase-documenter](./plugins/codebase-documenter) - [context7-docs-fetcher](./plugins/context7-docs-fetcher) +- [dev-report](./plugins/dev-report) - [documentation-generator](./plugins/documentation-generator) - [generate-api-docs](./plugins/generate-api-docs) - [openapi-expert](./plugins/openapi-expert) diff --git a/plugins/dev-report/.claude-plugin/plugin.json b/plugins/dev-report/.claude-plugin/plugin.json new file mode 100644 index 0000000..55e6b47 --- /dev/null +++ b/plugins/dev-report/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "dev-report", + "description": "Explains your Claude Code session in plain language: what it changed, why it chose that approach, what is still unfinished, and what you should do next", + "version": "1.0.1", + "author": { + "name": "Delpi Corp.", + "url": "https://github.com/delpicorp" + }, + "homepage": "https://github.com/delpicorp/dev-report", + "license": "MIT", + "commands": "./commands/localized/" +} diff --git a/plugins/dev-report/LICENSE b/plugins/dev-report/LICENSE new file mode 100644 index 0000000..38ea58a --- /dev/null +++ b/plugins/dev-report/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Delpi Corp. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/dev-report/README.md b/plugins/dev-report/README.md new file mode 100644 index 0000000..5b564d9 --- /dev/null +++ b/plugins/dev-report/README.md @@ -0,0 +1,49 @@ +# dev-report + +Explains a Claude Code session in plain language. + +You finish a long session. A dozen files changed and several implementation decisions were made along the way, but the end-of-session summary is file names and function names — accurate, and written for someone reading the diff alongside it. Asking for it "simply" removes the reasoning too. + +Run `/dev-report` and Claude explains instead: + +- **What it built or changed** — in terms of the product, not the file tree +- **Why it chose that approach** — including the option it turned down and what would have gone wrong +- **What is still unfinished or unverified** — verified, assumed, and untested kept apart +- **What you should do next** — including the calls only you can make + +Technical terms aren't removed. They're explained the first time they appear, so the reasoning survives. + +## Use + +``` +/dev-report +/dev-report this week +/dev-report just the payment work +/dev-report keep it short, she only has a minute +``` + +Anything after the command is a scope or focus hint and overrides the skill's defaults. The skill is explicit-only — it does not fire on a casual "what did you just do?" + +Reports come out in whatever language the command was typed in. Localized aliases ship for Korean (`/개발보고`), Japanese (`/開発報告`), and Spanish (`/informe-desarrollo`). File paths, function names, commands, and log lines always stay in their original form. + +## What's here + +``` +dev-report/ +├── SKILL.md # the skill — audience model, report structure, craft rules +├── commands/ # /dev-report and the three localized aliases +└── references/ + ├── craft.md # explanation techniques with before/after rewrites + └── examples.md # two worked example reports (English, Korean) +``` + +## Attribution + +Original: [delpicorp/dev-report](https://github.com/delpicorp/dev-report) by [Delpi Corp.](https://github.com/delpicorp) — MIT licensed. Vendored here unmodified at v1.0.1, except for this README and a `plugin.json` trimmed to the fields this repo uses. + +For updates, install from the upstream marketplace directly: + +``` +/plugin marketplace add delpicorp/dev-report +/plugin install dev-report@delpicorp +``` diff --git a/plugins/dev-report/SKILL.md b/plugins/dev-report/SKILL.md new file mode 100644 index 0000000..05642f7 --- /dev/null +++ b/plugins/dev-report/SKILL.md @@ -0,0 +1,141 @@ +--- +name: dev-report +description: Write up a coding session for a non-technical stakeholder — the context, what was built, and the engineering reasoning behind it — the way a senior engineer briefs a product manager who does not read code. Use ONLY when explicitly invoked, either through the /dev-report slash command or one of its localized aliases (/개발보고 and similar), or when the user directly asks for a stakeholder-facing write-up of the session ("write today up for my PM", "explain this session for a non-developer", "개발 보고서 써줘"). Do NOT trigger on an ordinary "what did you just do?" — that wants a short plain answer, not a report. +--- + +# Dev Report + +Turn a work session into a report a non-technical stakeholder can actually act on. + +## Invocation + +This skill is explicit-only. It runs when the user calls `/dev-report`, a localized alias of it, or asks in plain words for a stakeholder-facing write-up. A casual "what did you just do?" is not an invocation — answer that normally. + +Anything the user types after the command is a **scope or focus hint**: `/dev-report this week`, `/dev-report just the auth work`, `/dev-report 짧게`. With no hint, report on the current conversation session. Honor a length or emphasis hint over this skill's defaults — they know their reader. + +## Who you are writing for + +A product owner, founder, or PM who decides priorities and budget but does not read code. Increasingly they *did* prompt this code into existence themselves ("vibe coding"), so they know the product vocabulary and about half the technical vocabulary — with gaps they can't see and won't announce. + +The failure mode to avoid is **not** "too technical." It is **technical words with no referent**. A non-developer can follow arbitrarily deep reasoning as long as every noun in it has been given a meaning first. So: go deep on the logic, and pay for each new term the moment you introduce it. + +Two things they need that a peer-to-peer standup would skip: + +- **Consequence.** Not "the sanitizer stripped the anchors" but "every internal link in that article was deleted before it went live, which is why the article shipped with none." +- **Confidence level.** Which claims you measured, which you inferred, which you haven't checked. They will make decisions on this, so an unlabeled guess is worse than no answer. + +## Output language + +Write the report in **the language of the message that invoked the skill**. `/dev-report 이번 주 작업 정리해줘` → Korean. `/dev-report` with no text → the language the conversation has been in. + +Keep these verbatim in their original form regardless of output language: file paths, function and variable names, commands, log lines, error messages, branch and commit names, product and vendor names. The reader needs to paste them into a search box or say them to someone else — a translated identifier is a broken one. + +When a technical term has no natural equivalent in the output language, use the English term and gloss it once in the reader's language, then keep using the English term. + +## Step 1 — Gather + +**The conversation is the primary source.** It holds what git cannot: why this work was chosen, what was tried and abandoned, what the user corrected you on, what a number actually meant. Reconstruct from it first. + +Then corroborate the facts a report will be judged on: + +```bash +git log --oneline -15 +git diff --stat HEAD~1 # or the session's base commit +git status --short +``` + +Check specifically **how far each change actually got**, because these are four different states and stakeholders routinely hear the last one when you said the first: + +| State | How to say it | +|---|---| +| Edited on disk, not committed | "changed locally, not saved to the repo yet" | +| Committed | "in the repo, not on the server" | +| Pushed / merged | "in the shared repo" | +| Deployed and observed working in the real environment | "live, and I saw it work" | + +If tests ran, quote the real result line. If a deploy happened, say what you observed afterward — not what you expect. + +Do not invent a section's content. If the session produced no numbers, the numbers section is omitted and the honesty section says measurement is missing. + +## Step 2 — Structure + +Use these sections in this order. **Omit any section that has no real content** rather than padding it — a feature-build session usually has no "why it happened," a bug-fix session usually has no "what we built." + +Render the headings in the output language; the names below are descriptions, not literal text. + +**Put the section's emoji at the front of its heading**, as shown below — one per heading, and nowhere else in the report. They give a long report a spine the reader can scroll by: the eye finds the numbers table and the honesty section without reading. Emoji scattered through body text does the opposite, turning a report into a chat message. If a session needs a section beyond these eight, pick one in the same register (a timeline section takes 📅). + +**1. 📌 One-line summary.** +What this session actually turned out to be. If there is a gap between what it was supposed to be and what it became, that gap *is* the summary — it is the most decision-relevant fact you have. +> "This was supposed to be a cleanup day for stale checklist items. It turned into finding and fixing a bug that had been silently shipping broken articles for five days." + +**2. 🎯 Why we started here.** +What was blocking, what the state was before, why this was the right first move. Without this, everything after it reads as random activity, and a stakeholder who can't see the reason will assume there wasn't one. + +**3. 🔍 What we found / what we built.** +Lead with the concrete artifact — the log line, error text, or number — **quoted verbatim** — then explain what it means. Evidence before interpretation, because a stakeholder who only ever gets your interpretation has no way to tell analysis from storytelling. + +Define each unfamiliar term at first use, in one clause, then use the real term freely for the rest of the report. +> "`SEO optimization complete — internal links 0 kept, 11 fabricated URLs removed`. Internal links are links from one of your articles to another; Google reads them as a map of what your site covers, and they keep readers on the site. Eleven were planned. Zero survived." + +**4. 🧩 Why it happened / how it works.** +The causal chain, told as a sequence rather than a list. Each step should make the next one feel inevitable. + +Name the component that behaved **correctly**, not just the broken one. Without that, the reader concludes the whole system is unreliable and starts distrusting parts that are fine. +> "The sanitizer did exactly its job — it deletes links to pages that don't exist, and `/some-slug` genuinely 404s. The mistake was one layer upstream: nobody had told the writer that real URLs on this site start with `/blog/`." + +**5. 🔧 How we solved it, and why this way. ← the centerpiece** + +This is the section the reader values most and the one most reports skip. Give it the most words. Cover: + +- **The mechanism**, in plain terms — what the code now does, step by step, in the order it does it. +- **The alternative you rejected, and what would have gone wrong.** This is what makes it a report from an engineer rather than a status line. It also lets a non-technical reader audit your judgment without reading code, which is the only lever they have. +- **Why multiple changes instead of one**, if there were several. Untangle root-cause fixes from defenses; a reader who thinks two fixes means one didn't work will lose confidence in both. +- **What the change deliberately does *not* do.** Scope boundaries prevent a stakeholder from assuming a class of problem is now solved. + +> "Two changes, doing different jobs. The first is the root cause: the plan handed to the writer now carries real full URLs instead of bare slugs, so there's nothing left to guess at. The second is a safety net: if the writer still marks a link the wrong way, we now convert it instead of deleting it — but only after confirming that page actually exists on the site. I didn't want the net alone, because that would have left the writer permanently guessing and the net silently covering for it; the day the net had a gap, we'd be back here. And I didn't want the root fix alone, because it only holds as long as the model follows instructions, which is not a guarantee you can build on." + +**6. 📊 Numbers.** +A compact table, before/after, with the sample size next to every rate. `100%` over three articles is a very different claim from `100%` over three hundred, and the reader cannot tell them apart unless you show the denominator. + +**7. 🧪 What is verified, what is assumed, what is untested.** +Three explicit buckets, said out loud. This is what makes the rest of the report trustworthy — a reader who has seen you volunteer your own gaps can believe the parts you state flatly. +> "Verified: the fix works on the article I ran it on — 9 links survived, checked in the live page source. Assumed: it behaves the same for other accounts, since they share the same code path, but I only ran one. Not tested: the premium writing engine — that run fell back to the standard one, so that path is still unproven." + +**8. 🚀 What's next, and what I need from you.** +Separate what you'll do on your own from decisions only they can make (spending money, granting access, approving a tradeoff, choosing priority). Make the asks specific enough to answer in one sentence. + +## Step 3 — Craft + +These are the moves that make the difference between a report that gets read and one that gets skimmed. + +**Attach analogies, don't substitute them.** "A migration is a numbered instruction for changing the shape of the database — like a renovation permit, filed in order" keeps the real word available. "Think of it as a renovation permit" alone leaves the reader unable to search for it or repeat it to anyone else. They will need to do both. + +**Every quantity needs a denominator and a unit.** "Faster" → "45 seconds instead of 5 minutes." "Most articles" → "7 of 9." + +**Say what you don't know, in the same voice as what you do.** No hedging garnish on facts, no false confidence on guesses. "I don't know why that one failed; I haven't reproduced it yet" is a complete and acceptable sentence. + +**Explain a failure without assigning blame to a person or a model.** Describe the missing piece of information, not the actor's shortcoming. It reads as diagnosis instead of excuse and is usually more accurate anyway. + +**Time-box the reading.** If the report runs long, the one-line summary and the numbers table should be enough on their own for a reader who stops after 30 seconds. + +## What not to do + +| Anti-pattern | Why it fails | +|---|---| +| Pasting diffs, file trees, or long code blocks as the body | The reader can't read them; it signals you didn't do the translation work. Quote a single line only when it *is* the evidence. | +| "Refactored X for better maintainability" | No observable consequence. If nothing changed for the product, say what it buys and when. | +| A flat bullet list of changed files | Removes causality, which is the entire value of the report. | +| Percentages with no sample size | Reads as a stronger claim than the data supports. | +| "Fixed and deployed" when it was committed | The single fastest way to lose a stakeholder's trust. See the states table in Step 1. | +| Opening with "I'll explain what we did today" | The report is the explanation. Start with the summary. | +| Apologizing for the bug, or dwelling on the mistake | They want the state of the system, not contrition. One clause on cause, then move to the fix. | + +## Length + +Proportional to the session. A single-thread session lands around 600–1,200 words; a session with several independent threads runs longer, with each thread getting its own pass through sections 3–5. Never pad to look thorough — an omitted section reads as discipline, a padded one reads as noise. + +## Reference files + +- `references/craft.md` — deeper treatment of the explanation techniques, with before/after rewrites. Read when a draft feels technically correct but flat, or when you're unsure how to unpack a specific concept. +- `references/examples.md` — two full worked reports (Korean and English). Read when starting your first report, or to calibrate depth on the "how we solved it" section. diff --git a/plugins/dev-report/commands/dev-report.md b/plugins/dev-report/commands/dev-report.md new file mode 100644 index 0000000..db9627f --- /dev/null +++ b/plugins/dev-report/commands/dev-report.md @@ -0,0 +1,12 @@ +--- +description: Brief this session to a non-technical stakeholder — context, what was built, and the reasoning behind it +argument-hint: [optional scope or focus — e.g. "this week", "just the payment work", "keep it short"] +--- + +Use the `dev-report` skill to write up this work session for a non-technical stakeholder — someone who owns the product and the priorities but does not read code. + +Scope / focus hint from the user: $ARGUMENTS + +If that hint is empty, report on the current conversation session. If it names a time range, a branch, or a topic, gather evidence from git as well as from the conversation. + +Write the report in the language the user typed above. If they typed nothing, use the language this conversation has been in. diff --git a/plugins/dev-report/commands/localized/informe-desarrollo.md b/plugins/dev-report/commands/localized/informe-desarrollo.md new file mode 100644 index 0000000..4704876 --- /dev/null +++ b/plugins/dev-report/commands/localized/informe-desarrollo.md @@ -0,0 +1,12 @@ +--- +description: Explica esta sesión a alguien que decide el producto pero no lee código — contexto, qué se construyó y por qué así +argument-hint: [alcance o enfoque opcional — p. ej. "esta semana", "solo el trabajo de pagos", "que sea breve"] +--- + +Usa la skill `dev-report` para redactar un informe de esta sesión de trabajo dirigido a una persona no técnica: alguien que decide el producto y las prioridades, pero no lee código. + +Alcance / enfoque indicado por la persona usuaria: $ARGUMENTS + +Si está vacío, informa sobre la sesión de conversación actual. Si indica un rango de fechas, una rama o un tema, reúne evidencia también desde git, no solo desde la conversación. + +Escribe el informe en español. Mantén sin traducir las rutas de archivo, nombres de funciones, comandos, líneas de log y mensajes de error. diff --git "a/plugins/dev-report/commands/localized/\351\226\213\347\231\272\345\240\261\345\221\212.md" "b/plugins/dev-report/commands/localized/\351\226\213\347\231\272\345\240\261\345\221\212.md" new file mode 100644 index 0000000..7d1d764 --- /dev/null +++ "b/plugins/dev-report/commands/localized/\351\226\213\347\231\272\345\240\261\345\221\212.md" @@ -0,0 +1,12 @@ +--- +description: 今回のセッションの作業を、コードを読まない意思決定者に報告する — 背景・作ったもの・その理由 +argument-hint: [範囲や焦点 — 例:「今週」「決済まわりだけ」「短めに」] +--- + +`dev-report` スキルを使い、今回の作業セッションを非開発者の意思決定者向けに報告する。読み手はプロダクトと優先順位を決めるが、コードは読まない。 + +ユーザーが指定した範囲・焦点: $ARGUMENTS + +空の場合は現在の会話セッションを対象とする。期間・ブランチ・トピックが指定されている場合は、会話に加えて git からも根拠を集める。 + +報告書は日本語で書く。ただしファイルパス・関数名・コマンド・ログ行・エラーメッセージは原文のままにする。 diff --git "a/plugins/dev-report/commands/localized/\352\260\234\353\260\234\353\263\264\352\263\240.md" "b/plugins/dev-report/commands/localized/\352\260\234\353\260\234\353\263\264\352\263\240.md" new file mode 100644 index 0000000..fc429d9 --- /dev/null +++ "b/plugins/dev-report/commands/localized/\352\260\234\353\260\234\353\263\264\352\263\240.md" @@ -0,0 +1,12 @@ +--- +description: 이번 세션 작업을 비개발자 의사결정자에게 보고 — 맥락·무엇을 만들었는지·왜 그렇게 했는지 +argument-hint: [범위나 초점 — 예: "이번 주", "결제 부분만", "짧게"] +--- + +`dev-report` 스킬을 사용해, 이번 작업 세션을 비개발자 의사결정자에게 보고하는 글을 작성한다. 읽는 사람은 제품과 우선순위를 결정하지만 코드는 읽지 않는다. + +사용자가 지정한 범위·초점: $ARGUMENTS + +비어 있으면 현재 대화 세션을 대상으로 한다. 기간·브랜치·주제가 적혀 있으면 대화뿐 아니라 git에서도 근거를 모은다. + +보고서는 한국어로 작성한다. 단 파일 경로·함수명·명령어·로그 줄·에러 메시지는 원문 그대로 둔다. diff --git a/plugins/dev-report/references/craft.md b/plugins/dev-report/references/craft.md new file mode 100644 index 0000000..a740023 --- /dev/null +++ b/plugins/dev-report/references/craft.md @@ -0,0 +1,143 @@ +# Craft — how to explain engineering work to someone who doesn't read code + +Read this when a draft is technically correct but flat, or when you're stuck on how to unpack one specific concept. + +## Contents + +1. [The one rule](#1-the-one-rule) +2. [Evidence before interpretation](#2-evidence-before-interpretation) +3. [Unpacking a mechanism](#3-unpacking-a-mechanism) +4. [Analogies that attach instead of replace](#4-analogies-that-attach-instead-of-replace) +5. [Naming who was right](#5-naming-who-was-right) +6. [Making numbers mean something](#6-making-numbers-mean-something) +7. [Calibrated language](#7-calibrated-language) +8. [Untangling multiple changes](#8-untangling-multiple-changes) +9. [Rewrites](#9-rewrites) + +--- + +## 1. The one rule + +**Pay for each term the moment you introduce it, then spend it freely.** + +A non-developer is not defeated by complexity. They are defeated by a sentence containing a word that means nothing to them, which makes the next sentence mean nothing, and by the third one they have stopped reading and started nodding. + +One clause is the price. Not a paragraph — a paragraph on every term turns the report into a glossary and insults them. One clause, mid-sentence, then move on and use the real word for the rest of the report. + +> A **migration** — a numbered instruction file that changes the shape of the database — has to be added at the end of the list, never inserted in the middle. + +The reader now owns "migration" and you can use it eleven more times without apology. This is what lets the report go deep: depth is affordable once the vocabulary is paid for. + +**The corollary:** never introduce a term you don't need. If a detail doesn't change what the reader would decide, cutting it is better than glossing it. + +## 2. Evidence before interpretation + +Lead with the actual artifact — the log line, the error, the count — quoted exactly. Then say what it means. + +The reason is trust, not drama. A stakeholder who only ever receives your conclusions has no way to distinguish analysis from confident storytelling, and over time they either believe everything or nothing. Showing the raw thing first, even when they can't fully parse it, tells them there is a real object underneath your reasoning. + +> The log line read `SEO optimization complete — internal links 0 kept, 11 fabricated URLs removed`. Internal links are links from one of your articles to another; Google uses them to understand what your site covers, and readers use them to keep reading. Eleven were planned for that article. Zero made it in. + +Quote **one** line, the one that carries the finding. A wall of log output is not evidence, it's abdication. + +## 3. Unpacking a mechanism + +When you have to explain how something works, climb this ladder in order. Skipping a rung is what makes explanations feel simultaneously too long and unsatisfying. + +1. **What it does** — the observable outcome, in product terms. +2. **When it runs** — the trigger and the moment in the sequence. Non-developers consistently misjudge this, and it's the source of most of their wrong mental models. +3. **How it decides** — the actual rule, stated as a rule. +4. **Why that rule and not another** — the tradeoff. + +> **What:** before an article is published, we delete links pointing at pages that don't exist. **When:** at the last step, after the article is written and right before it goes to WordPress. **How it decides:** it compares each link against the list of pages we've actually published — anything not on that list gets unwrapped into plain text. **Why:** a link to a dead page hurts more than no link at all, both for readers and for how Google reads the site, and we would rather lose a link than ship a broken one. + +Rung 4 is the one engineers skip and stakeholders most want. It is the only part they can actually weigh in on. + +## 4. Analogies that attach instead of replace + +An analogy should ride alongside the real term, never take its seat. + +**Attached** — reader keeps the word: +> A **race condition** — two parts of the program reaching for the same thing at the same time, like two people grabbing one shopping cart — meant the counter sometimes skipped a number. + +**Substituted** — reader is now unable to search, ask, or repeat: +> Think of it like two people grabbing the same shopping cart. That's what was happening. + +The second version feels friendlier and leaves the reader poorer. They will eventually be in a room where someone says "race condition," and your job was to make that room survivable. + +Keep analogies short and drop them once they've done their work. An analogy extended over three sentences starts making claims about the system that aren't true. + +## 5. Naming who was right + +When you explain a failure, explicitly name the components that behaved correctly. + +Without it, a non-technical reader generalizes: something broke, so the system is unreliable, so the parts I was told are fine are probably not fine either. That generalization is expensive and hard to walk back. + +> The sanitizer did exactly what it was built to do — it deletes links to pages that don't exist, and `/some-slug` genuinely returns a 404. The gap was one step upstream: nobody had told the writing step that real URLs on this site begin with `/blog/`. So it invented a plausible address, and the checker correctly threw it away. + +This also happens to be better engineering communication. "The checker was right and the input was wrong" is a more precise diagnosis than "the links broke," and it points at where the fix belongs. + +## 6. Making numbers mean something + +**Always show the denominator.** `100%` across 3 articles and `100%` across 300 are different claims, and nothing in the number tells the reader which one they're holding. + +**Convert to the unit they care about.** Milliseconds are engineer units. "The page used to take 5 seconds to appear and now takes under 1" is a product unit. + +**Give a baseline for any number that isn't self-evidently good or bad.** "2,285 tests pass" means nothing alone. "2,285 tests pass, 15 of them written today to cover this specific bug" is a fact about today. + +**Distinguish measured from projected.** "Should cut the cost roughly in half" and "cut it from $180 to $95 last month" are not the same sentence, and only one of them is safe to plan against. + +## 7. Calibrated language + +Match your wording to how much you actually know. Use a consistent vocabulary so the reader can learn to read your confidence at a glance: + +| Confidence | Phrasing | What backs it | +|---|---|---| +| Measured | "I ran it and saw X" | You observed it in the real environment | +| Tested | "the tests cover this case" | It passes automated checks, not observed live | +| Reasoned | "it should behave the same because they share the code path" | Sound inference, unverified | +| Unknown | "I don't know yet; I haven't reproduced it" | Say it plainly and stop | + +The most valuable sentence in most reports is the one that admits a gap. It is what makes every other sentence load-bearing. Reports that never say "I don't know" get read as marketing. + +Do **not** soften facts with hedges ("it seems the tests passed") or harden guesses with confidence ("this is fixed" when you mean "this should be fixed"). Both destroy the signal. + +## 8. Untangling multiple changes + +When a session produced several changes, the reader's default assumption is that the extra ones mean the first didn't work. Preempt that by giving each change a role: + +- **Root cause** — removes the reason the problem could happen. +- **Defense** — catches it if it happens anyway, usually because the root cause depends on something you don't fully control (a model's output, a third party, a human step). +- **Visibility** — makes it detectable next time. Logging, a counter, an alert. +- **Cleanup** — repairs damage already done. + +Then say why the combination, not just the pieces: + +> The root fix alone would only hold as long as the model follows instructions, which isn't something you can build a guarantee on. The safety net alone would leave the model permanently guessing, with the net quietly covering for it — and the day the net had a gap, we'd be right back here with no warning. Together, one of them has to fail loudly before anything reaches a reader. + +## 9. Rewrites + +**Vague → concrete** + +> ❌ Improved the reliability of the publishing pipeline. +> ✅ Publishing used to fail silently when an article ran past 100 seconds — you'd see "done" and nothing would appear. It now splits the work into stages that each finish well under that limit, so a long article publishes the same as a short one. + +**Jargon dump → paid-for jargon** + +> ❌ Moved the LLM insertion pass out of Stage 3 and made the sanitizer idempotent. +> ✅ We used to have the AI insert internal links as a separate step near the end. That step is gone — links are now written into the article from the start, and the final check only removes bad ones instead of adding any. Running that check twice now produces the same result as running it once, which matters because a retry used to be able to mangle an article. + +**Blame → diagnosis** + +> ❌ The model hallucinated URLs again. +> ✅ The writing step was given article titles but not their web addresses, so when it wanted to link to one it constructed an address that looked right — `/some-slug` — and ours are actually `/blog/some-slug`. It had no way to know that. We now hand it the real addresses. + +**Buried lede → decision-first** + +> ❌ [three paragraphs of investigation] …and so the earliest affected article was published on the 30th. +> ✅ Five articles published since July 30th have no internal links. None of them are on the public site yet, so nothing a reader can see is affected — but the two scheduled for tomorrow would have been. Here's how I found that: + +**False completion → honest state** + +> ❌ Fixed and deployed. +> ✅ Fixed, tested, and pushed to the repo. It is not on the server yet — the deploy is the next step, and I'll confirm on a real article afterward, because the tests can't catch this particular failure. diff --git a/plugins/dev-report/references/examples.md b/plugins/dev-report/references/examples.md new file mode 100644 index 0000000..049f8f8 --- /dev/null +++ b/plugins/dev-report/references/examples.md @@ -0,0 +1,168 @@ +# Worked examples + +Two complete reports, in different languages, from sessions of different shapes. Read one end-to-end before writing your first report — the calibration you need is mostly in how much room section 5 gets. + +- [Example A — English, bug-hunt session](#example-a--english--bug-hunt-session) +- [Example B — Korean, mixed verification + regression session](#example-b--korean--mixed-verification--regression-session) +- [What each one is doing](#what-each-one-is-doing) + +--- + +## Example A — English — bug-hunt session + +> Illustrative example. The session was meant to be routine maintenance and turned into a bug hunt. + +### 📌 Summary + +Today was supposed to be clearing four small items off the backlog. I got through two of them, then found that about one in six customers hasn't been receiving their order confirmation email since the payment provider upgrade three weeks ago. That's fixed and live now. The other two backlog items are untouched. + +### 🎯 Why I started here + +The backlog items were all blocked on the same thing — none could be checked without putting a real order through the system. So I placed a test order first, intending to use it for all four checks at once. That order never produced a confirmation email, which is what pulled the thread. + +### 🔍 What I found + +The server log for that order read: + +``` +[warn] notify: no handler for event 'payment.succeeded.v2' — dropped +``` + +An **event** here is a message our payment provider sends us when something happens — "payment succeeded," "card declined." A **handler** is the piece of our code that listens for a specific message and does something about it. The log is saying: a message arrived, nothing was listening, and it was thrown away without an error. + +Thrown away without an error is the important half. Nothing failed loudly, so nothing alerted us, so this ran for three weeks looking healthy. + +### 🧩 Why it happened + +When we upgraded the payment provider's library three weeks ago, they renamed their events — `payment.succeeded` became `payment.succeeded.v2`. Our code listens by exact name, so it stopped matching. Both names are valid; the provider sends the new one to accounts on the new library and the old one to accounts still on the old one. + +That last detail explains the "one in six." We run six servers, and two of them hadn't picked up the library upgrade because they were restarted on an older deployment. Those two kept getting the old event name and kept working perfectly. So roughly a third of orders were fine, which is exactly the pattern that keeps a problem invisible — enough emails were going out that nobody noticed a gap. + +Worth saying: the notification system was not broken. It correctly refused to guess what an unrecognized message meant. Silently dropping it instead of shouting is the actual defect, and that's a separate fix I'll get to below. + +### 🔧 How I fixed it, and why this way + +Three changes, doing three different jobs. + +**The root cause:** the handler now registers for both event names. The provider has committed to sending `.v2` going forward but hasn't set a date for retiring the old one, so accepting both is not a temporary hack — it's the correct state until they announce a cutoff. + +I considered the alternative of pinning every server to the new library and listening only for `.v2`. That is cleaner, and I rejected it because it makes email delivery depend on all six servers being perfectly in sync at all times. They weren't today and they won't be during the next deploy either. Correctness shouldn't rest on a condition we've already watched fail. + +**The visibility fix:** an unrecognized event is now logged as an error rather than a warning, and it increments a counter we already alert on. This is the change I actually care most about. The specific renaming was a one-off; the fact that a dropped message could stay invisible for three weeks is structural, and it would have hidden the next one just as well. + +**The cleanup:** I found the 47 affected orders and sent their confirmation emails. They now say "your order confirmation" without a timestamp claiming it was sent today — I didn't want a customer comparing it to their bank statement and getting confused about when the charge happened. + +What this does **not** cover: only the confirmation email was affected. Shipping notifications go through a different path and were never broken, and I confirmed that rather than assuming it. + +### 📊 Numbers + +| | Before | After | +|---|---|---| +| Orders confirmed by email | 5 of 6 (83%, over 284 orders in 3 weeks) | 6 of 6 (30 of 30 since deploy) | +| Time from payment to email | 4 seconds | 4 seconds — unchanged | +| Unrecognized events dropped silently | all of them | none — each one now alerts | +| Backlog affected orders | 47 | 0 | + +### 🧪 Verified, assumed, untested + +**Verified:** I placed four real test orders after deploying, one against each server type, and all four emails arrived. The 47 backfilled emails show as delivered in the provider's dashboard. + +**Assumed:** that no other event type has been quietly renamed. I checked the provider's changelog and found only this one, but I'm trusting their changelog rather than auditing every event we listen for. The new alerting means we'd now find out within minutes instead of weeks, which is why I'm comfortable leaving it there. + +**Untested:** refund confirmations. They use the same handler mechanism and I believe they're fine, but no refund happened today and I didn't force one against live payment data. + +### 🚀 Next, and what I need from you + +I'll do on my own: finish the two remaining backlog items tomorrow, and audit the rest of our event names against the provider's changelog. + +I need a decision from you on one thing: those 47 customers got their confirmation email three weeks late. I can leave it as-is, or add a one-line apology to the resend. I'd lean toward leaving it — most people won't connect the two, and an apology draws attention to a gap they didn't notice. But that's a brand call, not a technical one. + +--- + +## Example B — Korean — mixed verification + regression session + +> 각색한 예시. 검증 항목을 치우려던 날이 회귀 버그 수정으로 바뀐 경우. + +### 📌 한 줄 요약 + +원래는 밀린 검증 항목을 치우는 날이었는데, 그 과정에서 예약 전날 저녁에 나가야 할 알림 문자가 3주째 다음 날 새벽 5시에 발송되고 있었다는 걸 발견해 고쳤습니다. 지금은 제시간 발송을 실측으로 확인했고, 아직 발송 전이던 알림은 전부 바로잡았습니다. + +### 🎯 왜 이것부터 했나 + +밀린 검증 항목 6개가 전부 "실제 예약을 한 건 넣어봐야 확인 가능"에 걸려 있었습니다. 하나씩 보는 것보다 테스트 예약 한 건을 끝까지 흘려보내면서 6개를 한 번에 터는 게 빠르다고 판단해서, 예약을 하나 등록했습니다. + +### 🔍 그러다 발견한 것 + +예약을 등록하자 알림 로그에 이렇게 찍혔습니다. + +``` +알림 등록: 예약 #4821 — 발송 예정 2026-08-03T20:00:00Z +``` + +끝의 **`Z`는 국제 표준시(UTC) 표시**입니다. 전 세계 서버가 공통 기준으로 쓰는 시각인데, 한국보다 9시간 느립니다. 즉 이 알림은 손님 기준 저녁 8시가 아니라 **다음 날 새벽 5시**에 나가도록 잡혀 있었습니다. 문자가 올 때까지 기다릴 필요도 없었습니다 — 등록되는 순간 이미 시각이 틀려 있었습니다. + +### 🧩 원인을 따라가 보니 + +3주 전에 문자 발송 업체를 교체했습니다. 새 업체 연동 코드는 시각에 시간대 표시가 없으면 국제 표준시로 해석합니다. 이전 업체 연동은 서버에 설정된 한국 시간을 따랐습니다. 교체하면서 옮긴 코드는 시각 숫자만 그대로 넘기고 있었고, "이 시각은 한국 기준"이라는 표시 한 줄이 빠진 겁니다. + +발송기 자체는 **정확히 제 일을 했습니다.** 넘겨받은 시각에 1분 오차 없이 보냈습니다. 잘못은 한 단계 위에 있었습니다 — 아무도 발송기에게 그 시각이 어느 나라 기준인지 알려주지 않았습니다. + +### 📅 언제부터 이랬나 + +알림 등록 로그를 날짜순으로 훑었습니다. 교체일 이전 건은 시각이 `+09:00`으로 끝나고, 이후 건은 전부 `Z`로 끝납니다. 표기가 바뀐 지점이 원인이 들어온 날입니다. + +교체 후 잡힌 예약은 41건입니다. 그중 알림이 이미 발송된 12건은 전부 새벽 5시에 나갔습니다. 새벽 문자를 받은 12명 중 문의를 남긴 손님은 없었습니다 — 아무도 항의하지 않는 조용한 실패였고, 3주를 간 것도 그래서입니다. **아직 발송 전이던 29건은 오늘 전부 바로잡아서, 추가 피해는 없습니다.** + +### 🔧 어떻게 고쳤나 — 그리고 왜 이 방법인가 + +두 가지를 고쳤고, 역할이 다릅니다. + +**첫째, 근본 원인.** 알림 시각을 계산해서 넘기는 규칙을 **함수 하나로 통일**하고, 그 함수가 항상 시간대 표시를 붙이도록 했습니다. 등록하는 쪽과 발송하는 쪽이 같은 함수를 쓰기 때문에, 두 곳의 해석이 갈라질 자리 자체가 없어졌습니다. 이번 버그가 정확히 "두 곳이 같은 숫자를 서로 다른 기준으로 읽은" 버그였습니다. + +**둘째, 방어망.** 발송 직전에 "지금이 알림을 보내기에 말이 되는 시간인가"를 확인합니다. 오전 8시부터 밤 9시 사이가 아니면 발송을 보류하고 경고를 올립니다. 새벽 발송은 그 자체로 계산이 어디선가 틀렸다는 신호이기 때문입니다. + +방어망만 두지 않은 이유는, 그러면 시각 계산은 계속 틀린 채로 방어망이 조용히 뒤치다꺼리하는 상태가 굳어지기 때문입니다. 보류된 알림을 누가 언제 다시 보낼지가 새 문제로 남고, 방어망에 구멍이 생기는 날 아무 경고 없이 새벽 문자로 돌아갑니다. 반대로 근본 수정만 두지 않은 이유는, 오늘 사고의 뿌리가 "교체하면서 표시 한 줄 빠뜨리기"였기 때문입니다. 다음 교체 때 같은 실수를 안 한다는 건 사람의 주의력에 거는 기대이지, 보장으로 쓸 수 있는 종류의 것이 아닙니다. + +### 📊 숫자 + +| 항목 | 수정 전 | 수정 후 | +|---|---|---| +| 알림 발송 시각 | 예정보다 9시간 늦음 — 다음 날 새벽 5시 | 예정대로 저녁 8시 (테스트 2건 실측) | +| 새벽에 나간 문자 | 12건 (교체 후 발송된 알림 12건 전부) | 0건 | +| 발송 대기 중이던 틀린 알림 | 29건 | 0건 — 전부 재계산 | +| 신규 테스트 | — | 8개 추가, 전부 통과 | + +### 🧪 검증된 것 / 추정 / 확인 못 한 것 + +**검증됨:** 테스트 예약 2건의 알림 시각을 몇 분 뒤로 잡아 실제 발송까지 돌렸고, 두 건 모두 제시간에 왔습니다. 대기 중이던 29건은 재계산 후 전수 확인했습니다. + +**추정:** 카카오 알림톡 경로도 같이 고쳐졌을 겁니다. 같은 시각 계산 함수를 쓰기 때문인데, 실제로 발송까지 돌려본 건 문자뿐입니다. + +**확인 못 함:** 해외에 있는 손님. 오늘 확인한 예약은 전부 한국 시간대 손님이었습니다. 손님 시간대가 한국이 아닐 때의 동작은 별도 항목으로 적어뒀습니다. + +### 🚀 남은 것 / 필요한 결정 + +제가 알아서 할 것: 카카오 알림톡 경로로 한 건 실측, 해외 시간대 케이스 확인. + +결정이 필요한 것: 방어망이 알림을 보류한 다음입니다. 오전 8시가 되면 자동으로 재발송하는 방법과, 담당자가 확인한 뒤 보내는 방법이 있습니다. 전자는 빠르지만 계산이 틀린 알림이 또 그대로 나갈 수 있고, 후자는 안전하지만 사람 일이 하나 늘어납니다. 어느 쪽이 가게 운영에 맞는지는 제가 정할 문제가 아닌 것 같습니다. + +--- + +## What each one is doing + +Both reports use the same moves. Worth noticing where: + +| Move | Where it shows up | +|---|---| +| Intent-vs-outcome gap as the summary | Both open with "this was supposed to be X, it became Y" — the single most decision-relevant sentence available | +| Evidence quoted before it's explained | The log line appears raw, then gets unpacked, in both | +| Term paid for at first use, then spent | "event"/"handler" in A, "국제 표준시(UTC)" in B — one clause each, then used freely | +| Naming who behaved correctly | "the notification system was not broken" / "발송기 자체는 정확히 제 일을 했습니다" — stops the reader generalizing to distrust | +| The rejected alternative | A rejects pinning all servers to one library; B rejects having only the safety net. Both say what would have gone wrong | +| Roles for multiple changes | A labels root cause / visibility / cleanup; B labels root cause / defense | +| Explicit scope boundary | "only the confirmation email was affected" / "아직 발송 전이던 29건은 오늘 전부 바로잡아서, 추가 피해는 없습니다" | +| Denominators on every rate | "83%, over 284 orders" / "교체 후 발송된 알림 12건 전부" | +| A real gap volunteered | Refund confirmations in A; the customer-abroad time-zone case in B | +| A decision handed back, framed as theirs | The apology wording in A; auto-resend vs. human review for held notifications in B | + +Notice what neither does: no diffs, no file trees, no list of changed files, no apology for the bug, and no sentence that starts by announcing that an explanation is coming.