Làm nội dung do AI tạo nghe tự nhiên, có cá tính, bớt sáo rỗng và bớt cảm giác máy móc.
---
name: "content-humanizer"
description: "Makes AI-generated content sound genuinely human — not just cleaned up, but alive. Use when content feels robotic, uses too many AI clichés, lacks personality, or reads like it was written by committee. Triggers: 'this sounds like AI', 'make it more human', 'add personality', 'it feels generic', 'sounds robotic', 'fix AI writing', 'inject our voice'. NOT for initial content creation (use content-production). NOT for SEO optimization (use content-production Mode 3)."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: marketing
updated: 2026-03-06
---
# Content Humanizer
You are an expert in authentic writing and brand voice. Your goal is to transform content that reads like it was generated by a machine — even when it technically was — into writing that sounds like a real person with real opinions, real experience, and real stakes in what they're saying.
This is not a cleaning service. You're not just removing "delve" and calling it a day. You're rebuilding the voice from the ground up.
## Before Starting
**Check for context first:**
If `marketing-context.md` exists, read it. It contains brand voice guidelines, writing examples, and the specific tone this brand uses. That context is your voice blueprint. Use it — don't improvise a voice when the brief already defines one.
Gather what you need before starting:
### What you need
- **The content** — paste the draft to humanize
- **Brand voice notes** — if no `marketing-context.md`, ask: "Is your voice direct/casual/technical/irreverent? Give me one example of writing you love."
- **Audience** — who reads this? (This changes what "human" sounds like)
- **Goal** — what should this piece do? (Knowing the goal tells you how much personality is appropriate)
One question if needed: "Before I rewrite this, give me an example of content you've written or read that felt right. Specific is better than descriptive."
## How This Skill Works
Three modes. Run them in sequence for a full transformation, or jump to the one you need:
### Mode 1: Detect — AI Pattern Analysis
Audit the content for AI tells. Name what's wrong and why before fixing anything. This is diagnostic — not editorial.
### Mode 2: Humanize — Pattern Removal and Rhythm Fix
Strip the AI patterns. Fix sentence rhythm. Replace generic with specific. The content starts sounding like a person.
### Mode 3: Voice Injection — Brand Character
Now that the generic is gone, inject the brand's specific personality. This is where "human" becomes *your brand's* human.
Run all three in one pass when you have enough context. Split them when the client needs to see the audit before you edit.
---
## Mode 1: Detect — AI Pattern Analysis
Scan the content for these categories. Score severity: 🔴 critical (kills credibility) / 🟡 medium (softens impact) / 🟢 minor (polish only).
See [references/ai-tells-checklist.md](references/ai-tells-checklist.md) for the comprehensive detection list.
### The Core AI Tell Categories
**1. Overused Filler Words** 🔴
The model loves certain words because they appear frequently in its training data. Flag these on sight:
- "delve," "delve into," "delve deeper"
- "landscape" (as in "the current AI landscape")
- "crucial," "vital," "pivotal"
- "leverage" (when "use" works fine)
- "furthermore," "moreover," "in addition"
- "navigate" (metaphorical: "navigate this challenge")
- "robust," "comprehensive," "holistic"
- "foster," "facilitate," "ensure"
**2. Hedging Chains** 🔴
AI hedges constantly. It hedges because it doesn't know if it's right. Humans hedge sometimes — but not in every sentence.
- "It's important to note that..."
- "It's worth mentioning that..."
- "One might argue that..."
- "In many cases," "In most scenarios,"
- "It goes without saying..."
- "Needless to say..."
**3. Em-Dash Overuse** 🟡
One or two em-dashes in a piece: fine. Em-dash in every other paragraph: AI fingerprint. The model uses em-dashes to add clauses the way humans add breath — but it does it compulsively.
**4. Identical Paragraph Structure** 🔴
Every paragraph: topic sentence → explanation → example → bridge to next. AI is remarkably consistent. Remarkably boring. Real writing has short paragraphs. Fragments. Asides. Digressions. Then it snaps back. The structure varies.
**5. Lack of Specificity** 🔴
AI replaces specific claims with vague ones because specific claims can be wrong. Look for:
- "Many companies" → which companies?
- "Studies show" → which studies?
- "Significantly improved" → improved by how much?
- "Leading brands" → name one
- "A lot of" → how many?
**6. False Certainty / False Authority** 🟡
AI asserts confidently about things no one can be certain about. "Companies that do X are more successful." According to what? This isn't humility — it's laziness dressed as confidence.
**7. The "In conclusion" Paragraph** 🟡
AI conclusions are often carbon copies of the intro. "In this article, we explored X, Y, and Z. By implementing these strategies, you can achieve..." No human concludes like this. Real conclusions either add something new or nail the exit line.
---
## Mode 2: Humanize — Pattern Removal and Rhythm Fix
After identifying what's wrong, fix it systematically.
### Replace Filler Words
**Rule:** Never just delete — always replace with something better.
| AI phrase | Human alternative |
|---|---|
| "delve into" | "look at," "dig into," "break down," or just: "here's what matters" |
| "the [X] landscape" | "how [X] works today," "the current state of [X]" |
| "leverage" | "use," "apply," "put to work" |
| "crucial" / "vital" | "the part that actually matters," "the one thing," or just state the thing — let it be self-evidently important |
| "furthermore" | nothing (just start the next sentence), or "and," or "also" |
| "robust" | specific: "handles 10,000 requests/sec," "covers 47 edge cases" |
| "facilitate" | "help," "make easier," "allow" |
| "navigate this challenge" | "handle this," "deal with this," "get through this" |
### Fix Sentence Rhythm
**The problem:** AI produces uniform sentence length. Every sentence is 18-22 words. The ear goes numb.
**The fix:** Deliberate variation. Read aloud. Then:
- Break long sentences into two
- Add a short sentence after a long one. Like this.
- Use fragments where they serve emphasis. Especially for emphasis.
- Let some sentences run longer when the thought needs to unwind and the reader has the context to follow it
**Rhythm patterns that feel human:**
- Long. Short. Long, long. Short.
- Question? Answer. Proof.
- Claim. Specific example. So what?
### Replace Generic with Specific
Every vague claim is an invitation to doubt. Replace:
**Before:** "Many companies have seen significant improvements by implementing this strategy."
**After:** "HubSpot published their onboarding funnel data in 2023 — companies that hit their first-value moment within 7 days showed 40% higher 90-day retention. That's not a rounding error."
If you don't have specific data, be honest: "I haven't seen controlled studies on this, but in my experience working with SaaS onboarding flows, the pattern is consistent: earlier activation = higher retention."
Personal experience beats vague authority. Every time.
### Vary Paragraph Structure
Break the uniform SEEB pattern (Statement → Explanation → Example → Bridge):
- **Single-sentence paragraph:** Use it. Emphasis needs air.
- **Question paragraph:** Pose a question. Then answer it.
- **List in the middle:** Drop a quick list when there are genuinely 3-5 parallel items. Then return to prose.
- **Aside / parenthetical paragraph:** A small digression that reveals personality. (Readers actually like these. It's the equivalent of a raised eyebrow mid-sentence.)
- **Confession:** "I got this wrong the first time." Instantly human.
### Add Friction and Imperfection
AI writing is too smooth. Too complete. Real people:
- Change direction mid-thought and acknowledge it: "Actually, let me back up..."
- Qualify things they're uncertain about without hiding the uncertainty
- Have opinions that might be wrong: "I might be wrong about this, but..."
- Notice things and say so: "What's interesting here is..."
- React: "Which, if you've ever tried to debug this, you know is maddening."
---
## Mode 3: Voice Injection — Brand Character
Humanizing removes AI. Voice injection makes it *yours*.
### Read the Voice Blueprint First
If `marketing-context.md` is available: read the brand voice section and writing examples. If not, ask for one example of content this brand loves. One. Then extract the patterns from it.
**What to extract from a voice example:**
- Sentence length preference (short punchy vs. longer flowing?)
- Formality level (contractions? slang? industry jargon?)
- Use of humor (dry wit? self-deprecating? none?)
- Relationship stance (peer-to-peer? expert-to-student? provocateur?)
- Signature phrases or patterns
See [references/voice-techniques.md](references/voice-techniques.md) for specific techniques for each voice type.
### Voice Injection Techniques
**1. Personal Anecdotes**
Even branded content gets more credible when grounded in experience. "We saw this firsthand when building X" is worth more than any study citation.
**2. Direct Address**
Talk to the reader as "you." Not "users" or "teams" or "organizations." You.
**3. Opinions Without Apology**
State your position. "We think the industry is wrong about this" is more credible than "there are various perspectives." Take the side.
**4. The Aside**
A brief parenthetical that shows the brand knows more than it's saying. "This also affects API performance, but that's a separate rabbit hole."
**5. Rhythm Signature**
Every brand has a rhythm. Some write in short staccato bursts. Some write long, winding sentences that spiral back on themselves. Find the rhythm from the examples and apply it consistently.
### Before / After Example
**Before (AI-generated):**
> It is crucial to leverage your existing customer data in order to effectively navigate the competitive landscape. Furthermore, by implementing a robust onboarding strategy, organizations can ensure that users achieve maximum value from the product and reduce churn significantly.
**After (humanized):**
> Here's the thing nobody says out loud: most SaaS companies have the data to fix their churn problem. They just don't look at it until after customers leave.
>
> Your activation funnel is in there. Your best cohorts, your worst, the moment the drop-off happens. You don't need another tool — you need someone to stop ignoring what the tool is already showing you.
>
> Nail onboarding first. Everything else is downstream.
What changed:
- Removed: "crucial," "leverage," "navigate," "robust," "ensure," "significantly," "furthermore"
- Added: direct address, specific accusation ("what the tool is already showing you"), short-sentence punch at the end
- Changed: passive recommendations → active point of view
---
## Proactive Triggers
Flag these without being asked:
- **AI fingerprint density too high** — If the piece has 10+ AI tells per 500 words, a patch job won't work. Flag that the piece needs a full rewrite, not an edit. Trying to polish a piece that's 80% AI patterns produces AI patterns with nicer words.
- **Voice context missing** — If `marketing-context.md` doesn't exist and the user hasn't given voice guidance, pause before injecting voice. Ask for one example. Guessing the voice and being wrong wastes everyone's time.
- **Specificity gap** — If the piece makes 5+ vague claims with zero data or attribution, flag it to the user. You can make the prose flow better, but you can't invent specific proof. They need to provide it.
- **Tone mismatch after humanizing** — If the piece is now genuinely human but sounds like a different brand than everything else the client publishes, flag it. Consistency matters as much as quality.
- **Over-editing risk** — If the original content has one or two genuinely good paragraphs buried in the AI mush, flag them before rewriting. Don't accidentally destroy the good parts.
---
## Output Artifacts
| When you ask for... | You get... |
|---|---|
| AI audit | Annotated version of the draft with each AI pattern flagged, severity score, and count by category |
| Humanized draft | Full rewrite with AI patterns removed, rhythm varied, specificity improved |
| Voice injection | Annotated draft with brand voice applied — specific changes called out so you can learn the pattern |
| Before/after comparison | Side-by-side view of key paragraphs showing what changed and why |
| Humanity score | Run `scripts/humanizer_scorer.py` — 0-100 score with breakdown by signal type |
---
## Communication
All output follows the structured standard:
- **Bottom line first** — answer before explanation
- **What + Why + How** — every finding includes all three
- **Actions have owners and deadlines** — no "you might want to consider"
- **Confidence tagging** — 🟢 verified pattern / 🟡 medium / 🔴 assumed based on limited voice context
When auditing: name the pattern → explain why it reads as AI → give the specific fix. Not "this sounds robotic." Say: "Paragraph 4 opens with 'It is important to note that' — this is a pure hedge. Cut it. Start with the actual note."
---
## Related Skills
- **content-production**: Use to produce the initial draft. Run content-humanizer after drafting, before the SEO optimization pass.
- **copywriting**: Use for conversion copy — landing pages, CTAs, headlines. content-humanizer works on longer-form pieces; copywriting handles short punchy copy with different principles.
- **content-strategy**: Use when deciding what content to create. NOT for voice or draft execution.
- **ai-seo**: Use after humanizing, to optimize for AI search citation. Human-sounding content gets cited more — but it still needs structure to get extracted.
FILE:references/ai-tells-checklist.md
# AI Tells Checklist
A comprehensive reference for detecting AI-generated or AI-assisted writing patterns. Use this during Mode 1 (Detect) to audit content before editing.
Rate each finding: 🔴 Critical (rewrites required) / 🟡 Medium (edits needed) / 🟢 Minor (polish)
---
## Category 1: Overused Vocabulary
These words appear in AI output at 5-20x the frequency of human writing. One instance is fine. Multiple instances in a single piece is a tell.
### Red-flag verbs (overused)
| Word | Problem | What humans say instead |
|---|---|---|
| delve / delve into | Pretentious filler | look at, dig into, explore, examine |
| leverage | Jargon for "use" | use, apply, put to work, capitalize on |
| foster | Formal filler | build, develop, encourage, create |
| facilitate | Bureaucratic filler | help, make easier, allow, support |
| navigate | Overused metaphor | handle, manage, work through, deal with |
| ensure | Empty guarantee | check that, make sure, verify |
| utilize | Formal for "use" | just say "use" |
| prioritize | Often redundant | just say what to do first |
| streamline | Vague improvement promise | be specific about what gets faster/easier |
### Red-flag adjectives (overused)
| Word | Problem | What humans say instead |
|---|---|---|
| crucial / vital / pivotal | Overloaded intensifiers | just show why it matters |
| robust | Vague positive | specific: "handles X load," "covers Y cases" |
| comprehensive | Overpromise | specific: "covers the 5 most common..." |
| innovative | Meaningless | say what it actually does differently |
| holistic | Buzzword | say what it covers |
| seamless | Overused product adjective | describe the actual experience |
| cutting-edge | Dated marketing | say what's new or different specifically |
| dynamic | Filler adjective | usually just delete it |
### Red-flag nouns (overused)
| Word | Problem | What humans say instead |
|---|---|---|
| landscape | Overused metaphor | "how X works today," "the state of X" |
| ecosystem | Overused for "industry" or "community" | be specific |
| framework | Often vague | name the specific framework or approach |
| paradigm | Academic filler | usually replaceable with "approach" or "way of thinking" |
| synergy | Corporate cliché | delete or replace with what actually happens |
---
## Category 2: Hedging and Qualification Patterns
Humans hedge. AI hedges compulsively. The difference is frequency and context.
### Opening hedges (usually safe to cut)
- "It's important to note that..."
- "It's worth mentioning that..."
- "It should be noted that..."
- "Needless to say..."
- "It goes without saying..."
- "Of course, ..."
- "Naturally, ..."
### Mid-sentence hedges (examine each)
- "In many cases" / "In most instances" / "In certain scenarios"
- "Generally speaking," / "For the most part,"
- "This may vary depending on..."
- "Results may differ based on..."
- "While this isn't always the case..."
**Diagnostic:** If the hedge is protecting a claim that should just be a claim, cut the hedge and state the claim. If the hedge reflects genuine uncertainty, keep it — but make the uncertainty specific: "I don't have data for this, but based on [context]..."
### Vague authority claims (replace with specifics)
- "Studies show..." → "A 2023 McKinsey study of 400 SaaS companies showed..."
- "Research suggests..." → same — name the research
- "Many companies..." → "HubSpot, Slack, and several bootstrapped SaaS founders we've talked to..."
- "Experts agree..." → name one expert
- "It has been shown that..." → by whom, when, where
---
## Category 3: Structural Patterns
### The SEEB paragraph (most common)
Pattern: Statement → Explanation → Example → Bridge to next topic
When every paragraph follows this exact structure, the writing reads like a machine assembled it. Because it was.
**Fix:** Mix in fragments. Questions. Short paragraphs. Asides. Let sections breathe differently.
### Parallelism overload
AI loves parallel structure. Three-item lists. Four-item lists. Everything in threes. Alliteration sometimes.
Occasional parallelism is powerful. Three consecutive bulleted lists of three items each is a tell.
**Fix:** Break the rhythm. Some sections need bullets. Most don't.
### The summary conclusion
AI conclusions restate the introduction. Paragraph by paragraph. "In this guide, we covered X, Y, and Z. By applying these strategies, you can achieve [thing from the intro]."
Human conclusions either add something (a new angle, an honest admission, a call to action that feels earned) or they nail the exit line and stop.
### Symmetric section lengths
Every H2 section is roughly the same length. ~300 words. Every one. That's a machine maintaining consistency. Humans have opinions — some things deserve more space, some less.
---
## Category 4: Punctuation and Formatting Tells
### Em-dash frequency 🟡
One or two em-dashes per piece: fine. Three per page: suspicion. Five+: AI fingerprint.
The em-dash is AI's favorite way to add subordinate clauses — like this — because it sounds sophisticated — and it learned this from a lot of well-written text — so now it can't stop.
**Fix:** Replace most with periods. Break into shorter sentences.
### Colon-then-list patterns 🟢
AI frequently: introduces a list: with: colons. Multiple times per section. Lists are useful. But if every section has a colon-introducing list, it's mechanical.
### Excessive bold 🟢
AI sometimes bolds every important-sounding phrase in a paragraph. **This results** in **too many** phrases being **highlighted for emphasis** when the emphasis **dilutes itself**.
Bold should be used sparingly — for the one thing that matters most in a section, not for three to five things per paragraph.
---
## Category 5: Tonal Tells
### False warmth 🟡
"We hope this guide has been helpful in your journey to..."
"We trust that you've found valuable insights in..."
"It's our sincere hope that these strategies will empower you to..."
No one talks like this. It's the corporate newsletter voice. Cut it.
### Emotional escalation without basis 🟡
AI sometimes starts clinical and then, near the end, gets unexpectedly warm and inspirational. "Now that you have these tools, you can transform your business and achieve your goals." The warmth wasn't earned by the preceding content.
### Artificial enthusiasm 🟢
"Exciting developments," "fascinating case study," "incredible opportunity" — used to simulate human engagement. Usually reads as hollow because it's disconnected from actual content that earns those descriptors.
---
## Quick Audit Scoring
For a 1,000-word piece, count:
| Signal | Count | Severity Threshold |
|---|---|---|
| Red-flag vocabulary words | | >5 = 🔴 |
| Opening hedges | | >2 = 🔴 |
| Vague authority claims ("studies show") | | >2 = 🔴 |
| Em-dashes | | >4 = 🟡 |
| SEEB paragraphs (all follow same structure) | | >60% of paragraphs = 🔴 |
| Instances of false warmth | | >1 = 🟡 |
**Scoring:**
- 0-5 total flags: Light edit (Mode 2 quick pass)
- 6-12 total flags: Full humanize pass (Mode 2 complete)
- 12+ total flags: Full rewrite recommended (Mode 1 audit → complete Mode 2 → Mode 3)
FILE:references/voice-techniques.md
# Voice Techniques Reference
Techniques for injecting authentic brand voice into content. This is the Mode 3 playbook — after you've removed AI patterns (Mode 2), these techniques put the brand's specific personality in.
---
## Step 1: Extract the Voice Profile
Before writing anything, extract the voice from examples. Ask for one piece of content the brand considers representative. Then answer these questions about it:
| Question | What to look for |
|---|---|
| Average sentence length? | Count words per sentence across 10 sentences. ≤15 = punchy. ≥25 = flowing. |
| Formality level? | Count contractions ("it's" vs "it is"). Count first-person. Count slang. |
| Use of humor? | Dry wit (unexpected juxtapositions). Self-deprecating (acknowledges own limitations). Provocateur (picks fights). None. |
| Relationship to reader? | Peer (we're both figuring this out). Expert (I know, you're learning). Challenger (you're probably wrong about this). |
| Signature phrases? | Phrases or constructions that appear more than once. That's the brand's verbal tic. |
| What do they avoid? | Listen for what's conspicuously absent. |
Document this as a voice profile before editing. Reference it throughout.
---
## The Core Voice Types
### Voice Type 1: The Direct Expert
**Profile:** Short sentences. Confident claims. No hedging. No qualifications unless the qualification is the point. Reads like advice from the smartest person in the room who also has the least patience.
**Natural habitat:** Developer tools, technical documentation, no-fluff B2B brands, opinionated founders.
**Techniques:**
- Lead every section with the conclusion, not the setup
- Make claims without softening them: "Most onboarding is broken" not "Many onboarding flows may have room for improvement"
- Use "you" relentlessly — direct address, always
- Short paragraphs. Often 1-2 sentences.
- When listing, use the smallest number of words possible
- No rhetorical questions unless you're about to immediately answer them
- Swear if the brand does (a well-placed "this is genuinely terrible" signals authenticity)
**Before:** "It's important to consider that implementing an effective onboarding strategy can have significant positive impacts on user retention rates."
**After:** "Fix your onboarding. It's where you're losing them."
---
### Voice Type 2: The Thoughtful Peer
**Profile:** Longer sentences when the thought requires it. Occasional vulnerability. Shares the thinking, not just the conclusion. Treats the reader as someone at roughly the same level.
**Natural habitat:** B2B SaaS with community focus, content brands, newsletter-first companies, founder blogs.
**Techniques:**
- Show the reasoning: "Here's how I think about this..." not just the conclusion
- Acknowledge what you don't know: "I don't have solid data on this, but my read is..."
- Share the mistake before the lesson: "We tried X first and it failed. Here's what we learned."
- Longer transitions: connect ideas, don't just jump to the next point
- Questions that earn their keep — rhetorical only when you're genuinely pointing at a tension
- First person plural ("we") where appropriate — but only if there actually is a "we"
**Before:** "Companies should invest in customer success to improve retention."
**After:** "I spent two years thinking retention was a marketing problem. It wasn't. Every company we talked to that had great retention had one thing in common: they treated customer success like a product team, not a support team. The product mindset made all the difference."
---
### Voice Type 3: The Provocateur
**Profile:** Challenges received wisdom. Takes positions most people avoid. Enjoys being right when everyone else was wrong. Not confrontational for its own sake — has actual conviction behind the provocation.
**Natural habitat:** Opinionated SaaS, contrarian analyst voices, certain agency brands, fast-growing startups trying to differentiate.
**Techniques:**
- Open with the unpopular claim: "Most SEO advice is wrong" not "There are many perspectives on SEO"
- Name the thing people don't say: "The reason nobody talks about X is because..."
- Acknowledge the argument against your position before making yours stronger
- Use "actually" strategically: "What actually happens is..."
- When everyone agrees on something, be specific about what they're getting wrong
- Let the evidence land without fanfare — state the uncomfortable number, then wait
**Before:** "There are various approaches to content marketing that teams may find valuable."
**After:** "Your competitors' content marketing budgets are wasted. The average B2B blog post gets 300 views and zero conversions. The top 10% of pieces generate 90% of the value. Most teams don't know which 10% they have — which means they're spending 90% of their budget on content that will never pay back."
---
### Voice Type 4: The Enthusiastic Practitioner
**Profile:** Genuine excitement about the craft. Energized by the detail. The person who could talk about their domain for hours and you'd actually enjoy it.
**Natural habitat:** Creative tools, marketing tech, community platforms, brands with a strong practitioner user base.
**Techniques:**
- Allow enthusiasm to come through without forcing it: "This is the part I love about cohort analysis..."
- Share the unexpected detail that only a practitioner would notice
- Use specific terminology confidently — don't over-explain to practitioners
- "The thing is..." as a transition — signals you're about to share the real insight
- Parenthetical asides that reveal depth: "(And yes, I know about [edge case] — that's a separate problem)"
- Let the complexity show when the complexity is interesting
**Before:** "Segmenting your audience allows for more targeted communication."
**After:** "Here's where audience segmentation gets genuinely interesting. Most people segment by company size or industry. That's fine. But the cohort that always outperforms is behavioral — people who hit your activation event within 72 hours. That cohort has 2-3x the LTV of the same-company-size cohort that took two weeks. The behavior tells you more than the firmographic ever will."
---
## Specific Techniques Regardless of Voice Type
### The Personal Anecdote Injection
Every brand can use a personal anecdote — even B2B technical brands. The format:
**Setup:** "When we [did X thing in context]..."
**What happened:** "[Specific unexpected thing occurred]."
**The lesson:** "[The principle that explains it]."
The anecdote doesn't have to be dramatic. It just has to be real and specific. "When we rebuilt our onboarding flow in 2023, we expected the copy to be the problem. It was the loading time."
### The Honest Admission
Instantly humanizing. Rarely done by brands because it feels like weakness. But readers trust brands that admit:
- What they got wrong
- What they don't know
- What they tried that failed
- What surprised them
The format: "We thought X. We were wrong. What actually happened was Y."
### The Contrarian Setup
Works for any voice type. Setup: state the conventional wisdom. Then undercut it.
"Everyone says [thing]. The data shows [opposite thing]."
Or more subtle: "The conventional wisdom is [X]. That's mostly true. Except when [specific condition] — and that's where it gets interesting."
### Sentence Rhythm Signature
After analyzing the brand voice, identify the rhythm pattern and apply it:
- **Staccato brand:** Short sentence. Shorter. Done.
- **Flowing brand:** Content that allows ideas to build on each other, connect, and arrive somewhere richer than where they started.
- **Mixed cadence (most natural):** Long sentence establishing context. Short punch. Then another long one that goes somewhere. Then: done.
Once you know the pattern, apply it consistently throughout the piece.
### The Earned Ending
The last sentence of a piece should land. Not summarize. Not encourage. Land.
Techniques:
- **The hard cut:** State the core truth one final time, nakedly. No wrap-up.
- **The reversal:** Setup expectation, then subvert it in the last line.
- **The action:** Name the single most important thing to do next. One thing.
- **The honest admission:** "We're still figuring this out too."
Avoid: "By following these steps, you'll be well on your way to achieving your goals." Nobody reads that. Nobody trusts it.
---
## Voice Injection Quality Check
After applying voice:
- [ ] Read the piece aloud — does it sound like a person you'd actually follow?
- [ ] Is every paragraph distinguishable from the others? (Not just same rhythm, same length, same pattern)
- [ ] Is there at least one moment where the brand's specific personality is unmistakable?
- [ ] Would someone who knows this brand's other content recognize this as theirs?
- [ ] Is there at least one place where the brand says something most brands wouldn't?
FILE:scripts/humanizer_scorer.py
#!/usr/bin/env python3
"""humanizer_scorer.py — scores content 0-100 on 'humanity' by detecting AI writing patterns."""
import sys
import re
import json
import math
from collections import Counter
# ── Sample content for zero-config demo ──────────────────────────────────────
SAMPLE_HUMAN = """
We tried to fix our churn problem the wrong way for about a year.
We threw money at marketing, assumed acquisition would outpace loss, and avoided looking at the actual numbers. It didn't work. Churn stayed flat at 8% monthly, which sounds manageable until you realize that's 65% annual churn. We were filling a leaky bucket with a garden hose.
The breakthrough — if you can call it that — was embarrassingly simple: we actually talked to the customers who left.
Not the ones who complained. The ones who quietly disappeared. We called 30 churned accounts over two weeks. You know what most of them said? They didn't hate the product. They just... forgot about it. It was solving a problem they cared about once, and then stopped caring about.
So we rebuilt our onboarding around one question: what would make this impossible to ignore? Not "valuable" — people know it's valuable. Impossible to ignore.
Three months later, 30-day activation was up 40%. Churn dropped to 4.5%.
The lesson wasn't about product or pricing. It was about habit formation. And we were terrible at it.
"""
SAMPLE_AI = """
It is crucial to leverage data-driven insights in order to effectively navigate the challenges of customer retention in the competitive SaaS landscape. Furthermore, by implementing robust onboarding strategies, organizations can ensure that users achieve maximum value from the product, thereby significantly reducing churn rates.
To facilitate this process, it's important to note that companies should delve into their customer behavior data to identify patterns and trends. Moreover, by fostering meaningful connections with customers and ensuring comprehensive support throughout their journey, businesses can cultivate lasting relationships that drive long-term success.
In conclusion, the implementation of these holistic strategies will empower organizations to streamline their customer success operations and achieve sustainable growth in an increasingly competitive marketplace.
"""
# ── AI vocabulary signals ─────────────────────────────────────────────────────
AI_VOCABULARY = [
# The notorious list
"delve", "delve into", "delves", "delving",
"landscape",
"crucial", "vital", "pivotal",
"leverage", "leveraging", "leveraged",
"robust",
"comprehensive",
"holistic",
"foster", "fosters", "fostering",
"facilitate", "facilitates", "facilitating",
"navigate", "navigating",
"ensure", "ensures", "ensuring",
"utilize", "utilizing", "utilizes",
"furthermore", "moreover",
"innovative", "cutting-edge",
"seamless", "seamlessly",
"empower", "empowers", "empowering",
"streamline", "streamlines", "streamlining",
"cultivate", "cultivating",
"paradigm",
"ecosystem",
"synergy",
"in conclusion",
"in summary",
"to summarize",
]
HEDGING_PHRASES = [
"it is important to note",
"it's important to note",
"it should be noted",
"it is worth mentioning",
"it's worth mentioning",
"it goes without saying",
"needless to say",
"in many cases",
"in most cases",
"in certain cases",
"in most instances",
"in many instances",
"generally speaking",
"for the most part",
"this may vary",
"results may differ",
"one might argue",
"it can be argued",
"there are various",
"there are many",
"it is crucial to",
"it's crucial to",
]
PASSIVE_PATTERNS = [
r'\b(is|are|was|were|be|been|being)\s+(being\s+)?\w+ed\b',
r'\b(can|could|should|would|may|might|must)\s+be\s+\w+ed\b',
]
VAGUE_AUTHORITY = [
"studies show",
"research suggests",
"research shows",
"experts agree",
"experts say",
"many companies",
"leading brands",
"it has been shown",
"according to research",
"data suggests",
"evidence suggests",
]
# ── Scoring functions ─────────────────────────────────────────────────────────
def score_ai_vocabulary(text: str) -> dict:
"""Score 0-25: fewer AI words = higher score."""
text_lower = text.lower()
words_total = max(1, len(re.findall(r'\b\w+\b', text)))
hits = []
for phrase in AI_VOCABULARY:
count = text_lower.count(phrase)
if count > 0:
hits.append((phrase, count))
total_hits = sum(c for _, c in hits)
density = total_hits / (words_total / 100) # per 100 words
# Score: 0 hits = 25, scales down
if total_hits == 0:
score = 25
elif total_hits <= 2:
score = 20
elif total_hits <= 5:
score = 14
elif total_hits <= 10:
score = 8
elif total_hits <= 15:
score = 3
else:
score = 0
return {
"score": score,
"max": 25,
"ai_word_hits": total_hits,
"density_per_100_words": round(density, 2),
"flagged_terms": [f for f, _ in hits[:10]], # top 10 for display
}
def score_sentence_variance(text: str) -> dict:
"""Score 0-20: high variance = more human (robots use uniform length)."""
sentences = re.split(r'[.!?]+', text)
sentences = [s.strip() for s in sentences if len(s.split()) >= 3]
if len(sentences) < 3:
return {"score": 10, "max": 20, "std_dev": 0, "avg_length": 0, "note": "too few sentences to score"}
lengths = [len(s.split()) for s in sentences]
avg = sum(lengths) / len(lengths)
variance = sum((l - avg) ** 2 for l in lengths) / len(lengths)
std_dev = math.sqrt(variance)
# Good human writing has std_dev of 8-15 for mixed content
if std_dev >= 12:
score = 20
elif std_dev >= 8:
score = 16
elif std_dev >= 5:
score = 10
elif std_dev >= 3:
score = 5
else:
score = 0 # very robotic: all sentences same length
return {
"score": score,
"max": 20,
"std_dev": round(std_dev, 1),
"avg_length": round(avg, 1),
"min_length": min(lengths),
"max_length": max(lengths),
}
def score_passive_voice(text: str) -> dict:
"""Score 0-20: less passive = more human."""
sentences = re.split(r'[.!?]+', text)
sentences = [s.strip() for s in sentences if s.strip()]
n_sentences = max(1, len(sentences))
passive_count = 0
for pattern in PASSIVE_PATTERNS:
passive_count += len(re.findall(pattern, text, re.IGNORECASE))
passive_ratio = passive_count / n_sentences
if passive_ratio < 0.1:
score = 20
elif passive_ratio < 0.2:
score = 16
elif passive_ratio < 0.3:
score = 10
elif passive_ratio < 0.4:
score = 5
else:
score = 0
return {
"score": score,
"max": 20,
"passive_count": passive_count,
"passive_ratio": round(passive_ratio, 2),
"passive_pct": f"{round(passive_ratio * 100)}%",
}
def score_hedging(text: str) -> dict:
"""Score 0-15: fewer hedges = more direct = more human."""
text_lower = text.lower()
hits = []
for phrase in HEDGING_PHRASES:
count = text_lower.count(phrase)
if count > 0:
hits.append((phrase, count))
total_hedges = sum(c for _, c in hits)
if total_hedges == 0:
score = 15
elif total_hedges == 1:
score = 12
elif total_hedges == 2:
score = 8
elif total_hedges == 3:
score = 4
else:
score = 0
vague_hits = sum(text_lower.count(p) for p in VAGUE_AUTHORITY)
return {
"score": score,
"max": 15,
"hedge_count": total_hedges,
"vague_authority_count": vague_hits,
"flagged_phrases": [f for f, _ in hits],
}
def score_em_dashes(text: str) -> dict:
"""Score 0-10: moderate em-dash use is fine; overuse is a tell."""
# Count em-dashes (—) and double-hyphen (--) used as em-dash
em_count = text.count('—') + text.count('--')
word_count = max(1, len(re.findall(r'\b\w+\b', text)))
per_100 = em_count / (word_count / 100)
if per_100 < 0.5:
score = 10 # none or very rare: fine
elif per_100 < 1.5:
score = 8 # occasional: good
elif per_100 < 3:
score = 5 # frequent: suspicious
elif per_100 < 5:
score = 2 # overuse: likely AI
else:
score = 0 # compulsive: AI fingerprint
return {
"score": score,
"max": 10,
"em_dash_count": em_count,
"per_100_words": round(per_100, 2),
}
def score_paragraph_variety(text: str) -> dict:
"""Score 0-10: varied paragraph lengths = more human."""
paragraphs = [p.strip() for p in text.split('\n\n') if p.strip() and not p.startswith('#')]
if len(paragraphs) < 3:
return {"score": 5, "max": 10, "note": "too few paragraphs to score"}
lengths = [len(p.split()) for p in paragraphs]
avg = sum(lengths) / len(lengths)
variance = sum((l - avg) ** 2 for l in lengths) / len(lengths)
std_dev = math.sqrt(variance)
# Has any single-sentence paragraphs? (hallmark of human writing)
has_short = any(l <= 15 for l in lengths)
has_long = any(l >= 80 for l in lengths)
score = 0
if std_dev >= 30:
score += 5
elif std_dev >= 15:
score += 3
elif std_dev >= 5:
score += 1
if has_short:
score += 3
if has_long and std_dev >= 15:
score += 2
score = min(10, score)
return {
"score": score,
"max": 10,
"paragraph_count": len(paragraphs),
"paragraph_std_dev": round(std_dev, 1),
"has_short_paragraphs": has_short,
"avg_paragraph_words": round(avg, 1),
}
# ── Main scoring ──────────────────────────────────────────────────────────────
def score_humanity(text: str) -> dict:
vocab = score_ai_vocabulary(text)
variance = score_sentence_variance(text)
passive = score_passive_voice(text)
hedging = score_hedging(text)
em = score_em_dashes(text)
paragraphs = score_paragraph_variety(text)
total = vocab["score"] + variance["score"] + passive["score"] + hedging["score"] + em["score"] + paragraphs["score"]
if total >= 85:
label = "Sounds human ✅"
elif total >= 70:
label = "Mostly human — light edits needed"
elif total >= 50:
label = "Mixed — AI patterns detectable"
elif total >= 30:
label = "Robotic — significant rewrite needed"
else:
label = "AI fingerprint — full rewrite required 🔴"
return {
"humanity_score": total,
"label": label,
"sections": {
"ai_vocabulary": vocab,
"sentence_variance": variance,
"passive_voice": passive,
"hedging": hedging,
"em_dashes": em,
"paragraph_variety": paragraphs,
}
}
def print_report(result: dict, label: str = "") -> None:
total = result["humanity_score"]
verdict = result["label"]
s = result["sections"]
bar_filled = int(total / 5)
bar = "█" * bar_filled + "░" * (20 - bar_filled)
print()
print("╔══════════════════════════════════════════╗")
print("║ HUMANIZER SCORER — REPORT ║")
print("╚══════════════════════════════════════════╝")
if label:
print(f" Input: {label}")
print()
print(f" HUMANITY SCORE: {total}/100")
print(f" [{bar}]")
print(f" Verdict: {verdict}")
print()
print(" ── Section Breakdown ──────────────────────")
sections = [
("AI Vocabulary", s["ai_vocabulary"], 25),
("Sentence Variance", s["sentence_variance"], 20),
("Passive Voice", s["passive_voice"], 20),
("Hedging Phrases", s["hedging"], 15),
("Em-Dash Use", s["em_dashes"], 10),
("Paragraph Variety", s["paragraph_variety"], 10),
]
for name, sec, mx in sections:
sc = sec["score"]
bar2 = "█" * int(sc / mx * 10) + "░" * (10 - int(sc / mx * 10))
print(f" {name:<20} {sc:>2}/{mx} [{bar2}]")
print()
print(" ── Detected Issues ────────────────────────")
v = s["ai_vocabulary"]
if v["ai_word_hits"] > 0:
terms = ", ".join(v["flagged_terms"][:5])
print(f" 🔴 AI vocabulary: {v['ai_word_hits']} hits — [{terms}]")
else:
print(" ✅ No AI vocabulary detected")
sv = s["sentence_variance"]
if sv["std_dev"] < 5:
print(f" 🔴 Sentence rhythm robotic — std dev only {sv['std_dev']} (target: 8+)")
elif sv["std_dev"] < 8:
print(f" 🟡 Sentence variance low — {sv['std_dev']} (target: 8+)")
else:
print(f" ✅ Sentence variance good — {sv['std_dev']}")
pv = s["passive_voice"]
if pv["passive_ratio"] > 0.3:
print(f" 🔴 Passive voice overuse — {pv['passive_pct']} of sentences")
elif pv["passive_ratio"] > 0.2:
print(f" 🟡 Passive voice elevated — {pv['passive_pct']}")
else:
print(f" ✅ Passive voice in range — {pv['passive_pct']}")
hg = s["hedging"]
if hg["hedge_count"] > 2:
terms = ", ".join(hg["flagged_phrases"][:3])
print(f" 🔴 Hedging overload — {hg['hedge_count']} phrases: [{terms}]")
elif hg["hedge_count"] > 0:
print(f" 🟡 Hedging present — {hg['hedge_count']} phrase(s): {hg['flagged_phrases']}")
else:
print(" ✅ No hedging detected")
if hg["vague_authority_count"] > 0:
print(f" 🟡 Vague authority claims: {hg['vague_authority_count']} (e.g. 'studies show') — add citations")
em = s["em_dashes"]
if em["per_100_words"] > 3:
print(f" 🟡 Em-dash overuse — {em['em_dash_count']} in piece ({em['per_100_words']}/100 words)")
pg = s["paragraph_variety"]
if not pg.get("has_short_paragraphs"):
print(" 🟡 No short paragraphs found — add some 1-2 sentence paragraphs for rhythm")
print()
print(" ── Priority Fixes ─────────────────────────")
if v["ai_word_hits"] > 5:
print(" 1. Replace AI vocabulary (biggest impact)")
if sv["std_dev"] < 8:
print(" 2. Vary sentence length — mix short punchy sentences with longer ones")
if pv["passive_ratio"] > 0.25:
print(" 3. Flip passive sentences to active voice")
if hg["hedge_count"] > 2:
print(" 4. Cut hedging phrases — state claims directly")
if not pg.get("has_short_paragraphs"):
print(" 5. Add short paragraphs — even 1-sentence paragraphs help rhythm")
if total >= 85:
print(" ✅ No priority fixes — content reads as human")
print()
def main():
import argparse
parser = argparse.ArgumentParser(
description="Scores content 0-100 on 'humanity' by detecting AI writing patterns. "
"Checks AI vocabulary, sentence variance, passive voice, hedging, "
"em-dash overuse, and paragraph variety."
)
parser.add_argument(
"file", nargs="?", default=None,
help="Path to a text file to analyze. If omitted, runs demo comparing "
"human vs AI sample content."
)
parser.add_argument(
"--json", action="store_true",
help="Also output results as JSON."
)
args = parser.parse_args()
if args.file is None:
# Demo mode: compare human vs AI sample
print("[Demo mode — comparing human vs AI sample content]")
print()
print("═" * 50)
print("SAMPLE 1: Human-written content")
print("═" * 50)
r1 = score_humanity(SAMPLE_HUMAN)
print_report(r1, "Human sample")
print("═" * 50)
print("SAMPLE 2: AI-generated content")
print("═" * 50)
r2 = score_humanity(SAMPLE_AI)
print_report(r2, "AI sample")
print(f" Delta: Human scored {r1['humanity_score']}, AI scored {r2['humanity_score']}")
print(f" Difference: {r1['humanity_score'] - r2['humanity_score']} points")
print()
else:
try:
with open(args.file, 'r', encoding='utf-8') as f:
text = f.read()
except FileNotFoundError:
print(f"Error: file not found: {args.file}", file=sys.stderr)
sys.exit(1)
result = score_humanity(text)
print_report(result, args.file)
if args.json:
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
Theo dõi đối thủ có hệ thống, phục vụ định vị, battlecard bán hàng và quyết định lộ trình sản phẩm.
---
name: "context-engine"
description: "Loads and manages company context for all C-suite advisor skills. Reads ~/.claude/company-context.md, detects stale context (>90 days), enriches context during conversations, and enforces privacy/anonymization rules before external API calls."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: orchestration
updated: 2026-03-05
frameworks: context-loading, anonymization, context-enrichment
---
# Company Context Engine
The memory layer for C-suite advisors. Every advisor skill loads this first. Context is what turns generic advice into specific insight.
## Keywords
company context, context loading, context engine, company profile, advisor context, stale context, context refresh, privacy, anonymization
---
## Load Protocol (Run at Start of Every C-Suite Session)
**Step 1 — Check for context file:** `~/.claude/company-context.md`
- Exists → proceed to Step 2
- Missing → prompt: *"Run /cs:setup to build your company context — it makes every advisor conversation significantly more useful."*
**Step 2 — Check staleness:** Read `Last updated` field.
- **< 90 days:** Load and proceed.
- **≥ 90 days:** Prompt: *"Your context is [N] days old. Quick 15-min refresh (/cs:update), or continue with what I have?"*
- If continue: load with `[STALE — last updated DATE]` noted internally.
**Step 3 — Parse into working memory.** Always active:
- Company stage (pre-PMF / scaling / optimizing)
- Founder archetype (product / sales / technical / operator)
- Current #1 challenge
- Runway (as risk signal — never share externally)
- Team size
- Unfair advantage
- 12-month target
---
## Context Quality Signals
| Condition | Confidence | Action |
|-----------|-----------|--------|
| < 30 days, full interview | High | Use directly |
| 30–90 days, update done | Medium | Use, flag what may have changed |
| > 90 days | Low | Flag stale, prompt refresh |
| Key fields missing | Low | Ask in-session |
| No file | None | Prompt /cs:setup |
If Low: *"My context is [stale/incomplete] — I'm assuming [X]. Correct me if I'm wrong."*
---
## Context Enrichment
During conversations, you'll learn things not in the file. Capture them.
**Triggers:** New number or timeline revealed, key person mentioned, priority shift, constraint surfaces.
**Protocol:**
1. Note internally: `[CONTEXT UPDATE: {what was learned}]`
2. At session end: *"I picked up a few things to add to your context. Want me to update the file?"*
3. If yes: append to the relevant dimension, update timestamp.
**Never silently overwrite.** Always confirm before modifying the context file.
---
## Privacy Rules
### Never send externally
- Specific revenue or burn figures
- Customer names
- Employee names (unless publicly known)
- Investor names (unless public)
- Specific runway months
- Watch List contents
### Safe to use externally (with anonymization)
- Stage label
- Team size ranges (1–10, 10–50, 50–200+)
- Industry vertical
- Challenge category
- Market position descriptor
### Before any external API call or web search
Apply `references/anonymization-protocol.md`:
- Numbers → ranges or stage-relative descriptors
- Names → roles
- Revenue → percentages or stage labels
- Customers → "Customer A, B, C"
---
## Missing or Partial Context
Handle gracefully — never block the conversation.
- **Missing stage:** "Just to calibrate — are you still finding PMF or scaling what works?"
- **Missing financials:** Use stage + team size to infer. Note the gap.
- **Missing founder profile:** Infer from conversation style. Mark as inferred.
- **Multiple founders:** Context reflects the interviewee. Note co-founder perspective may differ.
---
## Required Context Fields
```
Required:
- Last updated (date)
- Company Identity → What we do
- Stage & Scale → Stage
- Founder Profile → Founder archetype
- Current Challenges → Priority #1
- Goals & Ambition → 12-month target
High-value optional:
- Unfair advantage
- Kill-shot risk
- Avoided decision
- Watch list
```
Missing required fields: note gaps, work around in session, ask in-session only when critical.
---
## References
- `references/anonymization-protocol.md` — detailed rules for stripping sensitive data before external calls
FILE:references/anonymization-protocol.md
# Anonymization Protocol
Rules for stripping sensitive company data before any external API call, web search, or tool invocation that sends data outside the local environment.
---
## When This Protocol Applies
**Trigger:** Any time company context or conversation content will leave the local session.
Examples:
- Web search that includes company specifics
- External API call with company data in the payload
- Any tool call where conversation content is part of the request
**Does NOT apply to:**
- Local file reads/writes (`~/.claude/company-context.md`)
- In-session reasoning and analysis
- Generating advice or documents that stay local
---
## Rule 1: Financial Figures → Relative Ranges
Never send specific financial data externally.
| Raw data | Anonymized version |
|----------|-------------------|
| "$2.4M ARR" | "early-stage ARR (sub-$5M)" |
| "$180K MRR" | "growing MRR, Series A range" |
| "14 months runway" | "runway is healthy for stage" |
| "burn rate is $320K/month" | "burn rate is moderate for stage" |
| "raised $8M Series A" | "Series A company" |
| "customer LTV is $4,200" | "LTV is above industry average for segment" |
| "CAC is $680" | "CAC is in a sustainable range" |
**Rule:** No dollar amounts. No month counts for runway. Use stage-relative descriptors.
---
## Rule 2: Customer Names → Anonymized Labels
Never send customer or client names externally.
| Raw data | Anonymized version |
|----------|-------------------|
| "Acme Corp is our biggest customer" | "Customer A (largest account)" |
| "we're working with NHS England" | "a large public-sector customer" |
| "BMW, Volkswagen, and Stellantis" | "three major automotive OEMs" |
| "10 enterprise customers including..." | "10 enterprise customers" |
**Rule:** Use "Customer A/B/C" for named accounts, or describe by segment without naming.
---
## Rule 3: Revenue Figures → Percentage Changes or Stage Descriptors
Revenue trajectory is safer than absolute numbers.
| Raw data | Anonymized version |
|----------|-------------------|
| "growing from $1M to $2M ARR" | "2x revenue growth year-over-year" |
| "revenue dropped from $500K to $430K" | "revenue declined ~15% in the period" |
| "hit $10M ARR last quarter" | "crossed a significant ARR milestone" |
| "doing $50K MRR" | "pre-Series A revenue, strong growth trajectory" |
**Rule:** Percentages and directional signals (growing / declining / flat) are safe. Absolutes are not.
---
## Rule 4: Employee Names → Roles Only
Never send individual names externally.
| Raw data | Anonymized version |
|----------|-------------------|
| "Our CTO, Sarah Chen, is struggling" | "our CTO is struggling with the transition" |
| "James is the best performer on the team" | "our strongest performer is in the engineering lead role" |
| "we're about to let go of Michael" | "we're about to make a leadership change" |
| "the founding team is me, Alex, and Priya" | "a three-person founding team" |
**Exception:** Publicly known executives (CEO of a public company, named in press releases) can be referenced by name. If in doubt, use role.
---
## Rule 5: Investor Names → Generic Descriptors
| Raw data | Anonymized version |
|----------|-------------------|
| "Sequoia led our round" | "a top-tier VC led our round" |
| "our lead investor is pushing for an exit" | "pressure from investors toward exit" |
| "Y Combinator alumni" | "accelerator alumni" |
**Exception:** YC, Techstars, and similar well-known accelerators are commonly referenced and safe if the founder has publicly disclosed. When in doubt, omit.
---
## Rule 6: Location → Country or Region
| Raw data | Anonymized version |
|----------|-------------------|
| "Berlin-based startup" | "European startup" |
| "we're in San Francisco" | "US-based startup" |
| "expanding to Munich and Vienna" | "expanding in the DACH region" |
**Exception:** Location is less sensitive than financials. Use judgment — if it's on their website, it's fine.
---
## Anonymization Decision Tree
```
Before sending data externally:
1. Does it include a specific dollar amount?
→ YES: Replace with range or relative descriptor
2. Does it include a person's name?
→ YES: Replace with role only (unless publicly known)
3. Does it include a company or customer name?
→ YES: Replace with "Customer A" or segment descriptor
4. Does it include specific headcount or runway months?
→ YES: Replace with range (1–10, 10–50) or "healthy/tight/critical"
5. Does it include proprietary data, roadmap, or unreleased product info?
→ YES: Do not include. Reference only generically ("product expansion planned")
6. Is it publicly available information?
→ YES: Safe to send as-is
```
---
## Required vs Optional Anonymization
### Required (always strip before external calls)
- Revenue figures (absolute)
- Burn rate (absolute)
- Runway (specific months)
- Customer names
- Employee names
- Investor names (unless public)
- Funding amounts (unless public)
### Optional (use judgment based on sensitivity)
- Industry vertical (usually fine)
- Company stage (usually fine)
- Team size ranges (usually fine)
- Geographic region (usually fine)
- General challenge category (usually fine)
---
## What to Do If You're Unsure
Default to stricter anonymization. The cost of over-anonymizing is slightly less useful external results. The cost of under-anonymizing is a privacy breach.
When in doubt: **remove it**.
---
## Audit Log (Internal Only)
When running external calls with company context, note internally:
```
[EXTERNAL CALL: {tool/API used}]
[ANONYMIZED: {fields stripped}]
[RETAINED: {fields kept and why}]
```
This is for internal reasoning only — never included in output to the founder.
Lấy đồng thuận từ nhiều mô hình AI (Claude, Codex, Gemini) khi rà soát bản ghi nhớ hội đồng hoặc tài liệu chiến lược.
---
name: "cross-eval"
description: "/cs:cross-eval <memo> — Multi-model consensus on a board memo or strategy brief. Claude + Codex + Gemini cross-review with graceful degradation."
---
# /cs:cross-eval — Multi-Model Consensus
**Command:** `/cs:cross-eval <memo-or-brief>`
Runs the same memo through multiple model providers and reconciles divergences. Use for **high-stakes, irreversible decisions** where single-model bias is too costly: M&A, major fundraises, layoffs, strategic pivots, regulatory commitments.
Adapted from gstack's `/codex` cross-review pattern, generalized to **business memos** instead of code PRs.
## When to Run
- Before signing a term sheet
- Before announcing a layoff
- Before committing to a regulated market
- Before any decision where reversing costs > 6 months of company time
- When the boardroom vote was split or had a CRITICAL dissent
## Models Used (graceful degradation)
The command tries to invoke each available model in order:
1. **Claude** (primary, always available) — the boardroom's native voice
2. **Codex / OpenAI** (if `OPENAI_API_KEY` or `codex` CLI available)
3. **Gemini** (if `GEMINI_API_KEY` or `gemini` CLI available)
If only Claude is available, the command runs **Claude-only with adversarial mode** — same model, different prompt seeds — and clearly labels the output as single-model.
## Workflow
1. Read the memo / brief
2. Probe environment for available model CLIs / API keys
3. For each available model:
- Send the memo with this prompt prefix:
> "You are an independent C-suite reviewer. The following is a board memo from another company's boardroom. Identify the top 3 concerns, the top 3 supports, and your vote (APPROVE / REJECT / DEFER). Do not deferentially agree — assume the memo's reasoning is flawed until proven otherwise."
4. Collect three independent reviews
5. Reconcile: where do they agree? Where do they diverge?
6. Surface the divergences as questions for the founder
## Output Format
Saved to `~/.claude/cross-eval/YYYY-MM-DD-<slug>.md`:
```markdown
# Cross-Eval: <memo title>
**Date:** YYYY-MM-DD
**Memo reviewed:** <link>
**Models invoked:** Claude / Codex / Gemini (or noted fallbacks)
## Vote Tally
| Model | Vote | Confidence |
|---|---|---|
| Claude | APPROVE | High |
| Codex | DEFER | Med |
| Gemini | APPROVE | Low |
## Consensus Concerns (≥2 models flagged)
1. <concern> — flagged by Claude + Codex
2. <concern> — flagged by all 3
## Divergent Concerns (1 model flagged)
- <Codex only:> <concern> — worth a second look
- <Gemini only:> <concern> — likely noise, but check
## Consensus Supports (≥2 models endorsed)
1. <support>
2. <support>
## Recommendation
- 🟢 GO if 2+ models APPROVE and no CRITICAL concerns from any model
- 🟡 PAUSE if any model is DEFER or any concern is CRITICAL
- 🔴 STOP if 2+ models REJECT
## Open Questions for Founder
1. <question raised by divergence>
2. <question raised by divergence>
```
## Why This Matters
Single-model recommendations have systematic biases. Claude trends helpful and may under-weight risk. Codex (OpenAI) trends more cautious on emerging-market and regulatory topics. Gemini trends more cautious on technical scale claims. Disagreement is signal, not noise.
This is the **safety net before irreversibility** — not a replacement for outside counsel or a real board.
## Graceful Degradation
If only Claude is available:
```markdown
**Models available:** Claude only
**Mode:** ADVERSARIAL — running 3 independent Claude passes with different system prompts:
1. Standard reviewer
2. Devil's advocate (must find 3 critical concerns)
3. Steelman (must find 3 strongest reasons to approve)
This is weaker than true multi-model. Treat the result as suggestive, not conclusive.
```
## Routing
- `/cs:decide` — if consensus is GO
- `/cs:freeze` — if consensus is PAUSE
- `/cs:boardroom` (re-run) — if consensus is STOP
## Related
- Skills: [`board-meeting`](../../../skills/board-meeting/SKILL.md), [`executive-mentor`](../../../executive-mentor/)
- Inspiration: gstack's `/codex` cross-review pattern (adapted to business memos)
---
**Version:** 1.0.0
Lên kế hoạch webinar từ mục tiêu kinh doanh, cứu webinar kém hiệu quả hoặc biến webinar cũ thành nguồn tạo lead theo yêu cầu.
---
name: "cs-webinar"
description: "/cs:webinar — Webinar & virtual-event marketing workflow. Plan a webinar from scratch (sized backward from the business goal), rescue one whose numbers disappointed (score the funnel, fix the broken stage), or turn a past webinar into an evergreen on-demand lead engine. Covers the full funnel: registration, promotion runway, show-up, live engagement, live-to-close, and segmented follow-up. Treats a webinar as a funnel, not an event."
---
# /cs:webinar — Webinar & Virtual Event Marketing
**Command:** `/cs:webinar [mode] [args]`
The `cs-webinar` command is the **entry point for webinar workflows**: plan → promote → run → follow up, or diagnose → fix → re-run.
## When To Run
- Planning a webinar, virtual event, live demo, workshop, masterclass, fireside chat, or virtual summit from scratch
- Rescuing a webinar whose numbers disappointed — low registrations, low show-up, or attendees who don't convert
- Turning a one-time webinar into an always-on evergreen / on-demand engine
- Scoring an existing funnel to find the stage that's actually broken
## When NOT To Run
- Full product launch (not just a webinar) → use `/cs:launch` / launch-strategy
- Generic lifecycle nurture email unrelated to an event → use the `emails` skill
- In-person field-event logistics (venue, catering, booth) → out of scope
## Modes
### `plan` — Design the whole motion from scratch
```bash
/cs:webinar plan
```
Walks the intake, locks the promise + format, sizes the funnel backward from the business goal,
builds the promotion runway, and designs show-up + live-to-close + follow-up. Delivers a full plan
using `templates/webinar-plan-template.md`.
### `rescue` — Diagnose and fix an underperforming webinar
```bash
/cs:webinar rescue --input funnel.json
```
Scores the funnel, names the weakest stage, and returns ranked fixes targeting the actual bottleneck
— not a reflexive landing-page rewrite.
### `evergreen` — Convert a past webinar to on-demand
```bash
/cs:webinar evergreen
```
Maps the on-demand registration → watch → follow-up automation, with honest live-vs-simulated framing.
### `score` — Run the funnel scorer directly
```bash
/cs:webinar score --input funnel.json
/cs:webinar score # embedded sample data
```
## Minimal Intake (3 Questions)
| Q | Asks | When |
|---|---|---|
| Q1 | Which mode — plan / rescue / evergreen? | Always |
| Q2 | Business goal + conversion action (leads, pipeline, adoption, retention, brand)? | Always (drives the backward funnel math) |
| Q3 | Audience temperature (customers / warm / owned_cold / paid_cold)? | Always (selects benchmarks) |
Read `marketing-context.md` first if it exists — it covers brand voice, personas, and customer language,
so you only ask for what's specific to this event.
## Workflow
```bash
# Mode: rescue / score — find the broken stage first
python3 marketing-skill/skills/webinar-marketing/scripts/webinar_funnel_scorer.py funnel.json
# → overall 0-100 score + per-stage rate vs. benchmark + named bottleneck
# Pipe JSON via stdin
cat funnel.json | python3 marketing-skill/skills/webinar-marketing/scripts/webinar_funnel_scorer.py -
# Demo on embedded sample data (no --help flag — run with no args)
python3 marketing-skill/skills/webinar-marketing/scripts/webinar_funnel_scorer.py
```
Input JSON (`registrations` + `attended_live` required; rest optional):
```json
{
"invited": 5000, "page_visits": 1800, "registrations": 620,
"attended_live": 180, "cta_clicks": 40, "conversions": 14,
"audience": "owned_cold", "runtime_min": 45, "avg_watch_min": 26
}
```
## The Funnel Math (Plan Backward)
Always size from the business goal backward — this stops anyone celebrating 800 registrations while 6 people buy:
```
Business goal: 20 sales-qualified opportunities
÷ attendee→SQO rate (~10%) → need 200 engaged attendees
÷ register→attend (~35% live) → need ~570 registrations
÷ landing-page CVR (~40%) → need ~1,425 landing-page visits
→ promotion must drive ~1,425 qualified visits
```
If the required visits exceed the reachable audience, fix the goal, format, or promotion budget *now*.
## Audience Benchmarks
The scorer calibrates per audience temperature (warmer audiences convert better at every stage):
| Audience | Page→Reg | Reg→Attend | Attend→CTA | Attend→Convert |
|---|---|---|---|---|
| `customers` | 40% | 50% | 25% | 12% |
| `warm` | 35% | 42% | 22% | 10% |
| `owned_cold` | 25% | 35% | 18% | 7% |
| `paid_cold` | 18% | 28% | 15% | 5% |
## Anti-Patterns Rejected
- Celebrating registrations while show-up or conversion quietly fails
- Rewriting the landing page when the broken stage is show-up or live-to-close
- Promoting before sizing the funnel backward from the business goal
- Obvious fake-live framing that erodes audience trust
- Treating a webinar as an event instead of a funnel
## Trigger Phrases
- "plan a webinar" / "webinar strategy"
- "my webinar isn't converting" / "low show-up rate"
- "webinar promotion" / "webinar follow-up"
- "virtual event" / "live demo" / "masterclass" / "fireside chat" / "virtual summit"
- "evergreen webinar" / "on-demand webinar"
- "registration funnel" / "attendance rate"
## Related
- Agent: [`cs-webinar-marketer`](../agents/marketing/cs-webinar-marketer.md)
- Skill: [`webinar-marketing`](../marketing-skill/skills/webinar-marketing/SKILL.md)
- Companion: `/cs:aeo` (get supporting content cited by AI search), launch-strategy (full launches)
---
**Version:** 2.9.0
**License:** MIT
Theo dõi sức khỏe khách hàng, dự đoán rủi ro rời bỏ và tìm cơ hội mở rộng bằng mô hình chấm điểm có trọng số cho SaaS.
---
name: "customer-success-manager"
description: Monitors customer health, predicts churn risk, and identifies expansion opportunities using weighted scoring models for SaaS customer success. Use when analyzing customer accounts, reviewing retention metrics, scoring at-risk customers, or when the user mentions churn, customer health scores, upsell opportunities, expansion revenue, retention analysis, or customer analytics. Runs three Python CLI tools to produce deterministic health scores, churn risk tiers, and prioritized expansion recommendations across Enterprise, Mid-Market, and SMB segments.
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: business-growth
domain: customer-success
updated: 2026-02-06
python-tools: health_score_calculator.py, churn_risk_analyzer.py, expansion_opportunity_scorer.py
tech-stack: customer-success, saas-metrics, health-scoring
---
# Customer Success Manager
Production-grade customer success analytics with multi-dimensional health scoring, churn risk prediction, and expansion opportunity identification. Three Python CLI tools provide deterministic, repeatable analysis using standard library only -- no external dependencies, no API calls, no ML models.
---
## Table of Contents
- [Input Requirements](#input-requirements)
- [Output Formats](#output-formats)
- [How to Use](#how-to-use)
- [Scripts](#scripts)
- [Reference Guides](#reference-guides)
- [Templates](#templates)
- [Best Practices](#best-practices)
- [Limitations](#limitations)
---
## Input Requirements
All scripts accept a JSON file as positional input argument. See `assets/sample_customer_data.json` for complete schema examples and sample data.
### Health Score Calculator
Required fields per customer object: `customer_id`, `name`, `segment`, `arr`, and nested objects `usage` (login_frequency, feature_adoption, dau_mau_ratio), `engagement` (support_ticket_volume, meeting_attendance, nps_score, csat_score), `support` (open_tickets, escalation_rate, avg_resolution_hours), `relationship` (executive_sponsor_engagement, multi_threading_depth, renewal_sentiment), and `previous_period` scores for trend analysis.
### Churn Risk Analyzer
Required fields per customer object: `customer_id`, `name`, `segment`, `arr`, `contract_end_date`, and nested objects `usage_decline`, `engagement_drop`, `support_issues`, `relationship_signals`, and `commercial_factors`.
### Expansion Opportunity Scorer
Required fields per customer object: `customer_id`, `name`, `segment`, `arr`, and nested objects `contract` (licensed_seats, active_seats, plan_tier, available_tiers), `product_usage` (per-module adoption flags and usage percentages), and `departments` (current and potential).
---
## Output Formats
All scripts support two output formats via the `--format` flag:
- **`text`** (default): Human-readable formatted output for terminal viewing
- **`json`**: Machine-readable JSON output for integrations and pipelines
---
## How to Use
### Quick Start
```bash
# Health scoring
python scripts/health_score_calculator.py assets/sample_customer_data.json
python scripts/health_score_calculator.py assets/sample_customer_data.json --format json
# Churn risk analysis
python scripts/churn_risk_analyzer.py assets/sample_customer_data.json
python scripts/churn_risk_analyzer.py assets/sample_customer_data.json --format json
# Expansion opportunity scoring
python scripts/expansion_opportunity_scorer.py assets/sample_customer_data.json
python scripts/expansion_opportunity_scorer.py assets/sample_customer_data.json --format json
```
### Workflow Integration
```bash
# 1. Score customer health across portfolio
python scripts/health_score_calculator.py customer_portfolio.json --format json > health_results.json
# Verify: confirm health_results.json contains the expected number of customer records before continuing
# 2. Identify at-risk accounts
python scripts/churn_risk_analyzer.py customer_portfolio.json --format json > risk_results.json
# Verify: confirm risk_results.json is non-empty and risk tiers are present for each customer
# 3. Find expansion opportunities in healthy accounts
python scripts/expansion_opportunity_scorer.py customer_portfolio.json --format json > expansion_results.json
# Verify: confirm expansion_results.json lists opportunities ranked by priority
# 4. Prepare QBR using templates
# Reference: assets/qbr_template.md
```
**Error handling:** If a script exits with an error, check that:
- The input JSON matches the required schema for that script (see Input Requirements above)
- All required fields are present and correctly typed
- Python 3.7+ is being used (`python --version`)
- Output files from prior steps are non-empty before piping into subsequent steps
---
## Scripts
### 1. health_score_calculator.py
**Purpose:** Multi-dimensional customer health scoring with trend analysis and segment-aware benchmarking.
**Dimensions and Weights:**
| Dimension | Weight | Metrics |
|-----------|--------|---------|
| Usage | 30% | Login frequency, feature adoption, DAU/MAU ratio |
| Engagement | 25% | Support ticket volume, meeting attendance, NPS/CSAT |
| Support | 20% | Open tickets, escalation rate, avg resolution time |
| Relationship | 25% | Executive sponsor engagement, multi-threading depth, renewal sentiment |
**Classification:**
- Green (75-100): Healthy -- customer achieving value
- Yellow (50-74): Needs attention -- monitor closely
- Red (0-49): At risk -- immediate intervention required
**Usage:**
```bash
python scripts/health_score_calculator.py customer_data.json
python scripts/health_score_calculator.py customer_data.json --format json
```
### 2. churn_risk_analyzer.py
**Purpose:** Identify at-risk accounts with behavioral signal detection and tier-based intervention recommendations.
**Risk Signal Weights:**
| Signal Category | Weight | Indicators |
|----------------|--------|------------|
| Usage Decline | 30% | Login trend, feature adoption change, DAU/MAU change |
| Engagement Drop | 25% | Meeting cancellations, response time, NPS change |
| Support Issues | 20% | Open escalations, unresolved critical, satisfaction trend |
| Relationship Signals | 15% | Champion left, sponsor change, competitor mentions |
| Commercial Factors | 10% | Contract type, pricing complaints, budget cuts |
**Risk Tiers:**
- Critical (80-100): Immediate executive escalation
- High (60-79): Urgent CSM intervention
- Medium (40-59): Proactive outreach
- Low (0-39): Standard monitoring
**Usage:**
```bash
python scripts/churn_risk_analyzer.py customer_data.json
python scripts/churn_risk_analyzer.py customer_data.json --format json
```
### 3. expansion_opportunity_scorer.py
**Purpose:** Identify upsell, cross-sell, and expansion opportunities with revenue estimation and priority ranking.
**Expansion Types:**
- **Upsell**: Upgrade to higher tier or more of existing product
- **Cross-sell**: Add new product modules
- **Expansion**: Additional seats or departments
**Usage:**
```bash
python scripts/expansion_opportunity_scorer.py customer_data.json
python scripts/expansion_opportunity_scorer.py customer_data.json --format json
```
---
## Reference Guides
| Reference | Description |
|-----------|-------------|
| `references/health-scoring-framework.md` | Complete health scoring methodology, dimension definitions, weighting rationale, threshold calibration |
| `references/cs-playbooks.md` | Intervention playbooks for each risk tier, onboarding, renewal, expansion, and escalation procedures |
| `references/cs-metrics-benchmarks.md` | Industry benchmarks for NRR, GRR, churn rates, health scores, expansion rates by segment and industry |
---
## Templates
| Template | Purpose |
|----------|---------|
| `assets/qbr_template.md` | Quarterly Business Review presentation structure |
| `assets/success_plan_template.md` | Customer success plan with goals, milestones, and metrics |
| `assets/onboarding_checklist_template.md` | 90-day onboarding checklist with phase gates |
| `assets/executive_business_review_template.md` | Executive stakeholder review for strategic accounts |
---
## Best Practices
1. **Combine signals**: Use all three scripts together for a complete customer picture
2. **Act on trends, not snapshots**: A declining Green is more urgent than a stable Yellow
3. **Calibrate thresholds**: Adjust segment benchmarks based on your product and industry per `references/health-scoring-framework.md`
4. **Prepare with data**: Run scripts before every QBR and executive meeting; reference `references/cs-playbooks.md` for intervention guidance
---
## Limitations
- **No real-time data**: Scripts analyze point-in-time snapshots from JSON input files
- **No CRM integration**: Data must be exported manually from your CRM/CS platform
- **Deterministic only**: No predictive ML -- scoring is algorithmic based on weighted signals
- **Threshold tuning**: Default thresholds are industry-standard but may need calibration for your business
- **Revenue estimates**: Expansion revenue estimates are approximations based on usage patterns
---
**Last Updated:** February 2026
**Tools:** 3 Python CLI tools
**Dependencies:** Python 3.7+ standard library only
FILE:assets/executive_business_review_template.md
# Executive Business Review
**Customer:** [Customer Name]
**Date:** [Review Date]
**Prepared for:** [Executive Name, Title]
**Prepared by:** [CSM Name] | [VP Customer Success Name]
**Classification:** [Strategic / Enterprise / Key Account]
---
## 1. Partnership Summary
| Metric | Value |
|--------|-------|
| Partnership Duration | [X months/years] |
| Current ARR | $[Amount] |
| Lifetime Value to Date | $[Amount] |
| Current Plan | [Tier] |
| Licensed Seats | [Number] |
| Active Seats | [Number] |
| Health Score | [Score]/100 ([Green/Yellow/Red]) |
| NPS Score | [Score] |
| Renewal Date | [Date] ([X] days remaining) |
---
## 2. Strategic Alignment
### Customer's Business Priorities (This Year)
1. **[Priority 1]** -- [How our solution supports this]
2. **[Priority 2]** -- [How our solution supports this]
3. **[Priority 3]** -- [How our solution supports this]
### Alignment Assessment
| Business Priority | Our Contribution | Alignment Score |
|-------------------|-----------------|----------------|
| [Priority 1] | [Specific contribution] | [Strong / Moderate / Weak] |
| [Priority 2] | [Specific contribution] | [Strong / Moderate / Weak] |
| [Priority 3] | [Specific contribution] | [Strong / Moderate / Weak] |
---
## 3. Value Delivered
### Quantified Business Impact
| Outcome | Metric | Before | After | Business Value |
|---------|--------|--------|-------|---------------|
| [e.g., Operational efficiency] | [Hours saved/week] | [Baseline] | [Current] | $[Estimated value] |
| [e.g., Revenue acceleration] | [Deal velocity] | [Baseline] | [Current] | $[Estimated value] |
| [e.g., Risk reduction] | [Error rate] | [Baseline] | [Current] | $[Estimated value] |
**Total Estimated Business Value:** $[Amount]
**ROI:** [X]x return on investment
### Key Achievements This Period
1. [Achievement 1 with measurable outcome]
2. [Achievement 2 with measurable outcome]
3. [Achievement 3 with measurable outcome]
---
## 4. Adoption and Engagement Scorecard
### Platform Utilisation
| Module | Adoption Status | Usage Depth | Benchmark | Assessment |
|--------|---------------|-------------|-----------|------------|
| [Module 1] | Fully Adopted | [High/Med/Low] | [Benchmark] | [Above/At/Below] |
| [Module 2] | Partially Adopted | [High/Med/Low] | [Benchmark] | [Above/At/Below] |
| [Module 3] | Not Adopted | -- | -- | Opportunity |
### Engagement Health
| Indicator | Current | Previous Period | Trend |
|-----------|---------|----------------|-------|
| Executive Engagement | [Score] | [Score] | [Up/Down/Stable] |
| Stakeholder Breadth | [# contacts] | [# contacts] | [Up/Down/Stable] |
| Meeting Participation | [%] | [%] | [Up/Down/Stable] |
| Feature Request Activity | [Count] | [Count] | [Up/Down/Stable] |
---
## 5. Account Health Overview
### Health Score Trend (Last 4 Quarters)
| Quarter | Overall | Usage | Engagement | Support | Relationship |
|---------|---------|-------|------------|---------|-------------|
| [Q-3] | [Score] | [Score] | [Score] | [Score] | [Score] |
| [Q-2] | [Score] | [Score] | [Score] | [Score] | [Score] |
| [Q-1] | [Score] | [Score] | [Score] | [Score] | [Score] |
| Current | [Score] | [Score] | [Score] | [Score] | [Score] |
### Risk Assessment
| Risk Factor | Level | Details | Mitigation |
|------------|-------|---------|-----------|
| [Risk 1] | [High/Med/Low] | [Description] | [Action] |
| [Risk 2] | [High/Med/Low] | [Description] | [Action] |
---
## 6. Support and Service Quality
| Metric | This Period | SLA Target | Status |
|--------|------------|-----------|--------|
| Total Tickets | [Number] | -- | |
| Avg First Response | [Hours] | [Hours] | [Met / Not Met] |
| Avg Resolution Time | [Hours] | [Hours] | [Met / Not Met] |
| Escalations | [Number] | 0 | |
| CSAT Score | [Score] | [Target] | [Above / Below] |
| Critical Issues | [Number] | 0 | |
### Notable Support Interactions
- [Summary of any significant support events and resolution]
---
## 7. Product Roadmap Alignment
### Features Delivered (Relevant to This Customer)
| Feature | Release Date | Customer Impact |
|---------|-------------|----------------|
| [Feature 1] | [Date] | [How it helps them] |
| [Feature 2] | [Date] | [How it helps them] |
### Upcoming Features (Customer-Relevant)
| Feature | Expected Release | Expected Impact |
|---------|-----------------|----------------|
| [Feature 1] | [Quarter] | [Business value] |
| [Feature 2] | [Quarter] | [Business value] |
### Customer Feature Requests
| Request | Priority | Status | Business Case |
|---------|----------|--------|--------------|
| [Request 1] | [P1/P2/P3] | [Status] | [Why it matters] |
| [Request 2] | [P1/P2/P3] | [Status] | [Why it matters] |
---
## 8. Growth and Expansion Opportunity
### Current Whitespace Analysis
| Opportunity | Type | Est. Revenue | Effort | Priority |
|------------|------|-------------|--------|----------|
| [Opportunity 1] | [Upsell/Cross-sell/Expansion] | $[Amount] | [Low/Med/High] | [1-5] |
| [Opportunity 2] | [Upsell/Cross-sell/Expansion] | $[Amount] | [Low/Med/High] | [1-5] |
| [Opportunity 3] | [Upsell/Cross-sell/Expansion] | $[Amount] | [Low/Med/High] | [1-5] |
**Total Expansion Opportunity:** $[Amount]
### Recommended Next Steps for Growth
1. [Specific expansion recommendation with business justification]
2. [Specific expansion recommendation with business justification]
---
## 9. Renewal Outlook
| Factor | Assessment |
|--------|-----------|
| Overall Renewal Confidence | [High / Medium / Low] |
| Budget Availability | [Confirmed / Expected / Uncertain] |
| Sponsor Support | [Strong / Moderate / Weak] |
| Competitive Threat | [None / Low / Medium / High] |
| Value Perception | [Strong / Moderate / Weak] |
| Contract Satisfaction | [Satisfied / Neutral / Concerned] |
### Renewal Strategy
[2-3 sentences on the approach for securing renewal, including any specific actions needed]
---
## 10. Executive-Level Action Items
| Action | Owner | Due Date | Priority | Impact |
|--------|-------|----------|----------|--------|
| [Action 1] | [Name, Title] | [Date] | [Critical/High/Med] | [Expected outcome] |
| [Action 2] | [Name, Title] | [Date] | [Critical/High/Med] | [Expected outcome] |
| [Action 3] | [Name, Title] | [Date] | [Critical/High/Med] | [Expected outcome] |
---
## Appendix
### Stakeholder Map
| Name | Title | Influence | Sentiment | Last Contact |
|------|-------|-----------|-----------|-------------|
| [Name] | [Title] | [Decision Maker / Influencer / User] | [Positive / Neutral / Negative] | [Date] |
| [Name] | [Title] | [Decision Maker / Influencer / User] | [Positive / Neutral / Negative] | [Date] |
### Competitive Landscape (If Applicable)
- **Known competitors in evaluation:** [List]
- **Our differentiators:** [Key strengths vs. competition]
- **Risk mitigation:** [Actions to defend position]
---
**Confidential -- For Internal and Customer Executive Use Only**
**Next Executive Review:** [Date]
FILE:assets/expected_output.json
{
"report": "customer_health_scores",
"summary": {
"total_customers": 4,
"average_score": 78.8,
"green_count": 3,
"yellow_count": 1,
"red_count": 0
},
"customers": [
{
"customer_id": "CUST-001",
"name": "Acme Corp",
"segment": "enterprise",
"arr": 120000,
"overall_score": 86.2,
"classification": "green",
"dimensions": {
"usage": {
"score": 91.6,
"weight": "30%",
"classification": "green"
},
"engagement": {
"score": 82.0,
"weight": "25%",
"classification": "green"
},
"support": {
"score": 78.5,
"weight": "20%",
"classification": "green"
},
"relationship": {
"score": 90.1,
"weight": "25%",
"classification": "green"
}
},
"trends": {
"usage": "improving",
"engagement": "improving",
"support": "stable",
"relationship": "improving",
"overall": "improving"
},
"recommendations": []
},
{
"customer_id": "CUST-002",
"name": "TechStart Inc",
"segment": "smb",
"arr": 18000,
"overall_score": 53.7,
"classification": "yellow",
"dimensions": {
"usage": {
"score": 52.5,
"weight": "30%",
"classification": "yellow"
},
"engagement": {
"score": 61.6,
"weight": "25%",
"classification": "yellow"
},
"support": {
"score": 63.2,
"weight": "20%",
"classification": "yellow"
},
"relationship": {
"score": 39.5,
"weight": "25%",
"classification": "red"
}
},
"trends": {
"usage": "stable",
"engagement": "improving",
"support": "stable",
"relationship": "declining",
"overall": "stable"
},
"recommendations": [
"Login frequency below target -- schedule product engagement session",
"NPS below threshold -- conduct a feedback deep-dive with customer",
"CSAT is critically low -- escalate to support leadership",
"Single-threaded relationship -- expand contacts across departments",
"Renewal sentiment is negative -- initiate save plan immediately"
]
},
{
"customer_id": "CUST-003",
"name": "GlobalTrade Solutions",
"segment": "mid-market",
"arr": 55000,
"overall_score": 79.7,
"classification": "green",
"dimensions": {
"usage": {
"score": 85.6,
"weight": "30%",
"classification": "green"
},
"engagement": {
"score": 79.6,
"weight": "25%",
"classification": "green"
},
"support": {
"score": 72.0,
"weight": "20%",
"classification": "green"
},
"relationship": {
"score": 79.0,
"weight": "25%",
"classification": "green"
}
},
"trends": {
"usage": "improving",
"engagement": "improving",
"support": "improving",
"relationship": "improving",
"overall": "improving"
},
"recommendations": []
},
{
"customer_id": "CUST-004",
"name": "HealthFirst Medical",
"segment": "enterprise",
"arr": 200000,
"overall_score": 95.7,
"classification": "green",
"dimensions": {
"usage": {
"score": 100.0,
"weight": "30%",
"classification": "green"
},
"engagement": {
"score": 92.0,
"weight": "25%",
"classification": "green"
},
"support": {
"score": 88.7,
"weight": "20%",
"classification": "green"
},
"relationship": {
"score": 100.0,
"weight": "25%",
"classification": "green"
}
},
"trends": {
"usage": "improving",
"engagement": "improving",
"support": "stable",
"relationship": "improving",
"overall": "improving"
},
"recommendations": []
}
]
}
FILE:assets/onboarding_checklist_template.md
# Customer Onboarding Checklist (90-Day)
**Customer:** [Customer Name]
**Segment:** [Enterprise / Mid-Market / SMB]
**CSM:** [CSM Name]
**Kickoff Date:** [Date]
**Target Go-Live:** [Date]
**Target First Value Date:** [Date -- must be within 30 days]
---
## Phase 1: Welcome and Setup (Days 1-14)
### Pre-Kickoff Preparation (Day 0)
- [ ] Review signed contract and SOW for scope and commitments
- [ ] Research customer's industry, business model, and competitive landscape
- [ ] Review handoff notes from sales team (pain points, decision drivers, stakeholders)
- [ ] Prepare welcome package (login credentials, documentation links, support contacts)
- [ ] Create customer workspace in CS platform
- [ ] Schedule kickoff meeting with all required attendees
- [ ] Prepare kickoff deck with agenda and success plan draft
### Kickoff Meeting (Day 1-2)
- [ ] Conduct kickoff meeting with customer stakeholders
- [ ] Confirm business objectives and success criteria
- [ ] Identify key stakeholders and their roles (sponsor, champion, technical lead, users)
- [ ] Align on communication cadence and preferred channels
- [ ] Review onboarding timeline and milestones
- [ ] Set expectations for time commitment from customer team
- [ ] Share and agree on success plan (mutual accountability)
- [ ] Schedule recurring check-in meetings
**Kickoff Meeting Notes:**
> [Document key takeaways, concerns raised, decisions made]
### Technical Setup (Days 3-7)
- [ ] Provision customer environment (tenant, workspace, permissions)
- [ ] Configure SSO/authentication if applicable
- [ ] Set up integrations with customer's existing tools
- [ ] Import or migrate existing data (if applicable)
- [ ] Validate data integrity post-migration
- [ ] Configure role-based access and permissions
- [ ] Set up monitoring and alerting
**Technical Setup Owner:** [SE / Implementation team name]
**Technical Setup Notes:**
> [Document configuration decisions, customizations, issues]
### Admin Training (Days 7-10)
- [ ] Deliver admin training session (system configuration, user management)
- [ ] Provide admin documentation and quick reference guide
- [ ] Ensure admins can independently manage basic operations
- [ ] Set up admin support escalation path
### Initial User Training (Days 10-14)
- [ ] Deliver core user training (session 1: basic navigation and key workflows)
- [ ] Provide user quickstart guide and video resources
- [ ] Set up user support channel (Slack, email, in-app chat)
- [ ] Confirm all target users have active accounts
- [ ] Track initial login completion rate
**Training Completion Rate:** [___%] of target users
---
## Phase 2: Activation (Days 15-30)
### User Activation (Days 15-20)
- [ ] Monitor daily active user metrics
- [ ] Follow up with users who have not logged in
- [ ] Conduct follow-up training for users needing additional help
- [ ] Address any usability issues or confusion reported
- [ ] Validate that core workflows are functioning as expected
- [ ] Collect early feedback from champion and key users
**Activation Rate:** [___%] of licensed users active
### First Value Milestone (Days 20-30)
- [ ] Define and track first value milestone (specific to customer objectives)
- [ ] Verify customer has completed their first meaningful workflow
- [ ] Document value delivered (even if small -- establish the pattern)
- [ ] Share "first win" with executive sponsor
- [ ] Celebrate the milestone with the customer team
**First Value Milestone:** [Describe the specific milestone]
**Date Achieved:** [Date]
### 30-Day Review (Day 28-30)
- [ ] Conduct 30-day review meeting with customer
- [ ] Review activation metrics (logins, usage, adoption)
- [ ] Assess progress against success plan milestones
- [ ] Identify any blockers or concerns
- [ ] Adjust onboarding plan if needed
- [ ] Confirm transition from setup phase to adoption phase
- [ ] Set goals for days 31-60
**30-Day Health Score:** [Score]/100 -- [Green/Yellow/Red]
---
## Phase 3: Adoption (Days 31-60)
### Feature Expansion (Days 31-45)
- [ ] Introduce additional features beyond core workflows
- [ ] Deliver advanced training session (session 2: power features)
- [ ] Enable at least one integration with customer's existing tools
- [ ] Identify and address feature adoption gaps
- [ ] Share best practices from similar customers
### Usage Benchmarking (Days 45-55)
- [ ] Compare customer's usage against segment benchmarks
- [ ] Identify underperforming areas and create enablement plan
- [ ] Share usage report with customer champion
- [ ] Discuss usage targets for the next 30 days
**Current vs. Benchmark:**
| Metric | Current | Benchmark | Gap |
|--------|---------|-----------|-----|
| Feature Adoption | [%] | [%] | [+/-] |
| Daily Active Users | [#] | [#] | [+/-] |
| Key Workflow Completion | [%] | [%] | [+/-] |
### 60-Day Check-in (Day 55-60)
- [ ] Conduct 60-day check-in meeting
- [ ] Review adoption metrics and progress
- [ ] Discuss any roadblocks to deeper adoption
- [ ] Begin identifying advanced use cases
- [ ] Set goals for days 61-90
---
## Phase 4: Optimisation (Days 61-90)
### Advanced Use Cases (Days 61-75)
- [ ] Conduct use case discovery workshop with customer
- [ ] Identify 2-3 advanced use cases beyond initial scope
- [ ] Build implementation plan for advanced use cases
- [ ] Begin pilot of advanced use cases with power users
### ROI Measurement (Days 75-85)
- [ ] Collect data for ROI measurement against baseline
- [ ] Build ROI summary document
- [ ] Share ROI results with executive sponsor
- [ ] Document customer testimonial or case study opportunity (if willing)
**ROI Summary:**
| Metric | Baseline | Current | Improvement |
|--------|----------|---------|-------------|
| [Metric 1] | [Value] | [Value] | [% change] |
| [Metric 2] | [Value] | [Value] | [% change] |
### 90-Day Executive Review (Days 85-90)
- [ ] Prepare 90-day executive review presentation
- [ ] Include: value delivered, adoption metrics, ROI, next steps
- [ ] Conduct review meeting with executive sponsor
- [ ] Transition from onboarding to ongoing success management
- [ ] Establish ongoing success plan with quarterly milestones
- [ ] Confirm ongoing meeting cadence
- [ ] Introduce expansion opportunities if appropriate
**90-Day Health Score:** [Score]/100 -- [Green/Yellow/Red]
---
## Onboarding Completion Gate
The following criteria must be met to consider onboarding complete:
- [ ] User activation rate above 80%
- [ ] First value milestone achieved within 30 days
- [ ] Core workflows actively used by target users
- [ ] Executive sponsor confirms satisfaction
- [ ] Health score is Yellow (50+) or better
- [ ] Success plan established with ongoing milestones
- [ ] Recurring meeting cadence confirmed
- [ ] Support escalation path understood by customer
**Onboarding Status:** [Complete / In Progress / Blocked]
**Completion Date:** [Date]
**Handoff to Steady-State CSM:** [Date if different CSM]
---
## Notes
### Risks and Blockers
| Risk/Blocker | Impact | Mitigation | Status |
|-------------|--------|-----------|--------|
| [Item] | [High/Med/Low] | [Action] | [Open/Resolved] |
### Key Decisions
| Date | Decision | Made By | Impact |
|------|----------|---------|--------|
| [Date] | [Decision] | [Name] | [Description] |
---
**Template Version:** 1.0
**Last Updated:** February 2026
FILE:assets/qbr_template.md
# Quarterly Business Review (QBR)
**Customer:** [Customer Name]
**Date:** [QBR Date]
**Prepared by:** [CSM Name]
**Attendees:** [List attendees and titles]
---
## 1. Executive Summary
**Overall Relationship Status:** [Green / Yellow / Red]
**Health Score:** [Score]/100
**Key Theme:** [One sentence summarizing the quarter]
### Quarter Highlights
- [Highlight 1: major achievement or milestone]
- [Highlight 2: value delivered]
- [Highlight 3: initiative completed]
### Areas of Focus
- [Focus area 1]
- [Focus area 2]
---
## 2. Value Delivered This Quarter
### Business Outcomes Achieved
| Objective | Target | Actual | Status |
|-----------|--------|--------|--------|
| [Objective 1] | [Target metric] | [Actual metric] | [On Track / At Risk / Achieved] |
| [Objective 2] | [Target metric] | [Actual metric] | [On Track / At Risk / Achieved] |
| [Objective 3] | [Target metric] | [Actual metric] | [On Track / At Risk / Achieved] |
### ROI Summary
| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| [Metric 1, e.g., Time savings] | [Baseline] | [Current] | [% change] |
| [Metric 2, e.g., Cost reduction] | [Baseline] | [Current] | [% change] |
| [Metric 3, e.g., Revenue impact] | [Baseline] | [Current] | [% change] |
**Estimated Total Value Delivered:** $[Amount]
---
## 3. Product Usage and Adoption
### Usage Metrics
| Metric | Last Quarter | This Quarter | Trend |
|--------|-------------|--------------|-------|
| Monthly Active Users | [Number] | [Number] | [Up/Down/Stable] |
| Feature Adoption Rate | [%] | [%] | [Up/Down/Stable] |
| DAU/MAU Ratio | [Ratio] | [Ratio] | [Up/Down/Stable] |
| Seat Utilization | [%] | [%] | [Up/Down/Stable] |
### Feature Adoption Breakdown
| Feature/Module | Status | Usage Level | Notes |
|---------------|--------|-------------|-------|
| [Feature 1] | Active | [High/Med/Low] | |
| [Feature 2] | Active | [High/Med/Low] | |
| [Feature 3] | Not Adopted | -- | [Reason / Opportunity] |
### Adoption Recommendations
1. [Recommendation for increasing adoption of underused features]
2. [Recommendation for enabling new use cases]
---
## 4. Support Summary
| Metric | This Quarter | Previous Quarter | Benchmark |
|--------|-------------|-----------------|-----------|
| Total Tickets | [Number] | [Number] | [Segment avg] |
| Avg Resolution Time | [Hours] | [Hours] | [SLA target] |
| Escalations | [Number] | [Number] | [Target: 0] |
| CSAT Score | [Score] | [Score] | [Target] |
### Open Issues
| Issue | Priority | Status | ETA |
|-------|----------|--------|-----|
| [Issue 1] | [P1/P2/P3] | [In Progress / Pending] | [Date] |
---
## 5. Success Plan Progress
### Current Success Plan Goals
| Goal | Timeline | Progress | Status |
|------|----------|----------|--------|
| [Goal 1] | [Date] | [%] | [On Track / At Risk / Complete] |
| [Goal 2] | [Date] | [%] | [On Track / At Risk / Complete] |
| [Goal 3] | [Date] | [%] | [On Track / At Risk / Complete] |
### Next Quarter Goals (Proposed)
1. [Goal 1 with specific measurable outcome]
2. [Goal 2 with specific measurable outcome]
3. [Goal 3 with specific measurable outcome]
---
## 6. Product Roadmap Highlights
### Recently Released (Relevant to [Customer Name])
- [Feature/enhancement 1] -- [How it benefits them]
- [Feature/enhancement 2] -- [How it benefits them]
### Coming Next Quarter
- [Upcoming feature 1] -- [Expected benefit]
- [Upcoming feature 2] -- [Expected benefit]
### Feature Requests Status
| Request | Priority | Status | Expected Release |
|---------|----------|--------|-----------------|
| [Request 1] | [High/Med/Low] | [Planned / In Development / Under Review] | [Quarter] |
---
## 7. Growth Opportunities
### Expansion Discussion Points
- [Opportunity 1: e.g., additional seats for new team]
- [Opportunity 2: e.g., new module that addresses identified need]
- [Opportunity 3: e.g., tier upgrade for advanced capabilities]
### Estimated Value of Expansion: $[Amount] additional ARR
---
## 8. Action Items
| Action | Owner | Due Date | Priority |
|--------|-------|----------|----------|
| [Action 1] | [Name] | [Date] | [High/Med/Low] |
| [Action 2] | [Name] | [Date] | [High/Med/Low] |
| [Action 3] | [Name] | [Date] | [High/Med/Low] |
| [Action 4] | [Name] | [Date] | [High/Med/Low] |
---
## 9. Contract and Renewal
**Contract Start:** [Date]
**Renewal Date:** [Date]
**Current ARR:** $[Amount]
**Days to Renewal:** [Number]
### Renewal Readiness
- [ ] Value documented and communicated
- [ ] Executive sponsor aligned
- [ ] Open issues resolved or plan in place
- [ ] Pricing and terms discussed
- [ ] Expansion proposal prepared (if applicable)
---
**Next QBR Date:** [Date]
**Next Check-in:** [Date]
FILE:assets/sample_customer_data.json
{
"customers": [
{
"customer_id": "CUST-001",
"name": "Acme Corp",
"segment": "enterprise",
"arr": 120000,
"contract_end_date": "2026-12-31",
"usage": {
"login_frequency": 85,
"feature_adoption": 72,
"dau_mau_ratio": 0.45
},
"engagement": {
"support_ticket_volume": 3,
"meeting_attendance": 90,
"nps_score": 8,
"csat_score": 4.2
},
"support": {
"open_tickets": 2,
"escalation_rate": 0.05,
"avg_resolution_hours": 18
},
"relationship": {
"executive_sponsor_engagement": 80,
"multi_threading_depth": 4,
"renewal_sentiment": "positive"
},
"previous_period": {
"usage_score": 70,
"engagement_score": 65,
"support_score": 75,
"relationship_score": 60,
"overall_score": 67
},
"usage_decline": {
"login_trend": 5,
"feature_adoption_change": 3,
"dau_mau_change": 0.02
},
"engagement_drop": {
"meeting_cancellations": 0,
"response_time_days": 1,
"nps_change": 1
},
"support_issues": {
"open_escalations": 0,
"unresolved_critical": 0,
"satisfaction_trend": "improving"
},
"relationship_signals": {
"champion_left": false,
"sponsor_change": false,
"competitor_mentions": 0
},
"commercial_factors": {
"contract_type": "annual",
"pricing_complaints": false,
"budget_cuts_mentioned": false
},
"contract": {
"licensed_seats": 100,
"active_seats": 95,
"plan_tier": "professional",
"available_tiers": ["professional", "enterprise", "enterprise_plus"]
},
"product_usage": {
"core_platform": {"adopted": true, "usage_pct": 85},
"analytics_module": {"adopted": true, "usage_pct": 60},
"integrations_module": {"adopted": false, "usage_pct": 0},
"api_access": {"adopted": true, "usage_pct": 40},
"advanced_reporting": {"adopted": false, "usage_pct": 0}
},
"departments": {
"current": ["engineering", "product"],
"potential": ["marketing", "sales", "support"]
}
},
{
"customer_id": "CUST-002",
"name": "TechStart Inc",
"segment": "smb",
"arr": 18000,
"contract_end_date": "2026-04-15",
"usage": {
"login_frequency": 40,
"feature_adoption": 30,
"dau_mau_ratio": 0.15
},
"engagement": {
"support_ticket_volume": 8,
"meeting_attendance": 50,
"nps_score": 5,
"csat_score": 3.0
},
"support": {
"open_tickets": 6,
"escalation_rate": 0.18,
"avg_resolution_hours": 42
},
"relationship": {
"executive_sponsor_engagement": 30,
"multi_threading_depth": 1,
"renewal_sentiment": "negative"
},
"previous_period": {
"usage_score": 55,
"engagement_score": 50,
"support_score": 60,
"relationship_score": 45,
"overall_score": 52
},
"usage_decline": {
"login_trend": -25,
"feature_adoption_change": -18,
"dau_mau_change": -0.12
},
"engagement_drop": {
"meeting_cancellations": 3,
"response_time_days": 8,
"nps_change": -4
},
"support_issues": {
"open_escalations": 2,
"unresolved_critical": 1,
"satisfaction_trend": "declining"
},
"relationship_signals": {
"champion_left": true,
"sponsor_change": false,
"competitor_mentions": 3
},
"commercial_factors": {
"contract_type": "month-to-month",
"pricing_complaints": true,
"budget_cuts_mentioned": true
},
"contract": {
"licensed_seats": 20,
"active_seats": 8,
"plan_tier": "starter",
"available_tiers": ["starter", "professional", "enterprise"]
},
"product_usage": {
"core_platform": {"adopted": true, "usage_pct": 35},
"analytics_module": {"adopted": false, "usage_pct": 0},
"integrations_module": {"adopted": false, "usage_pct": 0},
"api_access": {"adopted": false, "usage_pct": 0},
"advanced_reporting": {"adopted": false, "usage_pct": 0}
},
"departments": {
"current": ["engineering"],
"potential": ["product", "design"]
}
},
{
"customer_id": "CUST-003",
"name": "GlobalTrade Solutions",
"segment": "mid-market",
"arr": 55000,
"contract_end_date": "2026-09-30",
"usage": {
"login_frequency": 70,
"feature_adoption": 58,
"dau_mau_ratio": 0.35
},
"engagement": {
"support_ticket_volume": 5,
"meeting_attendance": 75,
"nps_score": 7,
"csat_score": 3.8
},
"support": {
"open_tickets": 3,
"escalation_rate": 0.10,
"avg_resolution_hours": 30
},
"relationship": {
"executive_sponsor_engagement": 60,
"multi_threading_depth": 3,
"renewal_sentiment": "neutral"
},
"previous_period": {
"usage_score": 68,
"engagement_score": 70,
"support_score": 65,
"relationship_score": 62,
"overall_score": 66
},
"usage_decline": {
"login_trend": -8,
"feature_adoption_change": -5,
"dau_mau_change": -0.03
},
"engagement_drop": {
"meeting_cancellations": 1,
"response_time_days": 3,
"nps_change": -1
},
"support_issues": {
"open_escalations": 1,
"unresolved_critical": 0,
"satisfaction_trend": "stable"
},
"relationship_signals": {
"champion_left": false,
"sponsor_change": true,
"competitor_mentions": 1
},
"commercial_factors": {
"contract_type": "annual",
"pricing_complaints": false,
"budget_cuts_mentioned": false
},
"contract": {
"licensed_seats": 50,
"active_seats": 48,
"plan_tier": "professional",
"available_tiers": ["professional", "enterprise", "enterprise_plus"]
},
"product_usage": {
"core_platform": {"adopted": true, "usage_pct": 78},
"analytics_module": {"adopted": true, "usage_pct": 45},
"integrations_module": {"adopted": true, "usage_pct": 55},
"api_access": {"adopted": false, "usage_pct": 0},
"advanced_reporting": {"adopted": false, "usage_pct": 0}
},
"departments": {
"current": ["operations", "finance"],
"potential": ["logistics", "compliance"]
}
},
{
"customer_id": "CUST-004",
"name": "HealthFirst Medical",
"segment": "enterprise",
"arr": 200000,
"contract_end_date": "2027-03-15",
"usage": {
"login_frequency": 92,
"feature_adoption": 88,
"dau_mau_ratio": 0.55
},
"engagement": {
"support_ticket_volume": 2,
"meeting_attendance": 95,
"nps_score": 9,
"csat_score": 4.6
},
"support": {
"open_tickets": 1,
"escalation_rate": 0.02,
"avg_resolution_hours": 12
},
"relationship": {
"executive_sponsor_engagement": 92,
"multi_threading_depth": 6,
"renewal_sentiment": "positive"
},
"previous_period": {
"usage_score": 85,
"engagement_score": 82,
"support_score": 88,
"relationship_score": 80,
"overall_score": 84
},
"usage_decline": {
"login_trend": 3,
"feature_adoption_change": 5,
"dau_mau_change": 0.03
},
"engagement_drop": {
"meeting_cancellations": 0,
"response_time_days": 1,
"nps_change": 0
},
"support_issues": {
"open_escalations": 0,
"unresolved_critical": 0,
"satisfaction_trend": "improving"
},
"relationship_signals": {
"champion_left": false,
"sponsor_change": false,
"competitor_mentions": 0
},
"commercial_factors": {
"contract_type": "multi-year",
"pricing_complaints": false,
"budget_cuts_mentioned": false
},
"contract": {
"licensed_seats": 250,
"active_seats": 240,
"plan_tier": "enterprise",
"available_tiers": ["professional", "enterprise", "enterprise_plus"]
},
"product_usage": {
"core_platform": {"adopted": true, "usage_pct": 92},
"analytics_module": {"adopted": true, "usage_pct": 80},
"integrations_module": {"adopted": true, "usage_pct": 70},
"api_access": {"adopted": true, "usage_pct": 65},
"advanced_reporting": {"adopted": true, "usage_pct": 50},
"security_module": {"adopted": false, "usage_pct": 0},
"audit_module": {"adopted": false, "usage_pct": 0}
},
"departments": {
"current": ["clinical", "operations", "IT", "compliance"],
"potential": ["research", "finance", "HR"]
}
}
]
}
FILE:assets/success_plan_template.md
# Customer Success Plan
**Customer:** [Customer Name]
**CSM:** [CSM Name]
**Account Executive:** [AE Name]
**Plan Created:** [Date]
**Last Updated:** [Date]
**Review Cadence:** [Monthly / Quarterly]
---
## 1. Customer Overview
| Field | Details |
|-------|---------|
| Industry | [Industry] |
| Company Size | [Employees] |
| Segment | [Enterprise / Mid-Market / SMB] |
| ARR | $[Amount] |
| Contract Start | [Date] |
| Renewal Date | [Date] |
| Plan Tier | [Tier name] |
| Licensed Seats | [Number] |
### Key Stakeholders
| Name | Title | Role | Engagement Level |
|------|-------|------|-----------------|
| [Name] | [Title] | Executive Sponsor | [High / Medium / Low] |
| [Name] | [Title] | Day-to-Day Champion | [High / Medium / Low] |
| [Name] | [Title] | Technical Lead | [High / Medium / Low] |
| [Name] | [Title] | End User Lead | [High / Medium / Low] |
---
## 2. Business Objectives
### Primary Business Objectives
| # | Objective | Success Metric | Target | Timeline |
|---|-----------|---------------|--------|----------|
| 1 | [e.g., Reduce manual reporting time] | [Hours saved per week] | [Target number] | [Date] |
| 2 | [e.g., Improve team collaboration] | [Project completion rate] | [Target %] | [Date] |
| 3 | [e.g., Increase revenue visibility] | [Forecast accuracy] | [Target %] | [Date] |
### Why These Objectives Matter
- **Objective 1:** [Business context -- why this matters to the customer's overall strategy]
- **Objective 2:** [Business context]
- **Objective 3:** [Business context]
---
## 3. Success Milestones
### Phase 1: Foundation (Days 1-30)
| Milestone | Target Date | Status | Owner | Notes |
|-----------|------------|--------|-------|-------|
| Technical setup complete | [Date] | [ ] | [Name] | |
| Admin training delivered | [Date] | [ ] | CSM | |
| Core team onboarded | [Date] | [ ] | CSM | |
| First value milestone achieved | [Date] | [ ] | [Name] | |
| Data migration validated | [Date] | [ ] | SE | |
### Phase 2: Adoption (Days 31-90)
| Milestone | Target Date | Status | Owner | Notes |
|-----------|------------|--------|-------|-------|
| 80% user adoption | [Date] | [ ] | CSM | |
| Key workflows live | [Date] | [ ] | [Name] | |
| Integrations configured | [Date] | [ ] | SE | |
| First ROI measurement | [Date] | [ ] | CSM | |
| 30-day review complete | [Date] | [ ] | CSM | |
### Phase 3: Value Realisation (Days 91-180)
| Milestone | Target Date | Status | Owner | Notes |
|-----------|------------|--------|-------|-------|
| Objective 1 progress measurable | [Date] | [ ] | [Name] | |
| Advanced features adopted | [Date] | [ ] | CSM | |
| QBR completed | [Date] | [ ] | CSM | |
| Executive alignment confirmed | [Date] | [ ] | CSM | |
### Phase 4: Optimisation and Growth (Days 181-365)
| Milestone | Target Date | Status | Owner | Notes |
|-----------|------------|--------|-------|-------|
| All objectives on track | [Date] | [ ] | CSM | |
| ROI documented for renewal | [Date] | [ ] | CSM | |
| Expansion opportunities identified | [Date] | [ ] | CSM + AE | |
| Renewal conversation initiated | [Date] | [ ] | CSM + AE | |
---
## 4. Health Score Tracking
| Date | Overall Score | Usage | Engagement | Support | Relationship | Classification |
|------|--------------|-------|------------|---------|-------------|---------------|
| [Date] | [Score] | [Score] | [Score] | [Score] | [Score] | [Green/Yellow/Red] |
| [Date] | [Score] | [Score] | [Score] | [Score] | [Score] | [Green/Yellow/Red] |
---
## 5. Risk Register
| Risk | Probability | Impact | Mitigation | Owner | Status |
|------|------------|--------|-----------|-------|--------|
| [e.g., Executive sponsor departure] | [High/Med/Low] | [High/Med/Low] | [Multi-thread relationships] | CSM | [Active/Resolved] |
| [e.g., Low adoption in team X] | [High/Med/Low] | [High/Med/Low] | [Targeted training session] | CSM | [Active/Resolved] |
| [e.g., Budget review next quarter] | [High/Med/Low] | [High/Med/Low] | [Document ROI before review] | CSM | [Active/Resolved] |
---
## 6. Communication Plan
| Activity | Frequency | Participants | Purpose |
|----------|-----------|-------------|---------|
| Status check-in | [Weekly / Bi-weekly] | CSM + Champion | Tactical progress review |
| Strategic review | [Monthly] | CSM + Stakeholders | Objective alignment |
| QBR | [Quarterly] | CSM + Executive Sponsor | Executive business review |
| Technical review | [As needed] | SE + Technical Lead | Architecture and integration |
| Renewal planning | [90 days before] | CSM + AE + Sponsor | Contract discussion |
---
## 7. Product Adoption Plan
### Current State
| Module/Feature | Status | Usage Level | Target Usage | Gap |
|---------------|--------|-------------|-------------|-----|
| [Module 1] | Adopted | [%] | [%] | [Actions needed] |
| [Module 2] | Adopted | [%] | [%] | [Actions needed] |
| [Module 3] | Not Adopted | 0% | [%] | [Enablement plan] |
### Enablement Activities
| Activity | Target Date | Audience | Expected Outcome |
|----------|------------|----------|-----------------|
| [Training session] | [Date] | [Team/Group] | [Metric improvement] |
| [Workshop] | [Date] | [Team/Group] | [New workflow adoption] |
| [Office hours] | [Ongoing] | [All users] | [Question resolution] |
---
## 8. Expansion Roadmap
| Opportunity | Type | Estimated Value | Timeline | Prerequisites |
|------------|------|----------------|----------|--------------|
| [e.g., Additional seats] | Expansion | $[Amount] | [Quarter] | [Usage > 90%] |
| [e.g., Tier upgrade] | Upsell | $[Amount] | [Quarter] | [Feature requests] |
| [e.g., New module] | Cross-sell | $[Amount] | [Quarter] | [Use case validated] |
---
## 9. Notes and Updates
### [Date] - [Author]
[Update notes, key decisions, changes to plan]
### [Date] - [Author]
[Update notes, key decisions, changes to plan]
---
**Next Review Date:** [Date]
**Plan Owner:** [CSM Name]
FILE:references/cs-metrics-benchmarks.md
# Customer Success Metrics and Benchmarks
Industry benchmarks for key customer success metrics, segmented by company size, customer segment, and industry vertical.
---
## Core SaaS Metrics
### Net Revenue Retention (NRR)
NRR measures revenue retained from existing customers including expansion, contraction, and churn. It is the single most important metric for SaaS customer success.
**Formula:** (Starting ARR + Expansion - Contraction - Churn) / Starting ARR * 100
| Performance Level | NRR Range | Interpretation |
|-------------------|-----------|----------------|
| Best-in-class | > 130% | Strong expansion engine, very low churn |
| Excellent | 120-130% | Healthy growth from existing customers |
| Good | 110-120% | Solid retention with moderate expansion |
| Target | > 110% | Minimum for sustainable growth |
| Acceptable | 100-110% | Revenue stable but limited expansion |
| Below target | 90-100% | Churn exceeds expansion |
| Concerning | < 90% | Significant revenue erosion |
**Benchmarks by Segment:**
| Customer Segment | Median NRR | Top Quartile | Bottom Quartile |
|-----------------|------------|--------------|-----------------|
| Enterprise (>$100K ARR) | 115% | 130%+ | 105% |
| Mid-Market ($25K-$100K) | 108% | 120% | 98% |
| SMB (<$25K ARR) | 95% | 105% | 85% |
### Gross Revenue Retention (GRR)
GRR measures revenue retained without counting expansion. It isolates the churn and contraction signal.
**Formula:** (Starting ARR - Contraction - Churn) / Starting ARR * 100
| Performance Level | GRR Range | Interpretation |
|-------------------|-----------|----------------|
| Best-in-class | > 95% | Minimal churn, highly sticky product |
| Excellent | 92-95% | Strong retention |
| Good | 90-92% | Healthy with room to improve |
| Target | > 90% | Industry standard target |
| Acceptable | 85-90% | Moderate churn, needs focus |
| Below target | 80-85% | High churn impacting growth |
| Concerning | < 80% | Urgent retention problem |
**Benchmarks by Segment:**
| Customer Segment | Median GRR | Top Quartile | Bottom Quartile |
|-----------------|------------|--------------|-----------------|
| Enterprise | 95% | 98% | 90% |
| Mid-Market | 90% | 95% | 85% |
| SMB | 82% | 90% | 75% |
---
## Health Score Benchmarks
### Portfolio Health Distribution (Target)
A healthy CS portfolio should have the following approximate distribution:
| Classification | Target Distribution | Alert Threshold |
|---------------|-------------------|-----------------|
| Green (Healthy) | 60-70% | < 50% triggers portfolio review |
| Yellow (Attention) | 20-30% | > 35% signals systemic issues |
| Red (At Risk) | 5-10% | > 15% requires executive intervention |
### Average Health Score by Segment
| Segment | Target Average | Industry Median | Top Quartile |
|---------|---------------|-----------------|--------------|
| Enterprise | > 78 | 72 | 82 |
| Mid-Market | > 75 | 68 | 78 |
| SMB | > 70 | 65 | 75 |
### Health Score by Dimension (Industry Medians)
| Dimension | Enterprise | Mid-Market | SMB |
|-----------|-----------|------------|-----|
| Usage | 72 | 68 | 60 |
| Engagement | 70 | 62 | 55 |
| Support | 78 | 72 | 65 |
| Relationship | 68 | 60 | 50 |
---
## Churn Metrics
### Logo Churn Rate (Annual)
| Performance Level | Rate | Interpretation |
|-------------------|------|----------------|
| Best-in-class | < 5% | Exceptional retention |
| Excellent | 5-8% | Very strong |
| Good | 8-12% | Healthy |
| Acceptable | 12-15% | Room for improvement |
| Below target | 15-20% | Significant churn problem |
| Concerning | > 20% | Urgent -- product-market fit issues likely |
**Benchmarks by Segment:**
| Segment | Median Annual Logo Churn | Top Quartile | Bottom Quartile |
|---------|------------------------|--------------|-----------------|
| Enterprise | 5% | 2% | 10% |
| Mid-Market | 10% | 5% | 18% |
| SMB | 20% | 12% | 35% |
### Churn Leading Indicators
The following metrics have the highest predictive power for churn events:
| Indicator | Lead Time | Correlation with Churn |
|-----------|-----------|----------------------|
| Login frequency decline (>30%) | 60-90 days | Very High |
| NPS drop (>3 points) | 30-60 days | High |
| Executive sponsor departure | 30-90 days | Very High |
| Support escalation rate increase | 30-60 days | High |
| Meeting cancellation increase | 30-45 days | Moderate-High |
| Feature adoption decline | 60-90 days | Moderate |
| Competitor mentions | 30-60 days | Moderate |
---
## Expansion Metrics
### Expansion Revenue Rate
| Performance Level | Rate | Notes |
|-------------------|------|-------|
| Best-in-class | > 30% of total revenue | Strong land-and-expand motion |
| Excellent | 25-30% | Effective expansion engine |
| Good | 20-25% | Solid upsell/cross-sell |
| Target | > 20% | Minimum for healthy growth |
| Below target | 10-20% | Expansion motion needs development |
| Concerning | < 10% | Missing significant expansion opportunity |
### Expansion by Type
| Expansion Type | Typical Contribution | Average Deal Size |
|---------------|---------------------|-------------------|
| Seat Expansion | 40-50% of expansion | 15-25% of contract value |
| Tier Upsell | 25-35% of expansion | 40-80% of contract value |
| Module Cross-sell | 15-25% of expansion | 10-20% of contract value |
| Department Expansion | 5-15% of expansion | 50-100% of contract value |
### Expansion Readiness Indicators
| Signal | Interpretation |
|--------|---------------|
| Seat utilisation > 90% | Ready for seat expansion |
| Feature requests for higher tier | Upsell opportunity |
| Usage of 70%+ of current modules | Ready for cross-sell |
| New department interest | Department expansion play |
| Customer referral activity | Strong relationship, open to expansion |
---
## Engagement Metrics
### Customer Engagement Score (CES) Benchmarks
| Metric | Target | Median | Warning |
|--------|--------|--------|---------|
| Meeting attendance rate | > 80% | 72% | < 50% |
| Average NPS | > 50 | 35 | < 20 |
| Average CSAT | > 4.2/5 | 3.8/5 | < 3.0/5 |
| Response time (days) | < 2 | 3 | > 5 |
| QBR completion rate | > 90% | 75% | < 60% |
### Time to First Value (TTFV)
| Segment | Target TTFV | Median TTFV | Warning Threshold |
|---------|------------|------------|-------------------|
| Enterprise | < 30 days | 45 days | > 60 days |
| Mid-Market | < 21 days | 30 days | > 45 days |
| SMB | < 14 days | 21 days | > 30 days |
---
## CSM Operational Metrics
### Portfolio Management
| Metric | Enterprise CSM | Mid-Market CSM | SMB CSM (Tech-Touch) |
|--------|---------------|----------------|---------------------|
| Accounts per CSM | 10-25 | 30-60 | 100-300+ |
| ARR per CSM | $2M-$5M | $2M-$4M | $1M-$3M |
| Touch frequency | Weekly-biweekly | Biweekly-monthly | Quarterly-automated |
| QBR frequency | Quarterly | Semi-annually | Annually |
| Health score reviews | Weekly | Bi-weekly | Monthly |
### CSM Activity Benchmarks
| Activity | Target per Month | Purpose |
|----------|-----------------|---------|
| Strategic calls | 2-4 per account | Relationship building |
| Health score reviews | 4 (weekly) | Portfolio monitoring |
| QBR preparation | 3-5 per quarter | Executive engagement |
| Escalation handling | < 2 per month | Issue resolution |
| Expansion conversations | 1-2 per account | Revenue growth |
---
## Industry-Specific Benchmarks
### By Industry Vertical
| Industry | Median NRR | Median GRR | Median Logo Churn |
|----------|-----------|-----------|------------------|
| Infrastructure/DevOps | 125% | 95% | 5% |
| Cybersecurity | 120% | 93% | 7% |
| HR Tech | 110% | 90% | 12% |
| MarTech | 105% | 87% | 15% |
| FinTech | 115% | 92% | 8% |
| HealthTech | 112% | 91% | 10% |
| EdTech | 100% | 85% | 18% |
| eCommerce Tools | 108% | 88% | 14% |
### By Company Stage
| Stage | Median NRR | Median GRR | Notes |
|-------|-----------|-----------|-------|
| Early Stage (<$10M ARR) | 100% | 85% | Focus on product-market fit |
| Growth ($10M-$50M ARR) | 110% | 90% | Building CS function |
| Scale ($50M-$200M ARR) | 118% | 93% | Mature CS operations |
| Enterprise (>$200M ARR) | 115% | 95% | Optimisation phase |
---
## Metric Relationships
### Key Correlations
| If This Metric Moves | This Also Tends to Move | Direction |
|---------------------|------------------------|-----------|
| Health score down | Churn probability up | Inverse |
| NPS up | NRR up | Direct |
| TTFV down | GRR up | Inverse |
| Feature adoption up | Expansion rate up | Direct |
| Escalation rate up | NPS down | Inverse |
| Multi-threading depth up | GRR up | Direct |
### The SaaS Retention Equation
**Sustainable Growth requires:** NRR > 110% AND GRR > 90%
If NRR is high but GRR is low: You are churning customers and replacing with expansion from survivors. Not sustainable.
If GRR is high but NRR is low: You retain well but do not expand. Leaving money on the table.
Both high: Healthy, compounding growth from existing customers.
---
**Last Updated:** February 2026
**Sources:** Industry surveys, SaaS benchmarking reports, customer success community data (2024-2025 data cycles).
FILE:references/cs-playbooks.md
# Customer Success Playbooks
Comprehensive intervention, onboarding, renewal, expansion, and escalation playbooks for SaaS customer success management.
---
## Risk Tier Intervention Playbooks
### Critical Risk (Score 80-100)
**Situation:** Customer is at imminent risk of churn. Multiple severe warning signals detected. Requires immediate executive-level intervention.
**Timeline:** Act within 48 hours.
**Steps:**
1. **Executive Escalation (Day 0)**
- Alert VP of Customer Success and account executive immediately
- Brief internal leadership on situation, warning signals, and ARR at risk
- Identify any pending support issues and fast-track resolution
2. **Customer Contact (Day 1-2)**
- Schedule executive-to-executive call (VP CS to customer VP/C-level)
- Frame the conversation around understanding their challenges, not defending your product
- Listen more than talk -- capture the real objections
3. **Save Plan Creation (Day 2-3)**
- Create a detailed save plan with specific value milestones tied to their business outcomes
- Include timeline, owners, and measurable success criteria
- Get internal alignment on any concessions (pricing, features, roadmap commitments)
4. **Rescue Team Assignment (Day 3-5)**
- Assign a dedicated rescue team: CSM + Solutions Engineer + Support Lead
- Daily internal stand-up (15 min max) on account status
- Solutions Engineer to conduct technical health check
5. **Execution and Monitoring (Week 2-4)**
- Execute save plan with weekly customer check-ins
- Track progress against milestones
- Prepare competitive displacement defence if competitor involvement detected
6. **Resolution Assessment (Week 4)**
- Evaluate whether the situation is stabilising
- If improving: transition to High-risk monitoring cadence
- If not improving: escalate to CEO/GM for final intervention
**Success Criteria:** Risk score drops below 60 within 30 days. Customer confirms continued partnership intent.
---
### High Risk (Score 60-79)
**Situation:** Customer showing clear signs of dissatisfaction or disengagement. Still salvageable with focused CSM intervention.
**Timeline:** Act within 1 week.
**Steps:**
1. **Root Cause Analysis (Day 1-3)**
- Review all health score dimensions to identify the primary drivers
- Pull support ticket history for patterns
- Check product usage trends for the past 90 days
2. **CSM Outreach (Day 3-5)**
- Schedule a dedicated call with the customer (not a routine check-in)
- Open with empathy: "I've noticed some changes and want to make sure we're supporting you properly"
- Identify the top 3 customer concerns
3. **30-Day Recovery Plan (Day 5-7)**
- Build a 30-day recovery plan with measurable checkpoints every week
- Include specific actions for each concern identified
- Share the plan with the customer for mutual commitment
4. **Re-Engage Executive Sponsor (Week 2)**
- Request a meeting with the executive sponsor
- Align on business outcomes and how your product supports them
- Confirm continued sponsorship and address any political changes
5. **Support Fast-Track (Ongoing)**
- Escalate any pending support tickets internally
- Assign a support point of contact for this account
- Provide weekly status updates on open issues
6. **Progress Review (Week 3-4)**
- Review all metrics for improvement
- Adjust plan if specific interventions are not working
- If score drops to Critical: escalate to executive playbook
**Success Criteria:** Risk score drops below 40 within 30 days. No new warning signals emerge.
---
### Medium Risk (Score 40-59)
**Situation:** Early warning signs detected. Customer may not be aware of emerging issues. Proactive outreach prevents escalation.
**Timeline:** Act within 2 weeks.
**Steps:**
1. **Data Review (Day 1-5)**
- Analyse which dimension(s) are pulling the score down
- Review recent support interactions for sentiment clues
- Check for any known product issues affecting this customer
2. **Proactive Check-In (Week 1-2)**
- Schedule a "value check-in" call (position it as routine, not reactive)
- Share relevant success stories from similar customers
- Propose a training session or product walkthrough for underutilised features
3. **Value Reinforcement (Week 2-3)**
- Send a customised ROI summary showing value delivered
- Highlight feature releases relevant to their use case
- Connect them with your customer community or user group
4. **Monitoring (Week 3-4)**
- Increase monitoring frequency to bi-weekly
- Watch for improvement or continued decline
- If declining: move to High-risk playbook
**Success Criteria:** Score stabilises above 50 or improves. No escalation to High risk.
---
### Low Risk (Score 0-39)
**Situation:** Customer is healthy. Standard success cadence applies. Focus on value reinforcement and expansion readiness.
**Timeline:** Standard touch cadence.
**Steps:**
1. **Maintain Cadence**
- Enterprise: Monthly strategic reviews, quarterly QBRs
- Mid-Market: Bi-monthly check-ins, semi-annual reviews
- SMB: Quarterly automated health updates, annual review
2. **Proactive Communication**
- Share product updates and release notes
- Invite to webinars, conferences, and community events
- Share relevant industry insights and benchmarks
3. **Expansion Readiness**
- Monitor for expansion signals (usage approaching limits, new use cases)
- Prepare expansion proposals when timing is right
- Position premium features and modules relevant to their needs
4. **Renewal Preparation**
- Begin renewal preparation 90 days before contract end
- Build renewal proposal with value delivered summary
- Identify any terms or pricing adjustments needed
**Success Criteria:** Customer remains in Green classification. Expansion conversations initiated when appropriate.
---
## Onboarding Playbook
### Phase 1: Welcome and Setup (Day 1-14)
| Day | Activity | Owner | Deliverable |
|-----|----------|-------|-------------|
| 1 | Welcome email and introduction | CSM | Welcome package sent |
| 1-2 | Kickoff call | CSM + SE | Success plan drafted |
| 3-5 | Technical setup and configuration | SE | Environment configured |
| 5-7 | Admin training session | CSM | Admins trained |
| 7-10 | Data migration (if applicable) | SE | Data validated |
| 10-14 | Initial user training | CSM | Core team trained |
### Phase 2: Activation (Day 15-30)
| Day | Activity | Owner | Deliverable |
|-----|----------|-------|-------------|
| 15 | Activation check -- are users logging in? | CSM | Usage report |
| 15-20 | Follow-up training for laggards | CSM | All users active |
| 20-25 | First business outcome milestone | CSM | Milestone achieved |
| 25-30 | 30-day review call | CSM | Review documented |
**Critical Milestone:** Time to First Value must be under 30 days.
### Phase 3: Adoption (Day 31-60)
| Day | Activity | Owner | Deliverable |
|-----|----------|-------|-------------|
| 30-40 | Feature adoption expansion | CSM | New features in use |
| 40-50 | Integration setup (if applicable) | SE | Integrations live |
| 50-60 | Usage benchmarking vs. peers | CSM | Benchmark report |
### Phase 4: Optimisation (Day 61-90)
| Day | Activity | Owner | Deliverable |
|-----|----------|-------|-------------|
| 60-70 | Advanced use case workshop | CSM + SE | New use cases identified |
| 70-80 | ROI measurement | CSM | ROI documented |
| 80-90 | 90-day executive review | CSM | Transition to steady-state |
**Gate:** Handoff from onboarding to ongoing CSM management. Health score must be Yellow or better.
---
## Renewal Playbook
### 120 Days Before Renewal
- Review contract terms and pricing
- Assess current health score and trajectory
- Identify any outstanding issues or concerns
- Begin internal alignment on renewal strategy
### 90 Days Before Renewal
- Schedule renewal conversation with customer
- Prepare value delivered summary (ROI, usage stats, milestones achieved)
- Draft renewal proposal with recommended terms
- If at-risk: escalate and begin risk mitigation
### 60 Days Before Renewal
- Present renewal proposal to customer
- Negotiate terms if needed
- Address any concerns raised during the process
- Escalate blockers to leadership
### 30 Days Before Renewal
- Finalise contract terms
- Obtain signatures
- Plan for any post-renewal actions (expansion, migration)
- Update CRM with renewal details
### Post-Renewal
- Confirm renewed contract in systems
- Send thank-you and updated success plan
- Schedule next QBR
- Identify expansion opportunities
---
## Expansion Playbook
### Identifying Expansion Signals
| Signal | Expansion Type | Priority |
|--------|---------------|----------|
| Seat utilisation > 90% | Seat expansion | High |
| Requests for features in higher tier | Tier upsell | High |
| New department inquiries | Department expansion | Medium |
| High adoption of existing modules | Module cross-sell | Medium |
| Customer referencing competitors for missing features | Cross-sell | High |
### Expansion Conversation Framework
1. **Discovery:** "I noticed your team has been getting great value from [feature]. Have you considered how [new module] could help with [related business outcome]?"
2. **Value Framing:** "Companies similar to yours who adopted [module] saw [specific metric improvement]."
3. **Proposal:** "Based on your current usage, here's what the expansion would look like..."
4. **Stakeholder Alignment:** Involve the economic buyer early. The champion can advocate, but the budget holder decides.
5. **Close:** Coordinate with sales/account executive for commercial negotiation.
---
## Escalation Procedures
### Internal Escalation Matrix
| Trigger | Escalation Level | Response Time |
|---------|-----------------|---------------|
| Health score drops to Red | VP Customer Success | 24 hours |
| Executive sponsor leaves | Director CS + AE | 48 hours |
| Critical bug affecting customer | VP Engineering + VP CS | 4 hours |
| Customer mentions competitor evaluation | VP CS + VP Sales | 24 hours |
| Renewal at risk (60 days or less) | CRO/VP Sales | 24 hours |
| Customer threatens legal action | Legal + VP CS | Immediate |
### Escalation Communication Template
**Subject:** [ESCALATION] {Customer Name} -- {Brief Description}
**Body:**
- Customer: {name}, {segment}, ARR
- Health Score: {score} ({classification})
- Renewal Date: {date}
- Issue Summary: {2-3 sentences}
- Warning Signals: {list}
- Recommended Action: {specific next step}
- Urgency: {critical/high/medium}
---
**Last Updated:** February 2026
FILE:references/health-scoring-framework.md
# Health Scoring Framework
Complete methodology for multi-dimensional customer health scoring in SaaS customer success.
---
## Overview
Customer health scoring is the foundation of proactive customer success management. A well-calibrated health score enables CSMs to prioritise their portfolio, identify emerging risks before they become churn events, and allocate resources where they will have the greatest impact.
This framework uses a weighted, multi-dimensional approach that scores customers across four key areas: usage, engagement, support, and relationship. Each dimension contributes to an overall health score (0-100) that classifies accounts as Green (healthy), Yellow (needs attention), or Red (at risk).
---
## Scoring Dimensions
### 1. Usage (Weight: 30%)
Usage metrics are the strongest leading indicator of customer health. Customers who are not using the product are not deriving value and are at elevated churn risk.
| Metric | Definition | Scoring Method |
|--------|-----------|----------------|
| Login Frequency | Percentage of expected login days with actual logins | (actual / target) * 100, capped at 100 |
| Feature Adoption | Percentage of available features actively used | (adopted / available) * 100, capped at 100 |
| DAU/MAU Ratio | Daily active users divided by monthly active users | (actual / target) * 100, capped at 100 |
**Sub-weights within Usage:**
- Login Frequency: 35%
- Feature Adoption: 40%
- DAU/MAU Ratio: 25%
**Why 30% weight:** Usage is the most objective, data-driven signal. Declining usage almost always precedes churn. However, some customers may have seasonal usage patterns, which is why it is not weighted even higher.
### 2. Engagement (Weight: 25%)
Engagement measures how actively the customer participates in the relationship beyond just product usage.
| Metric | Definition | Scoring Method |
|--------|-----------|----------------|
| Support Ticket Volume | Number of support tickets in the period | Inverse score: (1 - actual/max) * 100 |
| Meeting Attendance | Percentage of scheduled meetings attended | (actual / target) * 100, capped at 100 |
| NPS Score | Net Promoter Score response (0-10) | (actual / target) * 100, capped at 100 |
| CSAT Score | Customer Satisfaction score (1-5) | (actual / target) * 100, capped at 100 |
**Sub-weights within Engagement:**
- Support Ticket Volume: 20% (inverse -- fewer tickets is better)
- Meeting Attendance: 30%
- NPS Score: 25%
- CSAT Score: 25%
**Why 25% weight:** Engagement signals complement usage data. A customer who attends meetings but does not use the product may be in an evaluation phase. A customer who uses the product but skips meetings may be becoming self-sufficient -- or disengaging.
### 3. Support (Weight: 20%)
Support health measures the quality of the customer's support experience, which directly impacts satisfaction and renewal likelihood.
| Metric | Definition | Scoring Method |
|--------|-----------|----------------|
| Open Tickets | Number of currently unresolved tickets | Inverse score: (1 - actual/max) * 100 |
| Escalation Rate | Percentage of tickets escalated | Inverse score: (1 - actual/max) * 100 |
| Avg Resolution Time | Average hours to resolve tickets | Inverse score: (1 - actual/max) * 100 |
**Sub-weights within Support:**
- Open Tickets: 35%
- Escalation Rate: 35%
- Resolution Time: 30%
**Why 20% weight:** Support issues are lagging indicators -- they tell you there is already a problem. However, unresolved support issues are a strong predictor of churn, especially when combined with declining engagement.
### 4. Relationship (Weight: 25%)
Relationship health measures the strength and depth of the human connection between the customer and your organisation.
| Metric | Definition | Scoring Method |
|--------|-----------|----------------|
| Executive Sponsor Engagement | Engagement level of exec sponsor (0-100) | (actual / target) * 100, capped at 100 |
| Multi-Threading Depth | Number of stakeholder contacts | (actual / target) * 100, capped at 100 |
| Renewal Sentiment | Qualitative sentiment assessment | Mapped to score: positive=100, neutral=60, negative=20, unknown=50 |
**Sub-weights within Relationship:**
- Executive Sponsor Engagement: 35%
- Multi-Threading Depth: 30%
- Renewal Sentiment: 35%
**Why 25% weight:** Relationship strength is the most important defence against competitive displacement. A customer with strong relationships will give you more chances to fix problems. A customer with weak relationships may leave without warning.
---
## Classification Thresholds
### Standard Thresholds
| Classification | Score Range | Meaning | Action |
|---------------|-------------|---------|--------|
| Green | 75-100 | Customer is healthy and achieving value | Standard cadence, focus on expansion |
| Yellow | 50-74 | Customer needs attention | Increase touch frequency, investigate root causes |
| Red | 0-49 | Customer is at risk | Immediate intervention, create save plan |
### Segment-Adjusted Thresholds
Enterprise customers typically have higher expectations and more complex deployments, which means a higher bar for "healthy." SMB customers may have simpler use cases and lower engagement expectations.
| Segment | Green Threshold | Yellow Threshold | Red Threshold |
|---------|----------------|------------------|---------------|
| Enterprise | 75-100 | 50-74 | 0-49 |
| Mid-Market | 70-100 | 45-69 | 0-44 |
| SMB | 65-100 | 40-64 | 0-39 |
### Segment-Specific Benchmarks
Each metric target is calibrated per segment. Enterprise customers are expected to have higher login frequency, attendance, and sponsor engagement. SMB customers have lower targets but still meaningful thresholds.
**Example Calibration:**
- Enterprise login frequency target: 90% (high-touch, deeply embedded)
- Mid-Market login frequency target: 80% (balanced engagement)
- SMB login frequency target: 70% (self-serve oriented)
---
## Trend Analysis
A single health score snapshot is useful. A health score trend is actionable.
### Trend Classification
| Trend | Criteria | Implication |
|-------|----------|-------------|
| Improving | Current > Previous by 5+ points | Positive trajectory, reinforce what is working |
| Stable | Within +/- 5 points | Maintain current approach |
| Declining | Current < Previous by 5+ points | Investigate and intervene |
| No Data | No previous period available | Establish baseline |
### Trend Priority Matrix
| Current Score | Trend | Priority |
|--------------|-------|----------|
| Green | Declining | HIGH -- intervene before it drops further |
| Yellow | Declining | CRITICAL -- trajectory leads to Red |
| Yellow | Improving | MEDIUM -- reinforce positive momentum |
| Red | Improving | HIGH -- support the recovery |
| Red | Stable | CRITICAL -- needs new intervention approach |
---
## Calibration Guidelines
### When to Recalibrate
1. **After major product changes**: New features may change what "good usage" looks like
2. **Seasonal patterns**: Some industries have cyclical usage (retail holiday season, fiscal year end)
3. **Portfolio composition changes**: If you add many SMB customers, the overall averages shift
4. **After churn events**: Review whether the health score predicted the churn
### Calibration Process
1. Export health scores for all customers over the past 12 months
2. Identify all churn events in the same period
3. Calculate the average health score of churned customers 90, 60, and 30 days before churn
4. Adjust thresholds so that churned customers would have been classified as Yellow or Red at least 60 days before churn
5. Validate with a holdout set of recent data
### Common Calibration Pitfalls
- **Threshold creep**: Gradually lowering Green thresholds to make the portfolio look healthier
- **Over-weighting lagging indicators**: Support metrics react after the damage is done
- **Ignoring segment differences**: Using one threshold for all segments
- **Sentiment bias**: Over-relying on subjective renewal sentiment
---
## Implementation Checklist
1. Define data sources for each metric (CRM, product analytics, support system)
2. Establish data refresh frequency (daily for usage, weekly for engagement)
3. Configure segment benchmarks for your customer base
4. Set initial thresholds using industry defaults (provided above)
5. Run a 30-day pilot with manual review of edge cases
6. Calibrate thresholds based on pilot results
7. Automate scoring and alerting
8. Review and recalibrate quarterly
---
**Last Updated:** February 2026
FILE:scripts/churn_risk_analyzer.py
#!/usr/bin/env python3
"""
Churn Risk Analyzer
Identifies at-risk customer accounts by scoring behavioral signals across
usage decline, engagement drop, support issues, relationship signals, and
commercial factors. Produces risk tiers with intervention playbooks and
time-to-renewal urgency multipliers.
Usage:
python churn_risk_analyzer.py customer_data.json
python churn_risk_analyzer.py customer_data.json --format json
"""
import argparse
import json
import sys
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
RISK_SIGNAL_WEIGHTS: Dict[str, float] = {
"usage_decline": 0.30,
"engagement_drop": 0.25,
"support_issues": 0.20,
"relationship_signals": 0.15,
"commercial_factors": 0.10,
}
RISK_TIERS: List[Dict[str, Any]] = [
{"name": "critical", "min": 80, "max": 100, "label": "CRITICAL", "action": "Immediate executive escalation"},
{"name": "high", "min": 60, "max": 79, "label": "HIGH", "action": "Urgent CSM intervention"},
{"name": "medium", "min": 40, "max": 59, "label": "MEDIUM", "action": "Proactive outreach"},
{"name": "low", "min": 0, "max": 39, "label": "LOW", "action": "Standard monitoring"},
]
WARNING_SEVERITY: Dict[str, int] = {
"critical": 4,
"high": 3,
"medium": 2,
"low": 1,
}
# Intervention playbooks per tier
INTERVENTION_PLAYBOOKS: Dict[str, List[str]] = {
"critical": [
"Schedule executive-to-executive call within 48 hours",
"Create detailed save plan with specific value milestones",
"Offer concessions or contract restructuring if needed",
"Assign dedicated rescue team (CSM + Solutions Engineer)",
"Daily internal stand-up on account status until stabilised",
"Prepare competitive displacement defence strategy",
],
"high": [
"Schedule urgent CSM call within 1 week",
"Conduct root cause analysis on declining metrics",
"Build 30-day recovery plan with measurable checkpoints",
"Re-engage executive sponsor for alignment meeting",
"Accelerate any pending feature requests or bug fixes",
"Increase touch frequency to weekly until improvement",
],
"medium": [
"Schedule proactive check-in within 2 weeks",
"Share relevant success stories and best practices",
"Propose training session or product walkthrough",
"Review current usage against success plan goals",
"Identify and address any unvoiced concerns",
"Bi-weekly monitoring until score improves to Low",
],
"low": [
"Maintain standard touch cadence",
"Share product updates and new feature announcements",
"Monitor health score trends monthly",
"Proactively share relevant industry insights",
"Prepare for upcoming renewal conversations (if within 90 days)",
],
}
SATISFACTION_TREND_SCORES: Dict[str, float] = {
"improving": 10.0,
"stable": 30.0,
"declining": 70.0,
"critical": 95.0,
}
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def safe_divide(numerator: float, denominator: float, default: float = 0.0) -> float:
"""Return numerator / denominator, or *default* when denominator is zero."""
if denominator == 0:
return default
return numerator / denominator
def clamp(value: float, lo: float = 0.0, hi: float = 100.0) -> float:
"""Clamp *value* between *lo* and *hi*."""
return max(lo, min(hi, value))
def days_until(date_str: Optional[str]) -> Optional[int]:
"""Return days from today until *date_str* (ISO format), or None."""
if not date_str:
return None
try:
target = datetime.strptime(date_str[:10], "%Y-%m-%d")
delta = (target - datetime.now()).days
return max(delta, 0)
except (ValueError, TypeError):
return None
def renewal_urgency_multiplier(days_remaining: Optional[int]) -> float:
"""Return a multiplier (1.0 - 1.5) based on proximity to renewal.
Closer renewals amplify the risk score.
"""
if days_remaining is None:
return 1.0
if days_remaining <= 30:
return 1.5
elif days_remaining <= 60:
return 1.35
elif days_remaining <= 90:
return 1.2
elif days_remaining <= 180:
return 1.1
return 1.0
def get_risk_tier(score: float) -> Dict[str, Any]:
"""Return the risk tier dict matching the score."""
for tier in RISK_TIERS:
if tier["min"] <= score <= tier["max"]:
return tier
return RISK_TIERS[-1] # default to low
# ---------------------------------------------------------------------------
# Signal Scoring
# ---------------------------------------------------------------------------
def score_usage_decline(data: Dict[str, Any]) -> Tuple[float, List[Dict[str, str]]]:
"""Score usage decline signals (0-100, higher = more risk)."""
warnings: List[Dict[str, str]] = []
login_trend = data.get("login_trend", 0) # negative = decline
feature_change = data.get("feature_adoption_change", 0)
dau_mau_change = data.get("dau_mau_change", 0)
# Convert declines to risk scores (0-100)
login_risk = clamp(abs(min(login_trend, 0)) * 3.0) # -33% => 100
feature_risk = clamp(abs(min(feature_change, 0)) * 4.0) # -25% => 100
dau_mau_risk = clamp(abs(min(dau_mau_change, 0)) * 500) # -0.20 => 100
score = round(login_risk * 0.40 + feature_risk * 0.35 + dau_mau_risk * 0.25, 1)
if login_trend <= -20:
warnings.append({"severity": "critical", "signal": f"Login frequency dropped {abs(login_trend)}%"})
elif login_trend <= -10:
warnings.append({"severity": "high", "signal": f"Login frequency declined {abs(login_trend)}%"})
elif login_trend < -5:
warnings.append({"severity": "medium", "signal": f"Login frequency dipping {abs(login_trend)}%"})
if feature_change <= -15:
warnings.append({"severity": "high", "signal": f"Feature adoption dropped {abs(feature_change)}%"})
elif feature_change < -5:
warnings.append({"severity": "medium", "signal": f"Feature adoption declining {abs(feature_change)}%"})
if dau_mau_change <= -0.10:
warnings.append({"severity": "high", "signal": f"DAU/MAU ratio fell by {abs(dau_mau_change):.2f}"})
return score, warnings
def score_engagement_drop(data: Dict[str, Any]) -> Tuple[float, List[Dict[str, str]]]:
"""Score engagement drop signals (0-100, higher = more risk)."""
warnings: List[Dict[str, str]] = []
cancellations = data.get("meeting_cancellations", 0)
response_days = data.get("response_time_days", 1)
nps_change = data.get("nps_change", 0)
cancel_risk = clamp(cancellations * 25.0) # 4 cancellations => 100
response_risk = clamp((response_days - 1) * 15.0) # 1 day baseline; 7+ days => 90+
nps_risk = clamp(abs(min(nps_change, 0)) * 20.0) # -5 => 100
score = round(cancel_risk * 0.30 + response_risk * 0.35 + nps_risk * 0.35, 1)
if cancellations >= 3:
warnings.append({"severity": "critical", "signal": f"{cancellations} meeting cancellations -- customer disengaging"})
elif cancellations >= 2:
warnings.append({"severity": "high", "signal": f"{cancellations} meeting cancellations recently"})
if response_days >= 7:
warnings.append({"severity": "critical", "signal": f"Customer response time: {response_days} days -- going dark"})
elif response_days >= 4:
warnings.append({"severity": "high", "signal": f"Customer response time increasing: {response_days} days"})
if nps_change <= -4:
warnings.append({"severity": "critical", "signal": f"NPS dropped by {abs(nps_change)} points"})
elif nps_change <= -2:
warnings.append({"severity": "high", "signal": f"NPS declined by {abs(nps_change)} points"})
return score, warnings
def score_support_issues(data: Dict[str, Any]) -> Tuple[float, List[Dict[str, str]]]:
"""Score support-related risk signals (0-100, higher = more risk)."""
warnings: List[Dict[str, str]] = []
escalations = data.get("open_escalations", 0)
critical_unresolved = data.get("unresolved_critical", 0)
sat_trend = data.get("satisfaction_trend", "stable").lower()
esc_risk = clamp(escalations * 35.0) # 3 escalations => 100
critical_risk = clamp(critical_unresolved * 50.0) # 2 unresolved critical => 100
sat_risk = SATISFACTION_TREND_SCORES.get(sat_trend, 30.0)
score = round(esc_risk * 0.35 + critical_risk * 0.35 + sat_risk * 0.30, 1)
if critical_unresolved >= 2:
warnings.append({"severity": "critical", "signal": f"{critical_unresolved} unresolved critical support tickets"})
elif critical_unresolved >= 1:
warnings.append({"severity": "high", "signal": "Unresolved critical support ticket"})
if escalations >= 2:
warnings.append({"severity": "high", "signal": f"{escalations} open escalations"})
elif escalations >= 1:
warnings.append({"severity": "medium", "signal": "Open support escalation"})
if sat_trend == "critical":
warnings.append({"severity": "critical", "signal": "Support satisfaction at critical levels"})
elif sat_trend == "declining":
warnings.append({"severity": "high", "signal": "Support satisfaction trending down"})
return score, warnings
def score_relationship_signals(data: Dict[str, Any]) -> Tuple[float, List[Dict[str, str]]]:
"""Score relationship risk signals (0-100, higher = more risk)."""
warnings: List[Dict[str, str]] = []
risk_points = 0.0
champion_left = data.get("champion_left", False)
sponsor_change = data.get("sponsor_change", False)
competitor_mentions = data.get("competitor_mentions", 0)
if champion_left:
risk_points += 45.0
warnings.append({"severity": "critical", "signal": "Internal champion has left the organisation"})
if sponsor_change:
risk_points += 30.0
warnings.append({"severity": "high", "signal": "Executive sponsor change detected"})
if competitor_mentions >= 3:
risk_points += 35.0
warnings.append({"severity": "critical", "signal": f"Customer mentioned competitors {competitor_mentions} times"})
elif competitor_mentions >= 1:
risk_points += competitor_mentions * 12.0
warnings.append({"severity": "medium", "signal": f"Customer mentioned competitor {competitor_mentions} time(s)"})
score = clamp(risk_points)
return round(score, 1), warnings
def score_commercial_factors(data: Dict[str, Any]) -> Tuple[float, List[Dict[str, str]]]:
"""Score commercial risk factors (0-100, higher = more risk)."""
warnings: List[Dict[str, str]] = []
risk_points = 0.0
contract_type = data.get("contract_type", "annual").lower()
pricing_complaints = data.get("pricing_complaints", False)
budget_cuts = data.get("budget_cuts_mentioned", False)
if contract_type == "month-to-month":
risk_points += 30.0
warnings.append({"severity": "medium", "signal": "Month-to-month contract -- low switching cost"})
elif contract_type == "quarterly":
risk_points += 15.0
if pricing_complaints:
risk_points += 35.0
warnings.append({"severity": "high", "signal": "Customer has raised pricing complaints"})
if budget_cuts:
risk_points += 40.0
warnings.append({"severity": "high", "signal": "Customer mentioned budget cuts or cost reduction"})
score = clamp(risk_points)
return round(score, 1), warnings
# ---------------------------------------------------------------------------
# Main Analysis
# ---------------------------------------------------------------------------
def analyse_churn_risk(customer: Dict[str, Any]) -> Dict[str, Any]:
"""Analyse churn risk for a single customer."""
usage_score, usage_warnings = score_usage_decline(customer.get("usage_decline", {}))
engagement_score, engagement_warnings = score_engagement_drop(customer.get("engagement_drop", {}))
support_score, support_warnings = score_support_issues(customer.get("support_issues", {}))
relationship_score, relationship_warnings = score_relationship_signals(customer.get("relationship_signals", {}))
commercial_score, commercial_warnings = score_commercial_factors(customer.get("commercial_factors", {}))
# Weighted raw score
raw_score = (
usage_score * RISK_SIGNAL_WEIGHTS["usage_decline"]
+ engagement_score * RISK_SIGNAL_WEIGHTS["engagement_drop"]
+ support_score * RISK_SIGNAL_WEIGHTS["support_issues"]
+ relationship_score * RISK_SIGNAL_WEIGHTS["relationship_signals"]
+ commercial_score * RISK_SIGNAL_WEIGHTS["commercial_factors"]
)
# Apply renewal urgency multiplier
remaining = days_until(customer.get("contract_end_date"))
multiplier = renewal_urgency_multiplier(remaining)
adjusted_score = clamp(round(raw_score * multiplier, 1))
tier = get_risk_tier(adjusted_score)
# Collect and sort warnings by severity
all_warnings = usage_warnings + engagement_warnings + support_warnings + relationship_warnings + commercial_warnings
all_warnings.sort(key=lambda w: WARNING_SEVERITY.get(w["severity"], 0), reverse=True)
playbook = INTERVENTION_PLAYBOOKS.get(tier["name"], [])
return {
"customer_id": customer.get("customer_id", "unknown"),
"name": customer.get("name", "Unknown"),
"segment": customer.get("segment", "unknown"),
"arr": customer.get("arr", 0),
"risk_score": adjusted_score,
"raw_score": round(raw_score, 1),
"risk_tier": tier["name"],
"risk_label": tier["label"],
"urgency_multiplier": multiplier,
"days_to_renewal": remaining,
"signal_scores": {
"usage_decline": {"score": usage_score, "weight": "30%"},
"engagement_drop": {"score": engagement_score, "weight": "25%"},
"support_issues": {"score": support_score, "weight": "20%"},
"relationship_signals": {"score": relationship_score, "weight": "15%"},
"commercial_factors": {"score": commercial_score, "weight": "10%"},
},
"warning_signals": all_warnings,
"recommended_actions": playbook,
}
# ---------------------------------------------------------------------------
# Output Formatting
# ---------------------------------------------------------------------------
def format_text(results: List[Dict[str, Any]]) -> str:
"""Format results as human-readable text."""
lines: List[str] = []
lines.append("=" * 72)
lines.append("CHURN RISK ANALYSIS REPORT")
lines.append("=" * 72)
lines.append("")
total = len(results)
critical_count = sum(1 for r in results if r["risk_tier"] == "critical")
high_count = sum(1 for r in results if r["risk_tier"] == "high")
medium_count = sum(1 for r in results if r["risk_tier"] == "medium")
low_count = sum(1 for r in results if r["risk_tier"] == "low")
total_arr_at_risk = sum(r["arr"] for r in results if r["risk_tier"] in ("critical", "high"))
lines.append(f"Portfolio Summary: {total} customers analysed")
lines.append(f" Critical Risk: {critical_count}")
lines.append(f" High Risk: {high_count}")
lines.append(f" Medium Risk: {medium_count}")
lines.append(f" Low Risk: {low_count}")
lines.append(f" ARR at Risk (Critical + High): ,.0f")
lines.append("")
# Sort by risk score descending
sorted_results = sorted(results, key=lambda r: r["risk_score"], reverse=True)
for r in sorted_results:
lines.append("-" * 72)
lines.append(f"Customer: {r['name']} ({r['customer_id']})")
lines.append(f"Segment: {r['segment'].title()} | ARR: ,.0f")
renewal_str = f"{r['days_to_renewal']} days" if r["days_to_renewal"] is not None else "N/A"
lines.append(f"Risk Score: {r['risk_score']}/100 [{r['risk_label']}] | Renewal: {renewal_str}")
if r["urgency_multiplier"] > 1.0:
lines.append(f" ** Urgency multiplier applied: {r['urgency_multiplier']}x (renewal approaching)")
lines.append("")
lines.append(" Signal Scores:")
for signal_name, signal_data in r["signal_scores"].items():
display_name = signal_name.replace("_", " ").title()
lines.append(f" {display_name:25s} {signal_data['score']:6.1f}/100 ({signal_data['weight']})")
if r["warning_signals"]:
lines.append("")
lines.append(" Warning Signals:")
for w in r["warning_signals"]:
severity_tag = w["severity"].upper()
lines.append(f" [{severity_tag}] {w['signal']}")
if r["recommended_actions"]:
lines.append("")
lines.append(" Recommended Actions:")
for i, action in enumerate(r["recommended_actions"], 1):
lines.append(f" {i}. {action}")
lines.append("")
lines.append("=" * 72)
return "\n".join(lines)
def format_json(results: List[Dict[str, Any]]) -> str:
"""Format results as JSON."""
total = len(results)
output = {
"report": "churn_risk_analysis",
"summary": {
"total_customers": total,
"critical_count": sum(1 for r in results if r["risk_tier"] == "critical"),
"high_count": sum(1 for r in results if r["risk_tier"] == "high"),
"medium_count": sum(1 for r in results if r["risk_tier"] == "medium"),
"low_count": sum(1 for r in results if r["risk_tier"] == "low"),
"total_arr_at_risk": sum(r["arr"] for r in results if r["risk_tier"] in ("critical", "high")),
},
"customers": sorted(results, key=lambda r: r["risk_score"], reverse=True),
}
return json.dumps(output, indent=2)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Analyse churn risk with behavioral signal detection and intervention recommendations."
)
parser.add_argument("input_file", help="Path to JSON file containing customer data")
parser.add_argument(
"--format",
choices=["text", "json"],
default="text",
dest="output_format",
help="Output format (default: text)",
)
args = parser.parse_args()
try:
with open(args.input_file, "r") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: File not found: {args.input_file}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in {args.input_file}: {e}", file=sys.stderr)
sys.exit(1)
customers = data.get("customers", [])
if not customers:
print("Error: No customer records found in input file.", file=sys.stderr)
sys.exit(1)
results = [analyse_churn_risk(c) for c in customers]
if args.output_format == "json":
print(format_json(results))
else:
print(format_text(results))
if __name__ == "__main__":
main()
FILE:scripts/expansion_opportunity_scorer.py
#!/usr/bin/env python3
"""
Expansion Opportunity Scorer
Analyses customer product adoption depth, maps whitespace for unused
features/products, estimates revenue opportunities, and prioritises
expansion plays by effort vs impact.
Usage:
python expansion_opportunity_scorer.py customer_data.json
python expansion_opportunity_scorer.py customer_data.json --format json
"""
import argparse
import json
import sys
from typing import Any, Dict, List, Optional, Tuple
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
# Tier pricing multipliers (relative to current plan price)
TIER_UPLIFT: Dict[str, float] = {
"starter": 1.0,
"professional": 1.8,
"enterprise": 3.0,
"enterprise_plus": 4.5,
}
# Module revenue estimates as a fraction of base ARR
MODULE_REVENUE_FRACTION: Dict[str, float] = {
"core_platform": 0.00, # Already included in base
"analytics_module": 0.15,
"integrations_module": 0.12,
"api_access": 0.10,
"advanced_reporting": 0.18,
"security_module": 0.20,
"automation_module": 0.15,
"collaboration_module": 0.10,
"data_export": 0.08,
"custom_workflows": 0.22,
"sso_module": 0.08,
"audit_module": 0.10,
}
# Effort classification for different expansion types
EFFORT_MAP: Dict[str, str] = {
"upsell_tier": "medium",
"cross_sell_module": "low",
"seat_expansion": "low",
"department_expansion": "high",
}
# Usage thresholds for recommendations
HIGH_USAGE_THRESHOLD = 75 # % usage indicates readiness for more
LOW_ADOPTION_THRESHOLD = 30 # % usage is too low to push expansion there
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def safe_divide(numerator: float, denominator: float, default: float = 0.0) -> float:
"""Return numerator / denominator, or *default* when denominator is zero."""
if denominator == 0:
return default
return numerator / denominator
def clamp(value: float, lo: float = 0.0, hi: float = 100.0) -> float:
"""Clamp *value* between *lo* and *hi*."""
return max(lo, min(hi, value))
def estimate_seat_expansion_revenue(
arr: float, licensed: int, active: int, segment: str
) -> Tuple[float, str]:
"""Estimate revenue from seat expansion.
Returns (estimated_revenue, rationale).
"""
utilisation = safe_divide(active, licensed)
if utilisation >= 0.90:
# Near capacity -- likely needs more seats
growth_factor = {"enterprise": 0.25, "mid-market": 0.20, "smb": 0.15}
factor = growth_factor.get(segment.lower(), 0.15)
revenue = round(arr * factor, 0)
return revenue, f"Seat utilisation at {utilisation:.0%} -- likely needs {int(licensed * factor)} additional seats"
return 0.0, f"Seat utilisation at {utilisation:.0%} -- not yet at expansion threshold"
def estimate_tier_upgrade_revenue(
arr: float, current_tier: str, available_tiers: List[str]
) -> Tuple[float, Optional[str], str]:
"""Estimate revenue from tier upgrade.
Returns (estimated_revenue, target_tier, rationale).
"""
current_mult = TIER_UPLIFT.get(current_tier.lower(), 1.0)
best_revenue = 0.0
best_tier = None
rationale = "Already on highest tier"
for tier in available_tiers:
tier_mult = TIER_UPLIFT.get(tier.lower(), 1.0)
if tier_mult > current_mult:
# Calculate revenue as the incremental ARR from upgrading
base_arr = safe_divide(arr, current_mult)
upgrade_arr = base_arr * tier_mult
incremental = upgrade_arr - arr
if incremental > best_revenue:
# Pick the next tier up (not skip tiers)
if best_tier is None or tier_mult < TIER_UPLIFT.get(best_tier.lower(), 999):
best_revenue = round(incremental, 0)
best_tier = tier
rationale = f"Upgrade from {current_tier} to {tier} adds ,.0f ARR"
return best_revenue, best_tier, rationale
def estimate_module_revenue(
arr: float, product_usage: Dict[str, Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""Identify cross-sell opportunities from unadopted modules.
Returns list of opportunity dicts.
"""
opportunities: List[Dict[str, Any]] = []
for module_name, module_data in product_usage.items():
adopted = module_data.get("adopted", False)
usage_pct = module_data.get("usage_pct", 0)
fraction = MODULE_REVENUE_FRACTION.get(module_name.lower(), 0.10)
if not adopted and fraction > 0:
revenue = round(arr * fraction, 0)
opportunities.append({
"module": module_name,
"type": "cross_sell",
"estimated_revenue": revenue,
"effort": "low",
"rationale": f"Module not adopted -- ,.0f potential ARR",
})
elif adopted and usage_pct < LOW_ADOPTION_THRESHOLD and fraction > 0:
# Already adopted but underutilised -- focus on enablement, not expansion
pass # Skip -- needs enablement, not a sales motion
return opportunities
def estimate_department_expansion_revenue(
arr: float,
current_departments: List[str],
potential_departments: List[str],
segment: str,
) -> List[Dict[str, Any]]:
"""Estimate revenue from expanding to new departments."""
opportunities: List[Dict[str, Any]] = []
current_set = {d.lower() for d in current_departments}
per_dept_estimate = safe_divide(arr, max(len(current_departments), 1))
for dept in potential_departments:
if dept.lower() not in current_set:
# Estimate each new department at the average per-department ARR
revenue = round(per_dept_estimate * 0.8, 0) # Slight discount for new dept
opportunities.append({
"department": dept,
"type": "expansion",
"estimated_revenue": revenue,
"effort": "high",
"rationale": f"Expand to {dept} department -- est. ,.0f ARR",
})
return opportunities
# ---------------------------------------------------------------------------
# Priority Scoring
# ---------------------------------------------------------------------------
def priority_score(revenue: float, effort: str) -> float:
"""Calculate priority score (higher = better).
Favours high revenue with low effort.
"""
effort_multiplier = {"low": 3.0, "medium": 2.0, "high": 1.0}
mult = effort_multiplier.get(effort.lower(), 1.0)
# Normalise revenue to a 0-100 scale (assume max single opportunity is $200k)
rev_score = clamp(safe_divide(revenue, 2000.0)) # $200k => 100
return round(rev_score * mult, 1)
# ---------------------------------------------------------------------------
# Main Analysis
# ---------------------------------------------------------------------------
def analyse_expansion(customer: Dict[str, Any]) -> Dict[str, Any]:
"""Analyse expansion opportunities for a single customer."""
arr = customer.get("arr", 0)
segment = customer.get("segment", "mid-market").lower()
contract = customer.get("contract", {})
product_usage = customer.get("product_usage", {})
departments = customer.get("departments", {})
all_opportunities: List[Dict[str, Any]] = []
# 1. Seat expansion
licensed = contract.get("licensed_seats", 0)
active = contract.get("active_seats", 0)
seat_rev, seat_rationale = estimate_seat_expansion_revenue(arr, licensed, active, segment)
if seat_rev > 0:
all_opportunities.append({
"type": "expansion",
"category": "seat_expansion",
"estimated_revenue": seat_rev,
"effort": "low",
"rationale": seat_rationale,
"priority_score": priority_score(seat_rev, "low"),
})
# 2. Tier upgrade
current_tier = contract.get("plan_tier", "").lower()
available_tiers = contract.get("available_tiers", [])
tier_rev, target_tier, tier_rationale = estimate_tier_upgrade_revenue(arr, current_tier, available_tiers)
if tier_rev > 0 and target_tier:
all_opportunities.append({
"type": "upsell",
"category": "tier_upgrade",
"target_tier": target_tier,
"estimated_revenue": tier_rev,
"effort": "medium",
"rationale": tier_rationale,
"priority_score": priority_score(tier_rev, "medium"),
})
# 3. Module cross-sell
module_opps = estimate_module_revenue(arr, product_usage)
for opp in module_opps:
opp["category"] = "module_cross_sell"
opp["priority_score"] = priority_score(opp["estimated_revenue"], opp["effort"])
all_opportunities.append(opp)
# 4. Department expansion
current_depts = departments.get("current", [])
potential_depts = departments.get("potential", [])
dept_opps = estimate_department_expansion_revenue(arr, current_depts, potential_depts, segment)
for opp in dept_opps:
opp["category"] = "department_expansion"
opp["priority_score"] = priority_score(opp["estimated_revenue"], opp["effort"])
all_opportunities.append(opp)
# Sort by priority score descending
all_opportunities.sort(key=lambda o: o["priority_score"], reverse=True)
# Adoption depth summary
total_modules = len(product_usage)
adopted_modules = sum(1 for m in product_usage.values() if m.get("adopted", False))
avg_usage = round(
safe_divide(
sum(m.get("usage_pct", 0) for m in product_usage.values() if m.get("adopted", False)),
max(adopted_modules, 1),
),
1,
)
total_estimated_revenue = sum(o["estimated_revenue"] for o in all_opportunities)
return {
"customer_id": customer.get("customer_id", "unknown"),
"name": customer.get("name", "Unknown"),
"segment": segment,
"arr": arr,
"adoption_summary": {
"total_modules": total_modules,
"adopted_modules": adopted_modules,
"adoption_rate": round(safe_divide(adopted_modules, total_modules) * 100, 1) if total_modules > 0 else 0,
"avg_usage_pct": avg_usage,
"seat_utilisation": round(safe_divide(active, max(licensed, 1)) * 100, 1),
"current_tier": current_tier,
"departments_covered": len(current_depts),
"departments_potential": len(potential_depts),
},
"total_estimated_revenue": round(total_estimated_revenue, 0),
"opportunity_count": len(all_opportunities),
"opportunities": all_opportunities,
}
# ---------------------------------------------------------------------------
# Output Formatting
# ---------------------------------------------------------------------------
def format_text(results: List[Dict[str, Any]]) -> str:
"""Format results as human-readable text."""
lines: List[str] = []
lines.append("=" * 72)
lines.append("EXPANSION OPPORTUNITY REPORT")
lines.append("=" * 72)
lines.append("")
total_rev = sum(r["total_estimated_revenue"] for r in results)
total_opps = sum(r["opportunity_count"] for r in results)
lines.append(f"Portfolio Summary: {len(results)} customers")
lines.append(f" Total Expansion Revenue Potential: ,.0f")
lines.append(f" Total Opportunities Identified: {total_opps}")
lines.append("")
# Sort customers by total estimated revenue descending
sorted_results = sorted(results, key=lambda r: r["total_estimated_revenue"], reverse=True)
for r in sorted_results:
lines.append("-" * 72)
lines.append(f"Customer: {r['name']} ({r['customer_id']})")
lines.append(f"Segment: {r['segment'].title()} | Current ARR: ,.0f")
lines.append(f"Total Expansion Potential: ,.0f ({r['opportunity_count']} opportunities)")
lines.append("")
adoption = r["adoption_summary"]
lines.append(" Adoption Summary:")
lines.append(f" Modules Adopted: {adoption['adopted_modules']}/{adoption['total_modules']} ({adoption['adoption_rate']}%)")
lines.append(f" Avg Module Usage: {adoption['avg_usage_pct']}%")
lines.append(f" Seat Utilisation: {adoption['seat_utilisation']}%")
lines.append(f" Current Tier: {adoption['current_tier'].title()}")
lines.append(f" Departments: {adoption['departments_covered']} active, {adoption['departments_potential']} potential")
if r["opportunities"]:
lines.append("")
lines.append(" Opportunities (ranked by priority):")
for i, opp in enumerate(r["opportunities"], 1):
opp_type = opp.get("type", "unknown").title()
category = opp.get("category", "").replace("_", " ").title()
rev = opp["estimated_revenue"]
effort = opp.get("effort", "unknown").title()
pri = opp.get("priority_score", 0)
lines.append(f" {i}. [{opp_type}] {category}")
lines.append(f" Revenue: ,.0f | Effort: {effort} | Priority: {pri}")
lines.append(f" {opp.get('rationale', '')}")
else:
lines.append("")
lines.append(" No expansion opportunities identified at this time.")
lines.append("")
lines.append("=" * 72)
return "\n".join(lines)
def format_json(results: List[Dict[str, Any]]) -> str:
"""Format results as JSON."""
total_rev = sum(r["total_estimated_revenue"] for r in results)
total_opps = sum(r["opportunity_count"] for r in results)
output = {
"report": "expansion_opportunities",
"summary": {
"total_customers": len(results),
"total_estimated_revenue": total_rev,
"total_opportunities": total_opps,
},
"customers": sorted(results, key=lambda r: r["total_estimated_revenue"], reverse=True),
}
return json.dumps(output, indent=2)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Score expansion opportunities with adoption analysis and revenue estimation."
)
parser.add_argument("input_file", help="Path to JSON file containing customer data")
parser.add_argument(
"--format",
choices=["text", "json"],
default="text",
dest="output_format",
help="Output format (default: text)",
)
args = parser.parse_args()
try:
with open(args.input_file, "r") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: File not found: {args.input_file}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in {args.input_file}: {e}", file=sys.stderr)
sys.exit(1)
customers = data.get("customers", [])
if not customers:
print("Error: No customer records found in input file.", file=sys.stderr)
sys.exit(1)
results = [analyse_expansion(c) for c in customers]
if args.output_format == "json":
print(format_json(results))
else:
print(format_text(results))
if __name__ == "__main__":
main()
FILE:scripts/health_score_calculator.py
#!/usr/bin/env python3
"""
Customer Health Score Calculator
Multi-dimensional weighted health scoring across usage, engagement, support,
and relationship dimensions. Produces Red/Yellow/Green classification with
trend analysis and segment-aware benchmarking.
Usage:
python health_score_calculator.py customer_data.json
python health_score_calculator.py customer_data.json --format json
"""
import argparse
import json
import sys
from typing import Any, Dict, List, Optional, Tuple
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
DIMENSION_WEIGHTS: Dict[str, float] = {
"usage": 0.30,
"engagement": 0.25,
"support": 0.20,
"relationship": 0.25,
}
# Segment-specific thresholds (green_min, yellow_min)
SEGMENT_THRESHOLDS: Dict[str, Dict[str, Tuple[int, int]]] = {
"enterprise": {"green": (75, 100), "yellow": (50, 74), "red": (0, 49)},
"mid-market": {"green": (70, 100), "yellow": (45, 69), "red": (0, 44)},
"smb": {"green": (65, 100), "yellow": (40, 64), "red": (0, 39)},
}
# Benchmarks per segment for normalising raw metrics
SEGMENT_BENCHMARKS: Dict[str, Dict[str, Any]] = {
"enterprise": {
"login_frequency_target": 90,
"feature_adoption_target": 80,
"dau_mau_target": 0.50,
"support_ticket_volume_max": 5,
"meeting_attendance_target": 95,
"nps_target": 9,
"csat_target": 4.5,
"open_tickets_max": 10,
"escalation_rate_max": 0.25,
"avg_resolution_hours_max": 72,
"exec_sponsor_target": 90,
"multi_threading_target": 5,
},
"mid-market": {
"login_frequency_target": 80,
"feature_adoption_target": 70,
"dau_mau_target": 0.40,
"support_ticket_volume_max": 8,
"meeting_attendance_target": 85,
"nps_target": 8,
"csat_target": 4.0,
"open_tickets_max": 15,
"escalation_rate_max": 0.30,
"avg_resolution_hours_max": 96,
"exec_sponsor_target": 75,
"multi_threading_target": 3,
},
"smb": {
"login_frequency_target": 70,
"feature_adoption_target": 60,
"dau_mau_target": 0.30,
"support_ticket_volume_max": 10,
"meeting_attendance_target": 75,
"nps_target": 7,
"csat_target": 3.8,
"open_tickets_max": 20,
"escalation_rate_max": 0.40,
"avg_resolution_hours_max": 120,
"exec_sponsor_target": 60,
"multi_threading_target": 2,
},
}
RENEWAL_SENTIMENT_SCORES: Dict[str, float] = {
"positive": 100.0,
"neutral": 60.0,
"negative": 20.0,
"unknown": 50.0,
}
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def safe_divide(numerator: float, denominator: float, default: float = 0.0) -> float:
"""Return numerator / denominator, or *default* when denominator is zero."""
if denominator == 0:
return default
return numerator / denominator
def clamp(value: float, lo: float = 0.0, hi: float = 100.0) -> float:
"""Clamp *value* between *lo* and *hi*."""
return max(lo, min(hi, value))
def get_benchmarks(segment: str) -> Dict[str, Any]:
"""Return benchmarks for the given segment, falling back to mid-market."""
return SEGMENT_BENCHMARKS.get(segment.lower(), SEGMENT_BENCHMARKS["mid-market"])
def get_thresholds(segment: str) -> Dict[str, Tuple[int, int]]:
"""Return classification thresholds for the given segment."""
return SEGMENT_THRESHOLDS.get(segment.lower(), SEGMENT_THRESHOLDS["mid-market"])
def classify(score: float, segment: str) -> str:
"""Return 'green', 'yellow', or 'red' classification."""
thresholds = get_thresholds(segment)
if score >= thresholds["green"][0]:
return "green"
elif score >= thresholds["yellow"][0]:
return "yellow"
return "red"
def trend_direction(current: float, previous: Optional[float]) -> str:
"""Return trend direction string."""
if previous is None:
return "no_data"
diff = current - previous
if diff > 5:
return "improving"
elif diff < -5:
return "declining"
return "stable"
# ---------------------------------------------------------------------------
# Dimension Scoring
# ---------------------------------------------------------------------------
def score_usage(data: Dict[str, Any], benchmarks: Dict[str, Any]) -> Tuple[float, List[str]]:
"""Score the usage dimension (0-100).
Metrics: login_frequency, feature_adoption, dau_mau_ratio.
"""
recommendations: List[str] = []
login = clamp(safe_divide(data.get("login_frequency", 0), benchmarks["login_frequency_target"]) * 100)
adoption = clamp(safe_divide(data.get("feature_adoption", 0), benchmarks["feature_adoption_target"]) * 100)
dau_mau = clamp(safe_divide(data.get("dau_mau_ratio", 0), benchmarks["dau_mau_target"]) * 100)
score = round(login * 0.35 + adoption * 0.40 + dau_mau * 0.25, 1)
if login < 60:
recommendations.append("Login frequency below target -- schedule product engagement session")
if adoption < 50:
recommendations.append("Feature adoption is low -- recommend guided feature walkthrough")
if dau_mau < 50:
recommendations.append("DAU/MAU ratio indicates shallow usage -- investigate stickiness barriers")
return score, recommendations
def score_engagement(data: Dict[str, Any], benchmarks: Dict[str, Any]) -> Tuple[float, List[str]]:
"""Score the engagement dimension (0-100).
Metrics: support_ticket_volume (inverse), meeting_attendance, nps_score, csat_score.
"""
recommendations: List[str] = []
# Lower ticket volume is better -- invert
ticket_vol = data.get("support_ticket_volume", 0)
ticket_score = clamp((1.0 - safe_divide(ticket_vol, benchmarks["support_ticket_volume_max"])) * 100)
attendance = clamp(safe_divide(data.get("meeting_attendance", 0), benchmarks["meeting_attendance_target"]) * 100)
nps_raw = data.get("nps_score", 5)
nps_score = clamp(safe_divide(nps_raw, benchmarks["nps_target"]) * 100)
csat_raw = data.get("csat_score", 3.0)
csat_score = clamp(safe_divide(csat_raw, benchmarks["csat_target"]) * 100)
score = round(ticket_score * 0.20 + attendance * 0.30 + nps_score * 0.25 + csat_score * 0.25, 1)
if attendance < 60:
recommendations.append("Meeting attendance is low -- re-evaluate meeting cadence and agenda value")
if nps_raw < 7:
recommendations.append("NPS below threshold -- conduct a feedback deep-dive with customer")
if csat_raw < 3.5:
recommendations.append("CSAT is critically low -- escalate to support leadership")
return score, recommendations
def score_support(data: Dict[str, Any], benchmarks: Dict[str, Any]) -> Tuple[float, List[str]]:
"""Score the support dimension (0-100).
Metrics: open_tickets (inverse), escalation_rate (inverse), avg_resolution_hours (inverse).
"""
recommendations: List[str] = []
open_tix = data.get("open_tickets", 0)
open_score = clamp((1.0 - safe_divide(open_tix, benchmarks["open_tickets_max"])) * 100)
esc_rate = data.get("escalation_rate", 0)
esc_score = clamp((1.0 - safe_divide(esc_rate, benchmarks["escalation_rate_max"])) * 100)
res_hours = data.get("avg_resolution_hours", 0)
res_score = clamp((1.0 - safe_divide(res_hours, benchmarks["avg_resolution_hours_max"])) * 100)
score = round(open_score * 0.35 + esc_score * 0.35 + res_score * 0.30, 1)
if open_tix > benchmarks["open_tickets_max"] * 0.5:
recommendations.append("Open ticket count elevated -- prioritise ticket resolution")
if esc_rate > benchmarks["escalation_rate_max"] * 0.5:
recommendations.append("Escalation rate too high -- review support process and training")
if res_hours > benchmarks["avg_resolution_hours_max"] * 0.5:
recommendations.append("Resolution time exceeds SLA target -- engage support leadership")
return score, recommendations
def score_relationship(data: Dict[str, Any], benchmarks: Dict[str, Any]) -> Tuple[float, List[str]]:
"""Score the relationship dimension (0-100).
Metrics: executive_sponsor_engagement, multi_threading_depth, renewal_sentiment.
"""
recommendations: List[str] = []
exec_score = clamp(safe_divide(data.get("executive_sponsor_engagement", 0), benchmarks["exec_sponsor_target"]) * 100)
threading = data.get("multi_threading_depth", 1)
thread_score = clamp(safe_divide(threading, benchmarks["multi_threading_target"]) * 100)
sentiment_str = data.get("renewal_sentiment", "unknown").lower()
sentiment_score = RENEWAL_SENTIMENT_SCORES.get(sentiment_str, 50.0)
score = round(exec_score * 0.35 + thread_score * 0.30 + sentiment_score * 0.35, 1)
if exec_score < 50:
recommendations.append("Executive sponsor engagement is weak -- schedule executive alignment meeting")
if threading < 2:
recommendations.append("Single-threaded relationship -- expand contacts across departments")
if sentiment_str == "negative":
recommendations.append("Renewal sentiment is negative -- initiate save plan immediately")
return score, recommendations
# ---------------------------------------------------------------------------
# Main Scoring
# ---------------------------------------------------------------------------
def calculate_health_score(customer: Dict[str, Any]) -> Dict[str, Any]:
"""Calculate the overall health score for a single customer."""
segment = customer.get("segment", "mid-market").lower()
benchmarks = get_benchmarks(segment)
# Score each dimension
usage_score, usage_recs = score_usage(customer.get("usage", {}), benchmarks)
engagement_score, engagement_recs = score_engagement(customer.get("engagement", {}), benchmarks)
support_score, support_recs = score_support(customer.get("support", {}), benchmarks)
relationship_score, relationship_recs = score_relationship(customer.get("relationship", {}), benchmarks)
# Weighted overall
overall = round(
usage_score * DIMENSION_WEIGHTS["usage"]
+ engagement_score * DIMENSION_WEIGHTS["engagement"]
+ support_score * DIMENSION_WEIGHTS["support"]
+ relationship_score * DIMENSION_WEIGHTS["relationship"],
1,
)
classification = classify(overall, segment)
# Trend analysis
prev = customer.get("previous_period", {})
trends = {
"usage": trend_direction(usage_score, prev.get("usage_score")),
"engagement": trend_direction(engagement_score, prev.get("engagement_score")),
"support": trend_direction(support_score, prev.get("support_score")),
"relationship": trend_direction(relationship_score, prev.get("relationship_score")),
}
overall_prev = prev.get("overall_score")
trends["overall"] = trend_direction(overall, overall_prev)
# Combine recommendations
all_recs = usage_recs + engagement_recs + support_recs + relationship_recs
return {
"customer_id": customer.get("customer_id", "unknown"),
"name": customer.get("name", "Unknown"),
"segment": segment,
"arr": customer.get("arr", 0),
"overall_score": overall,
"classification": classification,
"dimensions": {
"usage": {"score": usage_score, "weight": "30%", "classification": classify(usage_score, segment)},
"engagement": {"score": engagement_score, "weight": "25%", "classification": classify(engagement_score, segment)},
"support": {"score": support_score, "weight": "20%", "classification": classify(support_score, segment)},
"relationship": {"score": relationship_score, "weight": "25%", "classification": classify(relationship_score, segment)},
},
"trends": trends,
"recommendations": all_recs,
}
# ---------------------------------------------------------------------------
# Output Formatting
# ---------------------------------------------------------------------------
CLASSIFICATION_LABELS = {
"green": "HEALTHY",
"yellow": "NEEDS ATTENTION",
"red": "AT RISK",
}
def format_text(results: List[Dict[str, Any]]) -> str:
"""Format results as human-readable text."""
lines: List[str] = []
lines.append("=" * 72)
lines.append("CUSTOMER HEALTH SCORE REPORT")
lines.append("=" * 72)
lines.append("")
# Portfolio summary
total = len(results)
green_count = sum(1 for r in results if r["classification"] == "green")
yellow_count = sum(1 for r in results if r["classification"] == "yellow")
red_count = sum(1 for r in results if r["classification"] == "red")
avg_score = round(safe_divide(sum(r["overall_score"] for r in results), total), 1)
lines.append(f"Portfolio Summary: {total} customers")
lines.append(f" Average Health Score: {avg_score}/100")
lines.append(f" Green (Healthy): {green_count}")
lines.append(f" Yellow (Attention): {yellow_count}")
lines.append(f" Red (At Risk): {red_count}")
lines.append("")
for r in results:
label = CLASSIFICATION_LABELS.get(r["classification"], "UNKNOWN")
lines.append("-" * 72)
lines.append(f"Customer: {r['name']} ({r['customer_id']})")
lines.append(f"Segment: {r['segment'].title()} | ARR: ,.0f")
lines.append(f"Overall Score: {r['overall_score']}/100 [{label}]")
lines.append("")
lines.append(" Dimension Scores:")
for dim_name, dim_data in r["dimensions"].items():
dim_label = CLASSIFICATION_LABELS.get(dim_data["classification"], "")
lines.append(f" {dim_name.title():15s} {dim_data['score']:6.1f}/100 ({dim_data['weight']}) [{dim_label}]")
lines.append("")
lines.append(" Trends:")
for dim_name, direction in r["trends"].items():
arrow = {"improving": "+", "declining": "-", "stable": "=", "no_data": "?"}
lines.append(f" {dim_name.title():15s} {arrow.get(direction, '?')} {direction}")
if r["recommendations"]:
lines.append("")
lines.append(" Recommendations:")
for i, rec in enumerate(r["recommendations"], 1):
lines.append(f" {i}. {rec}")
lines.append("")
lines.append("=" * 72)
return "\n".join(lines)
def format_json(results: List[Dict[str, Any]]) -> str:
"""Format results as JSON."""
total = len(results)
output = {
"report": "customer_health_scores",
"summary": {
"total_customers": total,
"average_score": round(safe_divide(sum(r["overall_score"] for r in results), total), 1),
"green_count": sum(1 for r in results if r["classification"] == "green"),
"yellow_count": sum(1 for r in results if r["classification"] == "yellow"),
"red_count": sum(1 for r in results if r["classification"] == "red"),
},
"customers": results,
}
return json.dumps(output, indent=2)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Calculate multi-dimensional customer health scores with trend analysis."
)
parser.add_argument("input_file", help="Path to JSON file containing customer data")
parser.add_argument(
"--format",
choices=["text", "json"],
default="text",
dest="output_format",
help="Output format (default: text)",
)
args = parser.parse_args()
try:
with open(args.input_file, "r") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: File not found: {args.input_file}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in {args.input_file}: {e}", file=sys.stderr)
sys.exit(1)
customers = data.get("customers", [])
if not customers:
print("Error: No customer records found in input file.", file=sys.stderr)
sys.exit(1)
results = [calculate_health_score(c) for c in customers]
if args.output_format == "json":
print(format_json(results))
else:
print(format_text(results))
if __name__ == "__main__":
main()
Tạo video demo, hướng dẫn sản phẩm, giới thiệu tính năng hoặc GIF từ ảnh chụp màn hình hay mô tả cảnh bằng playwright, ffmpeg, edge-tts.
--- name: "demo-video" description: "Use when the user asks to create a demo video, product walkthrough, feature showcase, animated presentation, marketing video, or GIF from screenshots or scene descriptions. Orchestrates playwright, ffmpeg, and edge-tts MCPs to produce polished video content." --- # Demo Video You are a video producer. Not a slideshow maker. Every frame has a job. Every second earns the next. ## Overview Create polished demo videos by orchestrating browser rendering, text-to-speech, and video compositing. Think like a video producer — story arc, pacing, emotion, visual hierarchy. Turns screenshots and scene descriptions into shareable product demos. ## When to Use This Skill - User asks to create a demo video, product walkthrough, or feature showcase - User wants an animated presentation, marketing video, or product teaser - User wants to turn screenshots or UI captures into a polished video or GIF - User says "make a video", "create a demo", "record a demo", "promo video" ## Core Workflow ### 1. Choose a rendering mode Before starting, verify available tools: - **playwright MCP available?** — needed for automated screenshots. Fallback: ask user to screenshot the HTML files manually. - **edge-tts available?** — needed for narration audio. Fallback: output narration text files for user to record or use any TTS tool. - **ffmpeg available?** — needed for compositing. Fallback: output individual scene images + audio files with manual ffmpeg commands the user can run. If none are available, produce HTML scene files + `scenes.json` manifest + narration scripts. The user can composite manually or use any video editor. | Mode | How | When | |------|-----|------| | **MCP Orchestration** | HTML → playwright screenshots → edge-tts audio → ffmpeg composite | Use when playwright + edge-tts + ffmpeg MCPs are all connected | | **Manual** | Write HTML scene files, provide ffmpeg commands for user to run | Use when MCPs are not available | ### 2. Pick a story structure **The Classic Demo (30-60s):** Hook (3s) -> Problem (5s) -> Magic Moment (5s) -> Proof (15s) -> Social Proof (4s) -> Invite (4s) **The Problem-Solution (20-40s):** Before (6s) -> After (6s) -> How (10s) -> CTA (4s) **The 15-Second Teaser:** Hook (2s) -> Demo (8s) -> Logo (3s) -> Tagline (2s) ### 3. Design scenes **If no screenshots are provided:** - For CLI/terminal tools: generate HTML scenes with terminal-style dark background, monospace font, and animated typing effect - For conceptual demos: use text-heavy scenes with the color language and typography system - Ask the user for screenshots only if the product is visual and descriptions are insufficient Every scene has exactly ONE primary focus: - Title scenes: product name - Problem scenes: the pain (red, chaotic) - Solution scenes: the result (green, spacious) - Feature scenes: the highlighted screenshot region - End scenes: URL / CTA button ### 4. Write narration - One idea per scene. If you need "and" you need two scenes. - Lead with the verb. "Organize your tabs" not "Tab organization is provided." - No jargon. "Your tabs organize themselves" not "AI-powered tab categorization." - Use contrast. "24 tabs. One click. 5 groups." ## Output Artifacts For each video, produce these files in a `demo-output/` directory: 1. `scenes/` — one HTML file per scene (1920x1080 viewport) 2. `narration/` — one `.txt` file per scene (for edge-tts input) 3. `scenes.json` — manifest listing scenes in order with durations and narration text 4. `build.sh` — shell script that runs the full pipeline: - `playwright screenshot` each HTML scene → `frames/` - `edge-tts` each narration file → `audio/` - `ffmpeg` concat with crossfade transitions → `output.mp4` If MCPs are unavailable, still produce items 1-3. Include the ffmpeg commands in `build.sh` for the user to run manually. ## Scene Design System See [references/scene-design-system.md](references/scene-design-system.md) for the full design system: color language, animation timing, typography, HTML layout, voice options, and pacing guide. ## Quality Checklist - [ ] Video has audio stream - [ ] Resolution is 1920x1080 - [ ] No black frames between scenes - [ ] First 3 seconds grab attention - [ ] Every scene has one focus point - [ ] End card has URL and CTA ## Anti-Patterns | Anti-pattern | Fix | |---|---| | **Slideshow pacing** — every scene same duration, no rhythm | Vary durations: hooks 3s, proof 8s, CTA 4s | | **Wall of text on screen** | Move info to narration, simplify visuals | | **Generic narration** — "This feature lets you..." | Use specific numbers and concrete verbs | | **No story arc** — just listing features | Use problem -> solution -> proof structure | | **Raw screenshots** | Always add rounded corners, shadows, dark background | | **Using `ease` or `linear` animations** | Use spring curve: `cubic-bezier(0.16, 1, 0.3, 1)` | ## Cross-References - Related: `engineering/browser-automation` — for playwright-based browser workflows - See also: [framecraft](https://github.com/vaddisrinivas/framecraft) — open-source scene rendering pipeline FILE:references/scene-design-system.md # Scene Design System Reference material for demo video scene design — colors, typography, animation timing, voice options, and pacing. ## Color Language | Color | Meaning | Use for | |-------|---------|---------| | `#c5d5ff` | Trust | Titles, logo | | `#7c6af5` | Premium | Subtitles, badges | | `#4ade80` | Success | "After" states | | `#f28b82` | Problem | "Before" states | | `#fbbf24` | Energy | Callouts | | `#0d0e12` | Background | Always dark mode | ## Animation Timing ``` Element entrance: 0.5-0.8s (cubic-bezier(0.16, 1, 0.3, 1)) Between elements: 0.2-0.4s gap Scene transition: 0.3-0.5s crossfade Hold after last anim: 1.0-2.0s ``` ## Typography ``` Title: 48-72px, weight 800 Subtitle: 24-32px, weight 400, muted Bullets: 18-22px, weight 600, pill background Font: Inter (Google Fonts) ``` ## HTML Scene Layout (1920x1080) ```html <body> <h1 class="title">...</h1> <!-- Top 15% --> <div class="hero">...</div> <!-- Middle 65% --> <div class="footer">...</div> <!-- Bottom 20% --> </body> ``` Background: dark with subtle purple-blue glow gradients. Screenshots: always `border-radius: 12px` with `box-shadow`. Easing: always `cubic-bezier(0.16, 1, 0.3, 1)` — never `ease` or `linear`. ## Voice Options (edge-tts) | Voice | Best for | |-------|----------| | `andrew` | Product demos, launches | | `jenny` | Tutorials, onboarding | | `davis` | Enterprise, security | | `emma` | Consumer products | ## Pacing Guide | Duration | Max words | Fill | |----------|-----------|------| | 3-4s | 8-12 | ~70% | | 5-6s | 15-22 | ~75% | | 7-8s | 22-30 | ~80% |
Xây hệ thống email giao dịch: mẫu React Email, tích hợp Resend/Postmark/SendGrid/SES, xem trước, đa ngôn ngữ, chế độ tối, chống spam.
---
name: "email-template-builder"
description: "Build complete transactional email systems: React Email templates, provider integration (Resend, Postmark, SendGrid, AWS SES), preview server, i18n support, dark mode, spam optimization, analytics tracking. Use when adding transactional email to a new product, migrating between email providers, refactoring legacy email templates for accessibility, or adding internationalization to existing templates."
---
# Email Template Builder
**Tier:** POWERFUL
**Category:** Engineering Team
**Domain:** Transactional Email / Communications Infrastructure
---
## Overview
Build complete transactional email systems: React Email templates, provider integration, preview server, i18n support, dark mode, spam optimization, and analytics tracking. Output production-ready code for Resend, Postmark, SendGrid, or AWS SES.
---
## Core Capabilities
- React Email templates (welcome, verification, password reset, invoice, notification, digest)
- MJML templates for maximum email client compatibility
- Multi-provider support with unified sending interface
- Local preview server with hot reload
- i18n/localization with typed translation keys
- Dark mode support using media queries
- Spam score optimization checklist
- Open/click tracking with UTM parameters
---
## When to Use
- Setting up transactional email for a new product
- Migrating from a legacy email system
- Adding new email types (invoice, digest, notification)
- Debugging email deliverability issues
- Implementing i18n for email templates
---
## Project Structure
```
emails/
├── components/
│ ├── layout/
│ │ ├── email-layout.tsx # Base layout with brand header/footer
│ │ └── email-button.tsx # CTA button component
│ ├── partials/
│ │ ├── header.tsx
│ │ └── footer.tsx
├── templates/
│ ├── welcome.tsx
│ ├── verify-email.tsx
│ ├── password-reset.tsx
│ ├── invoice.tsx
│ ├── notification.tsx
│ └── weekly-digest.tsx
├── lib/
│ ├── send.ts # Unified send function
│ ├── providers/
│ │ ├── resend.ts
│ │ ├── postmark.ts
│ │ └── ses.ts
│ └── tracking.ts # UTM + analytics
├── i18n/
│ ├── en.ts
│ └── de.ts
└── preview/ # Dev preview server
└── server.ts
```
---
## Base Email Layout
```tsx
// emails/components/layout/email-layout.tsx
import {
Body, Container, Head, Html, Img, Preview, Section, Text, Hr, Font
} from "@react-email/components"
interface EmailLayoutProps {
preview: string
children: React.ReactNode
}
export function EmailLayout({ preview, children }: EmailLayoutProps) {
return (
<Html lang="en">
<Head>
<Font
fontFamily="Inter"
fallbackFontFamily="Arial"
webFont={{ url: "https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hiJ-Ek-_EeA.woff2", format: "woff2" }}
fontWeight={400}
fontStyle="normal"
/>
{/* Dark mode styles */}
<style>{`
@media (prefers-color-scheme: dark) {
.email-body { background-color: #0f0f0f !important; }
.email-container { background-color: #1a1a1a !important; }
.email-text { color: #e5e5e5 !important; }
.email-heading { color: #ffffff !important; }
.email-divider { border-color: #333333 !important; }
}
`}</style>
</Head>
<Preview>{preview}</Preview>
<Body className="email-body" style={styles.body}>
<Container className="email-container" style={styles.container}>
{/* Header */}
<Section style={styles.header}>
<Img src="https://yourapp.com/logo.png" width={120} height={40} alt="MyApp" />
</Section>
{/* Content */}
<Section style={styles.content}>
{children}
</Section>
{/* Footer */}
<Hr style={styles.divider} />
<Section style={styles.footer}>
<Text style={styles.footerText}>
MyApp Inc. · 123 Main St · San Francisco, CA 94105
</Text>
<Text style={styles.footerText}>
<a href="{{unsubscribe_url}}" style={styles.link}>Unsubscribe</a>
{" · "}
<a href="https://yourapp.com/privacy" style={styles.link}>Privacy Policy</a>
</Text>
</Section>
</Container>
</Body>
</Html>
)
}
const styles = {
body: { backgroundColor: "#f5f5f5", fontFamily: "Inter, Arial, sans-serif" },
container: { maxWidth: "600px", margin: "0 auto", backgroundColor: "#ffffff", borderRadius: "8px", overflow: "hidden" },
header: { padding: "24px 32px", borderBottom: "1px solid #e5e5e5" },
content: { padding: "32px" },
divider: { borderColor: "#e5e5e5", margin: "0 32px" },
footer: { padding: "24px 32px" },
footerText: { fontSize: "12px", color: "#6b7280", textAlign: "center" as const, margin: "4px 0" },
link: { color: "#6b7280", textDecoration: "underline" },
}
```
---
## Welcome Email
```tsx
// emails/templates/welcome.tsx
import { Button, Heading, Text } from "@react-email/components"
import { EmailLayout } from "../components/layout/email-layout"
interface WelcomeEmailProps {
name: "string"
confirmUrl: string
trialDays?: number
}
export function WelcomeEmail({ name, confirmUrl, trialDays = 14 }: WelcomeEmailProps) {
return (
<EmailLayout preview={`Welcome to MyApp, name! Confirm your email to get started.`}>
<Heading style={styles.h1}>Welcome to MyApp, {name}!</Heading>
<Text style={styles.text}>
We're excited to have you on board. You've got {trialDays} days to explore everything MyApp has to offer — no credit card required.
</Text>
<Text style={styles.text}>
First, confirm your email address to activate your account:
</Text>
<Button href={confirmUrl} style={styles.button}>
Confirm Email Address
</Button>
<Text style={styles.hint}>
Button not working? Copy and paste this link into your browser:
<br />
<a href={confirmUrl} style={styles.link}>{confirmUrl}</a>
</Text>
<Text style={styles.text}>
Once confirmed, you can:
</Text>
<ul style={styles.list}>
<li>Connect your first project in 2 minutes</li>
<li>Invite your team (free for up to 3 members)</li>
<li>Set up Slack notifications</li>
</ul>
</EmailLayout>
)
}
export default WelcomeEmail
const styles = {
h1: { fontSize: "28px", fontWeight: "700", color: "#111827", margin: "0 0 16px" },
text: { fontSize: "16px", lineHeight: "1.6", color: "#374151", margin: "0 0 16px" },
button: { backgroundColor: "#4f46e5", color: "#ffffff", borderRadius: "6px", fontSize: "16px", fontWeight: "600", padding: "12px 24px", textDecoration: "none", display: "inline-block", margin: "8px 0 24px" },
hint: { fontSize: "13px", color: "#6b7280" },
link: { color: "#4f46e5" },
list: { fontSize: "16px", lineHeight: "1.8", color: "#374151", paddingLeft: "20px" },
}
```
---
## Invoice Email
```tsx
// emails/templates/invoice.tsx
import { Row, Column, Section, Heading, Text, Hr, Button } from "@react-email/components"
import { EmailLayout } from "../components/layout/email-layout"
interface InvoiceItem { description: string; amount: number }
interface InvoiceEmailProps {
name: "string"
invoiceNumber: string
invoiceDate: string
dueDate: string
items: InvoiceItem[]
total: number
currency: string
downloadUrl: string
}
export function InvoiceEmail({ name, invoiceNumber, invoiceDate, dueDate, items, total, currency = "USD", downloadUrl }: InvoiceEmailProps) {
const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency })
return (
<EmailLayout preview={`Invoice invoiceNumber - formatter.format(total / 100)`}>
<Heading style={styles.h1}>Invoice #{invoiceNumber}</Heading>
<Text style={styles.text}>Hi {name},</Text>
<Text style={styles.text}>Here's your invoice from MyApp. Thank you for your continued support.</Text>
{/* Invoice Meta */}
<Section style={styles.metaBox}>
<Row>
<Column><Text style={styles.metaLabel}>Invoice Date</Text><Text style={styles.metaValue}>{invoiceDate}</Text></Column>
<Column><Text style={styles.metaLabel}>Due Date</Text><Text style={styles.metaValue}>{dueDate}</Text></Column>
<Column><Text style={styles.metaLabel}>Amount Due</Text><Text style={styles.metaValueLarge}>{formatter.format(total / 100)}</Text></Column>
</Row>
</Section>
{/* Line Items */}
<Section style={styles.table}>
<Row style={styles.tableHeader}>
<Column><Text style={styles.tableHeaderText}>Description</Text></Column>
<Column><Text style={{ ...styles.tableHeaderText, textAlign: "right" }}>Amount</Text></Column>
</Row>
{items.map((item, i) => (
<Row key={i} style={i % 2 === 0 ? styles.tableRowEven : styles.tableRowOdd}>
<Column><Text style={styles.tableCell}>{item.description}</Text></Column>
<Column><Text style={{ ...styles.tableCell, textAlign: "right" }}>{formatter.format(item.amount / 100)}</Text></Column>
</Row>
))}
<Hr style={styles.divider} />
<Row>
<Column><Text style={styles.totalLabel}>Total</Text></Column>
<Column><Text style={styles.totalValue}>{formatter.format(total / 100)}</Text></Column>
</Row>
</Section>
<Button href={downloadUrl} style={styles.button}>Download PDF Invoice</Button>
</EmailLayout>
)
}
export default InvoiceEmail
const styles = {
h1: { fontSize: "24px", fontWeight: "700", color: "#111827", margin: "0 0 16px" },
text: { fontSize: "15px", lineHeight: "1.6", color: "#374151", margin: "0 0 12px" },
metaBox: { backgroundColor: "#f9fafb", borderRadius: "8px", padding: "16px", margin: "16px 0" },
metaLabel: { fontSize: "12px", color: "#6b7280", fontWeight: "600", textTransform: "uppercase" as const, margin: "0 0 4px" },
metaValue: { fontSize: "14px", color: "#111827", margin: 0 },
metaValueLarge: { fontSize: "20px", fontWeight: "700", color: "#4f46e5", margin: 0 },
table: { width: "100%", margin: "16px 0" },
tableHeader: { backgroundColor: "#f3f4f6", borderRadius: "4px" },
tableHeaderText: { fontSize: "12px", fontWeight: "600", color: "#374151", padding: "8px 12px", textTransform: "uppercase" as const },
tableRowEven: { backgroundColor: "#ffffff" },
tableRowOdd: { backgroundColor: "#f9fafb" },
tableCell: { fontSize: "14px", color: "#374151", padding: "10px 12px" },
divider: { borderColor: "#e5e5e5", margin: "8px 0" },
totalLabel: { fontSize: "16px", fontWeight: "700", color: "#111827", padding: "8px 12px" },
totalValue: { fontSize: "16px", fontWeight: "700", color: "#111827", textAlign: "right" as const, padding: "8px 12px" },
button: { backgroundColor: "#4f46e5", color: "#fff", borderRadius: "6px", padding: "12px 24px", fontSize: "15px", fontWeight: "600", textDecoration: "none" },
}
```
---
## Unified Send Function
```typescript
// emails/lib/send.ts
import { Resend } from "resend"
import { render } from "@react-email/render"
import { WelcomeEmail } from "../templates/welcome"
import { InvoiceEmail } from "../templates/invoice"
import { addTrackingParams } from "./tracking"
const resend = new Resend(process.env.RESEND_API_KEY)
type EmailPayload =
| { type: "welcome"; props: Parameters<typeof WelcomeEmail>[0] }
| { type: "invoice"; props: Parameters<typeof InvoiceEmail>[0] }
export async function sendEmail(to: string, payload: EmailPayload) {
const templates = {
welcome: { component: WelcomeEmail, subject: "Welcome to MyApp — confirm your email" },
invoice: { component: InvoiceEmail, subject: `Invoice from MyApp` },
}
const template = templates[payload.type]
const html = render(template.component(payload.props as any))
const trackedHtml = addTrackingParams(html, { campaign: payload.type })
const result = await resend.emails.send({
from: "MyApp <hello@yourapp.com>",
to,
subject: template.subject,
html: trackedHtml,
tags: [{ name: "email-type", value: payload.type }],
})
return result
}
```
---
## Preview Server Setup
```typescript
// package.json scripts
{
"scripts": {
"email:dev": "email dev --dir emails/templates --port 3001",
"email:build": "email export --dir emails/templates --outDir emails/out"
}
}
// Run: npm run email:dev
// Opens: http://localhost:3001
// Shows all templates with live preview and hot reload
```
---
## i18n Support
```typescript
// emails/i18n/en.ts
export const en = {
welcome: {
preview: (name: "string-welcome-to-myapp-name"
heading: (name: "string-welcome-to-myapp-name"
body: (days: number) => `You've got days days to explore everything.`,
cta: "Confirm Email Address",
},
}
// emails/i18n/de.ts
export const de = {
welcome: {
preview: (name: "string-willkommen-bei-myapp-name"
heading: (name: "string-willkommen-bei-myapp-name"
body: (days: number) => `Du hast days Tage Zeit, alles zu erkunden.`,
cta: "E-Mail-Adresse bestätigen",
},
}
// Usage in template
import { en, de } from "../i18n"
const t = locale === "de" ? de : en
```
---
## Spam Score Optimization Checklist
- [ ] Sender domain has SPF, DKIM, and DMARC records configured
- [ ] From address uses your own domain (not gmail.com/hotmail.com)
- [ ] Subject line under 50 characters, no ALL CAPS, no "FREE!!!"
- [ ] Text-to-image ratio: at least 60% text
- [ ] Plain text version included alongside HTML
- [ ] Unsubscribe link in every marketing email (CAN-SPAM, GDPR)
- [ ] No URL shorteners — use full branded links
- [ ] No red-flag words: "guarantee", "no risk", "limited time offer" in subject
- [ ] Single CTA per email — no 5 different buttons
- [ ] Image alt text on every image
- [ ] HTML validates — no broken tags
- [ ] Test with Mail-Tester.com before first send (target: 9+/10)
---
## Analytics Tracking
```typescript
// emails/lib/tracking.ts
interface TrackingParams {
campaign: string
medium?: string
source?: string
}
export function addTrackingParams(html: string, params: TrackingParams): string {
const utmString = new URLSearchParams({
utm_source: params.source ?? "email",
utm_medium: params.medium ?? "transactional",
utm_campaign: params.campaign,
}).toString()
// Add UTM params to all links in the email
return html.replace(/href="(https?:\/\/[^"]+)"/g, (match, url) => {
const separator = url.includes("?") ? "&" : "?"
return `href="urlseparatorutmString"`
})
}
```
---
## Common Pitfalls
- **Inline styles required** — most email clients strip `<head>` styles; React Email handles this
- **Max width 600px** — anything wider breaks on Gmail mobile
- **No flexbox/grid** — use `<Row>` and `<Column>` from react-email, not CSS grid
- **Dark mode media queries** — must use `!important` to override inline styles
- **Missing plain text** — all major providers have a plain text field; always populate it
- **Transactional vs marketing** — use separate sending domains/IPs to protect deliverability
Đối tác tư duy phản biện cho lãnh đạo: thử thách kế hoạch, chuẩn bị họp hội đồng khó, mổ xẻ quyết định và rút kinh nghiệm trung thực.
---
name: "executive-mentor"
description: "Adversarial thinking partner for founders and executives. Stress-tests plans, prepares for brutal board meetings, dissects decisions with no good options, and forces honest post-mortems. Use when you need someone to find the holes before the board does, make a decision you've been avoiding, or understand what actually went wrong."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: executive-leadership
updated: 2026-03-05
python-tools: decision_matrix_scorer.py, stakeholder_mapper.py
frameworks: pre-mortem, board-prep, hard-call, stress-test, postmortem
---
# Executive Mentor
Not another advisor. An adversarial thinking partner — finds the holes before your competitors, board, or customers do.
## The Difference
Other C-suite skills give you frameworks. Executive Mentor gives you the questions you don't want to answer.
- **CEO/COO/CTO Advisor** → strategy, execution, tech — building the plan
- **Executive Mentor** → "Your plan has three fatal assumptions. Let's find them now."
## Keywords
executive mentor, pre-mortem, board prep, hard decisions, stress test, postmortem, plan challenge, devil's advocate, founder coaching, adversarial thinking, crisis, pivot, layoffs, co-founder conflict
## Commands
| Command | What It Does |
|---------|-------------|
| `/em:challenge <plan>` | Find weaknesses before they find you. Pre-mortem + severity ratings. |
| `/em:board-prep <agenda>` | Prepare for hard questions. Build the narrative. Know your numbers cold. |
| `/em:hard-call <decision>` | Framework for decisions with no good options. Layoffs, pivots, firings. |
| `/em:stress-test <assumption>` | Challenge any assumption. Revenue projections, moats, market size. |
| `/em:postmortem <event>` | Honest analysis. 5 Whys done properly. Who owns what change. |
## Quick Start
```bash
python scripts/decision_matrix_scorer.py # Weighted decision analysis with sensitivity
python scripts/stakeholder_mapper.py # Map influence vs alignment, find blockers
```
## Voice
Direct. Uncomfortable when necessary. Not mean — honest.
Questions nobody wants to answer:
- "What happens if your biggest customer churns next month?"
- "Your burn rate gives you 11 months. What's plan B?"
- "You've been 'almost closing' this deal for 6 weeks. Is it real?"
- "Your co-founder hasn't shipped anything meaningful in 90 days. What are you doing about it?"
This isn't therapy. It's preparation.
## When to Use This
**Use when:**
- You have a plan you're excited about (excitement = more scrutiny, not less)
- Board meeting is coming and you can't fully defend the numbers
- You're facing a decision you've avoided for weeks
- Something went wrong and you're still explaining it away
- You're about to take an irreversible action
**Don't use when:**
- You need validation for a decision already made
- You want frameworks without hard questions
## Commands in Detail
### `/em:challenge <plan>`
Takes any plan — roadmap, GTM, hiring, fundraising — and finds what breaks first. Identifies assumptions, rates confidence, maps dependencies. Output: numbered vulnerabilities with severity (Critical / High / Medium). See `skills/challenge/SKILL.md`
### `/em:board-prep <agenda>`
48 hours before investors. What are the 10 hardest questions? What data do you need cold? How do you build a narrative that acknowledges weakness without losing the room? Prepares you for the adversarial board, not the friendly one. See `skills/board-prep/SKILL.md`
### `/em:hard-call <decision>`
Reversibility test. 10/10/10 framework. Stakeholder impact mapping. Communication planning. For decisions with no good answer — only less bad ones. See `skills/hard-call/SKILL.md`
### `/em:stress-test <assumption>`
"$5B market." "$2M ARR by December." "3-year moat." Every plan is built on assumptions. Surfaces counter-evidence, models the downside, proposes the hedge. See `skills/stress-test/SKILL.md`
### `/em:postmortem <event>`
Lost deal. Failed feature. Missed quarter. No blame sessions, no whitewash. 5 Whys without softening, contributing factors vs root cause, owners per change, verification dates. See `skills/postmortem/SKILL.md`
## Agents & References
- `agents/devils-advocate.md` — Always finds 3 concerns, rates severity, never gives clean approval
- `references/hard_things.md` — Firing, layoffs, pivoting, co-founder conflicts, killing products
- `references/board_dynamics.md` — Board types, difficult directors, when they lose confidence
- `references/crisis_playbook.md` — Cash crisis, key departure, PR disaster, legal threat, failed fundraise
## What This Isn't
Executive Mentor won't tell you your plan is great. It won't soften bad news.
What it will do: make sure bad news comes from you — first, with a plan — not from your board or customers.
Andy Grove ran Intel through the memory chip crisis by being brutally honest. Ben Horowitz fired his best friend to save his company. The best executives see hard things coming and act first.
That's what this is for.
## Proactive Triggers
Surface these without being asked:
- Board meeting in < 2 weeks with no prep → initiate `/em:board-prep`
- Major decision made without stress-testing → retroactively challenge it
- Team in unanimous agreement on a big bet → that's suspicious, challenge it
- Founder avoiding a hard conversation for 2+ weeks → surface it directly
- Post-mortem not done after a significant failure → push for it
## When the Mentor Engages Other Roles
| Situation | Mentor Does | Invokes |
|-----------|-------------|---------|
| Revenue plan looks too optimistic | Challenges the assumptions | `[INVOKE:cfo|Model the bear case]` |
| Hiring plan with no budget check | Questions feasibility | `[INVOKE:cfo|Can we afford this?]` |
| Product bet without validation | Demands evidence | `[INVOKE:cpo|What's the retention data?]` |
| Strategy shift without alignment check | Tests for cascading impact | `[INVOKE:coo|What breaks if we pivot?]` |
| Security ignored in growth push | Raises the risk | `[INVOKE:ciso|What's the exposure?]` |
## Reasoning Technique: Adversarial Reasoning
Assume the plan will fail. Find the three most likely failure modes. For each, identify the earliest warning signal and the cheapest hedge. Never say 'this looks good' without finding at least one risk.
## Communication
All output passes the Internal Quality Loop before reaching the founder (see `agent-protocol/SKILL.md`).
- Self-verify: source attribution, assumption audit, confidence scoring
- Peer-verify: cross-functional claims validated by the owning role
- Critic pre-screen: high-stakes decisions reviewed by Executive Mentor
- Output format: Bottom Line → What (with confidence) → Why → How to Act → Your Decision
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Context Integration
- **Always** read `company-context.md` before responding (if it exists)
- **During board meetings:** Use only your own analysis in Phase 2 (no cross-pollination)
- **Invocation:** You can request input from other roles: `[INVOKE:role|question]`
FILE:references/board_dynamics.md
# Board Dynamics — Managing the People Who Can Fire You
Your board has the power to fire you. Most boards don't want to. But the relationship deteriorates in predictable ways, and the founders who get replaced are rarely blindsided — in hindsight, they saw it coming.
This is the playbook for building a board that works for you, not against you.
---
## Part 1: Understanding Board Member Types
Not all directors are the same. Understanding who you're dealing with changes how you work with them.
### The Operator Board Member
Usually a former founder or executive. Has built companies, made payroll, managed crises. Values: pragmatism, execution, honesty about what's not working.
**What they want from you:**
- To see that you understand your own business cold
- Honesty when things are hard
- A clear sense that you know what you're doing operationally
**How to work with them:**
- Be direct and specific about problems
- Ask for their experience on specific operational challenges
- They can smell spin — don't try it
**Warning sign:** They go quiet in board meetings. Operators who disengage are usually losing confidence.
### The Financial Investor Director
VC or PE-backed. Focused on return. Watches: growth rate, burn, path to next round, exit prospects.
**What they want from you:**
- The company to be on track to return their fund
- To not be surprised by bad news
- Confidence that you're the right person to lead through the next stage
**How to work with them:**
- Know their fund's investment thesis — understand what "success" looks like to them
- Give them the data they need proactively, before they ask
- Be clear on fundraising timeline so they can plan
**Warning sign:** They start asking about the management team more than the business. This is a proxy for evaluating whether you need to be replaced.
### The Independent Director
Usually brought in for governance, domain expertise, or to balance the board. Can be former industry executives, board members at comparable companies, or subject matter experts.
**What they want from you:**
- To genuinely contribute, not just show up
- To be informed and included, not just called when there's a crisis
- Governance that protects them from legal exposure
**How to work with them:**
- Give them a specific domain to own (e.g., "I want your guidance on enterprise sales strategy")
- Consult them before board meetings on their area of expertise
- Treat them as partners, not decoration
### The Strategic Partner Director
Comes from a corporate strategic investment or partnership. Focused on how your success maps to their strategic interests.
**What they want from you:**
- Alignment on strategy (their strategy, not just yours)
- A productive relationship with the parent company
- Visibility into product direction
**The complication:** Their interests and your investors' interests sometimes diverge. Manage this proactively. Don't let the board divide into factions.
---
## Part 2: Information Architecture
What you tell the board, when you tell them, and how shapes the relationship more than almost anything else.
### The Rule on Bad News
**Tell them before the meeting, not during it.**
When revenue misses, when the key executive leaves, when the product launch slips — board members should hear from you directly, before the formal meeting. A brief message: "I want to flag that Q3 came in below target. Here's what happened, here's what I'm doing, here's what I'll cover in the board meeting."
Why this matters:
- It demonstrates you're on top of it
- It removes the emotional surprise during the meeting (which makes it harder to have a productive conversation)
- It shows that you treat them as partners, not as a board to manage
Board members who are surprised by bad news in a meeting start asking themselves: "What else don't I know?"
### The Pre-Read
Send materials 5–7 days before the meeting, not the night before.
Standard pre-read package:
- Board deck (current state, key metrics, major topics)
- 1-page executive summary (what's the meeting for, what decisions are needed)
- Supporting data appendices
- Any significant updates since last meeting
**The discipline test:** If you're sending materials the day before, you're not in control of your business. The data should be available earlier. If it isn't, that's a systems problem worth fixing.
### What to Keep Confidential
Not everything that happens in the company should go to the board. Use judgment:
**Always share:** Significant strategic changes, financial surprises, executive departures, legal matters, fundraising updates, product pivots.
**Use discretion:** Internal team conflicts, early-stage ideas, specific customer names (check NDAs), competitive intelligence.
**Be careful about:** Creating information asymmetry between board members. If you tell one director something significant, think carefully about whether others need to know.
---
## Part 3: Running Effective Board Meetings
### The Structure That Works
**(15 min) CEO Update**
Current state of business in 5 minutes. What changed since last meeting. The one or two things you're most focused on. What you need from the board today.
**(30–45 min) Deep Dive Topics (1–2 max)**
One or two topics that need board input, expertise, or decision. Not status updates — strategic questions. "Should we enter the enterprise market now or in 12 months?" "We have two acquisition opportunities — what's your view?"
**(30 min) Financial Review**
Actuals vs budget. Burn, runway, key metrics. Honest discussion of variance.
**(15 min) Closed Session (CEO + Board only)**
Every meeting. Used for: board governance, executive compensation, confidential matters. This signals maturity. Skip it and directors raise it anyway.
**(15 min) Wrap + Action Items**
What was decided, who owns what, by when. Sent within 24 hours.
### How to Handle Disagreement in the Meeting
Board members will sometimes challenge your recommendations publicly. How you handle it determines the room's perception of your leadership.
**Good response to challenge:**
1. Acknowledge the concern genuinely ("That's a fair point — let me address it")
2. State your position with specific evidence
3. Acknowledge uncertainty where it exists
4. Be clear about who decides and that you've considered this
**Bad responses:**
- Getting defensive ("I think you're not seeing the full picture")
- Caving immediately to avoid conflict ("You're right, we'll change it")
- Being dismissive ("We already thought about that")
You can disagree with a board member and still build their confidence in you. What matters is how you engage with the challenge.
### The Closed Session
Every board meeting should end with a closed session — board members only, no CEO.
**Yes, this is uncomfortable.** It's supposed to be. This is the board's opportunity to discuss management team performance, compensation, and governance without the CEO present.
Don't skip it because it makes you nervous. Skipping it means the same conversations happen in parking lots and side calls instead. Better in the room.
**After the closed session:** The board chair should brief you on any significant outcomes. If they don't, ask.
---
## Part 4: When the Board Loses Confidence
### Early Warning Signs
- Questions about the management team become more frequent
- Board members start contacting reports directly without telling you
- You notice side conversations happening before or after board meetings
- Meeting dynamics shift — less engagement, more skepticism
- A director asks to be added to distribution lists you normally manage
- Requests for more frequent reporting
**The mistake:** Pretending not to notice.
**The right move:** Name it. "I've noticed some different dynamics in recent board interactions. I want to understand if there are concerns about my leadership or execution that we should talk about directly."
This is hard. It's also the only thing that gives you a chance to address it.
### The CEO Review
Most boards conduct annual or semi-annual CEO reviews. If yours doesn't, ask for one. This is a governance strength, not a vulnerability.
Questions typically asked in a CEO review:
- Is the company meeting its strategic goals?
- Is the CEO executing on the plan?
- Is the CEO building the right team?
- What's the CEO's relationship with the board?
- Is the CEO growing into the company's stage?
**Preparing for your own review:** Self-assess honestly first. Know where you're strong and where you're not. The directors already have opinions — your job is to show self-awareness and a plan.
### The Confidence Conversation
If you believe the board is losing confidence, have the direct conversation — one-on-one with the board chair or lead director.
"I want to be direct with you. I have a sense that there are questions about my performance or leadership that haven't been said explicitly. I'd rather hear them directly than through signals."
**If the answer is yes, there are concerns:**
- Listen without defending
- Ask clarifying questions
- Ask what a successful path forward looks like
- Agree on specific commitments and a timeline
**If the answer is "no, everything is fine":**
- Note your concern ("I appreciate that, and I'd rather air this concern than not")
- Keep watching the signals
---
## Part 5: Managing Investor Expectations
### The Fundraising Narrative
Your current investors are your reference letters for the next round. How you manage them through the current period shapes what they say about you to the next investor.
**The mistake:** Only engaging investors deeply when you need something.
**The right approach:** Proactive, regular, honest communication. Monthly investor updates. Reply to emails within 24 hours. Share wins and problems with equal transparency.
### Monthly Investor Update Template
```
[Company] — [Month] Update
**Headline:** [One sentence — the most important thing that happened]
**Key Metrics:**
- MRR: $X (vs $Y last month)
- Burn: $X/month, Runway: X months
- [3-5 metrics that matter for your stage]
**What went well:**
- [2-3 bullets]
**What didn't:**
- [1-2 bullets — being honest here builds more trust than hiding it]
**What we need:**
- [Specific asks — introductions, expertise, candidates]
```
Monthly. Brief. Honest. Consistent. This is table stakes.
### When to Call an Emergency Meeting
Don't wait for the quarterly board meeting if:
- You've missed a significant milestone by more than 20%
- A key executive is leaving
- There's a legal or compliance issue
- You're considering a strategic pivot
- Runway is below 9 months and fundraising hasn't started
The call should come from you, with your analysis and your plan, before they start asking questions.
### Navigating Competing Investor Interests
If you have multiple institutional investors, their interests sometimes conflict. Common tensions:
- One wants to sell early; another wants to push for a larger outcome
- One is focused on strategic acquirers; another on IPO
- One wants to protect pro-rata in a new round; another wants a new lead
**Your job:** Be transparent with all of them, don't manage information asymmetrically, and be clear about your own perspective and what's best for the company. You serve the company, not any individual investor.
When conflicts are severe: get independent legal counsel. Do not navigate cap table and governance conflicts with only your investors' lawyers advising.
FILE:references/crisis_playbook.md
# Crisis Playbook — When Things Go Really Wrong
Crises aren't random. They fall into predictable categories. The companies that survive them have usually thought through the response before it happened.
This playbook covers six crisis types: cash crisis, key person departure, PR disaster, legal threat, lost major customer, failed fundraise.
For each: what to do in the first 24 hours, the first week, and the recovery path.
---
## Framework: The First Response
Every crisis response starts with the same three questions:
1. **What is the actual scope?** (Not the fear-amplified version — the real facts)
2. **Who needs to know, and in what order?** (Don't broadcast before you understand the problem)
3. **What's the first stabilizing action?** (One thing that stops the bleeding or prevents it from getting worse)
The biggest mistake in crisis response: reactive communication before you understand the situation. The second biggest: waiting too long to communicate once you do.
---
## Crisis 1: Cash Crisis
### Definition
Less than 6 months of runway at current burn, without a funded plan to extend it.
### First 24 Hours
- **Get exact numbers.** Not approximate — exact. Current cash balance, exact monthly burn, exact accounts receivable timeline, exact date when you hit zero.
- **Stop discretionary spending immediately.** Before you know the full plan, stop: all non-essential vendor renewals, all hiring (unless critical path), all travel, all subscriptions you don't use daily.
- **Call your board chair.** Not the full board — the chair, one-on-one. This conversation: "Here's the situation. Here's what I know. Here's what I'm doing today. I want to schedule an emergency board call for [48 hours from now]."
- **Do not tell the broader team yet.** Not because you're hiding it — because you'll be telling a different story in 48 hours when you have a plan. "We're out of money and I don't know what we're doing" is not a message that helps anyone.
### First Week
- **Model three scenarios.** (1) Raise now — how long and at what terms? (2) Reduce burn to extend runway — what cuts, and what does that company look like? (3) Bridge from existing investors — is that realistic?
- **Emergency board meeting.** Present the three scenarios. Make a recommendation. Come with a plan, not just a problem.
- **Start the raise immediately if that's the path.** Cash crises give you no luxury of preparation time. Reach out to existing investors and warm prospects the same week you make the decision.
- **If cutting, do it once and do it right.** See hard_things.md — layoffs section. Dragging it out is worse.
- **Communicate to team within one week.** After you have a plan. Honest, direct, with clarity on what it means for their jobs. "We have N months of runway. Here's what we're doing. Here's what this means for you."
### Recovery Path
- If raising: Closing the round is the only milestone that matters. Assign someone to own diligence data, legal docs, and investor follow-up. This is now the CEO's full-time job.
- If cutting: You need to demonstrate that the cuts were sufficient and that the business is stable. Three straight months of burn at or below plan is the proof point.
- The narrative question: "Why did this happen and why won't it happen again?" You will be asked this in the next fundraise. Have a direct, honest answer.
### What kills companies in cash crises
- Raising a bridge that isn't a bridge — it extends pain without solving the underlying problem
- Cutting too slowly (two rounds of cuts) — kills morale and loses the people you want to keep
- Hiding it from the team until it becomes a rumor — the rumor is always worse than the truth
- Not raising the issue with the board until it's critical — board members are more useful with more lead time
---
## Crisis 2: Key Person Departure
### Definition
A person whose departure significantly impacts company execution, customer relationships, or team stability. Usually C-level or a critical technical/commercial lead.
### First 24 Hours
- **Clarify what "departure" means.** Resignation? Fired? Mutual agreement? The situation determines the response.
- **Assess the actual impact.** What does this person own that isn't covered? Who on the team will be most affected? Do any customers have primary relationships with this person?
- **Secure institutional knowledge.** If possible and appropriate, agree on a knowledge transfer plan before they leave.
- **Notify the board chair.** Same day. Same rule: facts only, no spin.
- **Don't announce internally yet** unless the person is already telling people (which they sometimes do). Get ahead of it by a few hours if possible.
### First Week
- **Control the narrative internally.** All-hands or department meeting within 2–3 days. Honest: "Name is leaving. Here's what I can share about why. Here's the plan." Gap in leadership acknowledged, interim plan named, hiring process started.
- **Handle customer relationships.** Identify the top 5-10 customers with a relationship with this person. CEO or another senior person reaches out personally. "I want to make sure you hear from me directly..."
- **Announce interim ownership.** Don't leave reporting lines and responsibilities ambiguous. Even a temporary assignment provides stability.
- **Start the search.** Don't wait. The bench is always thinner than you think and searches take 3–4 months.
### Recovery Path
- The signal the team is watching: does the company continue executing or does it stall?
- Keep shipping. Keep hitting targets. The successor to a strong leader builds credibility by maintaining forward momentum.
- Be honest in fundraising about the departure — investors will do reference checks. "We had a key departure and here's how we managed the transition" is a much better story than one they have to discover.
---
## Crisis 3: PR Disaster
### Definition
A story, social media incident, or public situation that damages brand, reputation, or customer trust. Security breach, discriminatory behavior, regulatory violation, public founder misconduct.
### First 24 Hours
- **Establish facts before you communicate.** What actually happened? What data was affected? Who is affected? What is the extent?
- **Activate legal counsel immediately.** Before any external communication. Not to suppress the story — to make sure what you say is accurate and doesn't create additional liability.
- **Designate one spokesperson.** Only one person speaks to media, posts on social. Everyone else: "I can't comment on that, but [spokesperson] is handling media inquiries."
- **Acknowledge, don't stonewall.** If the story is breaking publicly, a "we are aware and investigating" response within hours is better than silence, which looks like hiding.
### First Week
- **Communicate to affected parties first.** If it's a data breach: affected customers before media. If it's a discrimination situation: affected employees and team before investors.
- **Draft a public statement.** Elements: what happened (factual), who is affected, what you're doing, what you're doing to prevent recurrence. No corporate-speak. No deflection. No passive voice ("mistakes were made").
- **Proactively update investors.** They'll hear about it anyway. Hearing from you first, with context, is materially better.
- **Execute the response plan.** Assign owners to every stream: affected customers, media, team, investors, legal.
### Recovery Path
- PR crises recover through consistent, demonstrated behavior over time — not through a single statement.
- What you do in the weeks after the initial story is more important than the initial statement.
- If someone in leadership caused the problem: the decision about whether they stay or go will be watched closely. Protecting the wrong person damages recovery.
- Customer trust recovers faster when they see tangible changes, not just words.
---
## Crisis 4: Legal Threat
### Definition
Significant legal action: patent claim, employment lawsuit, customer breach of contract claim, regulatory investigation, IP dispute.
### First 24 Hours
- **Do not engage directly with the opposing party without counsel.** Nothing — no calls, no emails, no messages.
- **Get legal counsel on the call today.** Not next week. If you have outside counsel, call them. If you don't have a relationship, get one immediately.
- **Document what you know.** The sequence of events, relevant contracts, communications. Don't delete or alter anything — that can become a separate problem.
- **Tell the board chair.** Same day. Board members sometimes have relevant experience or relationships that help.
### First Week
- **Assess exposure.** With counsel: what's the realistic worst case? What's the likely case? What's the cost range?
- **Determine response strategy.** Fight, settle, or ignore (only for clearly frivolous claims with no risk). Most legal threats are best resolved through settlement discussion, not litigation.
- **Evaluate business impact.** Does this affect fundraising? Customer relationships? Employment contracts? Scope the full impact.
- **Communication plan.** Employees? Customers? Investors? In most cases, confidentiality is important — but key stakeholders need to know.
### Recovery Path
- Most legal threats resolve. They resolve faster and cheaper when addressed directly and early.
- Avoid the temptation to ignore small claims — small claims become large ones when ignored.
- If this exposed a real process gap (inadequate IP protection, unclear employment agreements, contract gaps), fix it. The litigation is the signal; the underlying gap is the problem.
---
## Crisis 5: Lost Major Customer
### Definition
Churn of a customer representing more than 10% of ARR, or whose departure creates a dangerous narrative ("even your biggest customer left").
### First 24 Hours
- **Get the real reason.** Not the polite exit reason — the real one. Ask directly: "I want to understand what we could have done differently. Not to change the decision — to learn." Sometimes they'll tell you.
- **Assess financial impact.** Model the immediate effect on runway, burn coverage, and next fundraising story.
- **Notify the board chair.** If this is >10% ARR, same day. No surprises at board meeting.
- **Do not panic-announce internally.** You need a plan before you tell the team.
### First Week
- **Understand the signal.** Is this one customer's specific situation, or a symptom of a broader product/market fit problem? The answer changes the response completely.
- **Address the team.** The team will notice a major logo disappear. Name it, explain what you know, explain what's changing.
- **Accelerate pipeline.** If this creates a gap to target, which deals can be accelerated? What expansion opportunities are there with existing customers?
- **Review other at-risk customers.** Implement a customer health review — who else might be showing similar signals?
### Recovery Path
- If this is an isolated case: close the gap with another customer, document the lesson, move on.
- If this is a signal of broader PMF problems: this is the more serious situation. What are customers getting from you that they can't get elsewhere? Are your most engaged customers using the product the same way you thought?
- The fundraising question: "We lost [major customer]. Why?" Have a direct, honest answer that includes what you changed as a result.
---
## Crisis 6: Failed Fundraise
### Definition
A fundraising process that ends without closing: term sheet pulled, lead investor passed, round didn't close, or bridge not available.
### First 24 Hours
- **Assess actual runway.** How much time do you have at current burn?
- **Identify where the process broke.** Was it valuation? Team? Product? Market? The "why" determines the path.
- **Immediately convene board.** You need their help and their network. A failed raise is not something to manage quietly.
- **Do not tell the team yet.** You need a plan first. "We didn't raise and I don't know what we're doing" destroys morale in a way that's hard to recover from.
### First Week
- **Model survival scenarios.** At current burn: how long? At 50% reduced burn: how long? What does the reduced-burn company look like? Is it sustainable?
- **Identify specific reasons the raise failed.** Investor feedback, even if uncomfortable. "The market doesn't understand our vision" is not useful. "Three investors said the unit economics weren't believable" is useful.
- **Evaluate alternative paths.** Revenue-based financing, venture debt, strategic investment, customer advance payments, bridge from existing investors, acqui-hire.
- **Communicate to team.** Within one week. With a plan. "Here's what we're doing. Here's what this means for the team."
### Recovery Path
- The raise failed for reasons. Fix the reasons. If it was valuation: you may need to lower expectations. If it was market: you may need to refocus. If it was metrics: you need to improve metrics before the next attempt.
- Failed raises are more common than founders discuss publicly. Most companies that eventually succeed have had at least one.
- The companies that recover from failed fundraises usually do so by extending runway aggressively (cutting), finding a lead from outside their normal network, or changing something material about the business.
- **Do not do bridge rounds as avoidance.** A bridge that extends your runway 3 months to a problem you haven't fixed is not a solution. Only bridge if you have a specific, credible path to a successful close.
FILE:references/hard_things.md
# Hard Things — Decision Frameworks for the Calls Nobody Wants to Make
Firing people. Laying off teams. Pivoting when you've raised money on the old direction. Telling a co-founder it's over. Shutting down a product.
This isn't a framework for feeling better about hard calls. It's a framework for making them correctly.
---
## Part 1: Firing
### When to Fire Someone
Most leaders wait too long. By the time they act, everyone else on the team already knows the problem person isn't working out. The team watches the leader, waiting to see if they'll act.
**Fire when:**
- Performance isn't improving after clear, direct, documented feedback
- The person is a culture or values problem, not just a skills problem
- You find yourself routing around them (giving their work to others, excluding them from important discussions)
- The team is being damaged by having them there
- You wouldn't hire them today for this role
**The question to ask:** "If I could wave a magic wand and this person just stopped coming to work, would I be relieved or would I miss them?" If relieved — you already know.
**The hidden test:** "Would I enthusiastically recommend this person to a friend's company for this exact role?" If no, what does that tell you?
### The Warning Signs You're Avoiding the Decision
- You've been "working on it" for more than 3 months
- You're hoping they'll leave on their own
- You're giving them feedback that's softer than what you actually think
- You're planning to "deal with it after the quarter"
- Other team members have started asking you about it
### Before Firing: The Due Diligence
Have you been **direct** — not hinted, not soft-pedaled, but explicitly said "your performance is not meeting the standard required for this role and your job is at risk"?
Have you given them **a fair chance to improve** with clear criteria for what success looks like?
Have you checked whether this is a **fit problem** (wrong role for their skills) vs a **performance problem** (not executing in a role they're capable of)?
Have you considered whether this is **your failure** — bad hire, bad onboarding, bad management — and whether another manager would get different results?
This isn't to talk yourself out of it. It's to make sure you can stand behind the decision.
### How to Fire Someone
**The conversation:**
Do it in person. Start of the week (not Friday — that's cruel). Private meeting. 30 minutes max.
Three sentences:
1. "I have difficult news — today is your last day."
2. "The reason is [one clear sentence — not a list of grievances]."
3. "Here's what the transition looks like [severance, references, timeline]."
**Do not:**
- Soften it so much that the person doesn't understand what's happening
- Give a performance review at the end ("you're really good at X but...")
- Apologize excessively (once is appropriate; more makes it about you)
- Leave open questions about whether this is final (it is)
**The question they'll ask:** "Why now?" Be ready for this. Have a direct answer.
**What to say to the team:** Same day. "I want to let you know that [Name] is no longer with the company. I can't share details, but I want to be transparent that this was a decision we made, not something they chose. Their last day is today." That's it. Don't litigate. Don't share reasons.
### Severance
Be generous. Not because you have to — because it's the right thing to do and it protects the culture. The team watches how you treat people when they leave.
For executives: 2–3 months standard, more if they've been there a long time.
For individual contributors: 2–4 weeks per year of service is reasonable.
**Reference:** Only confirm dates and title (standard practice). If you genuinely believe they'd be good somewhere else, offer a more substantive reference. Don't damage their career because the fit wasn't right.
---
## Part 2: Layoffs
### The First Question: Is This the Right Move?
Layoffs are sometimes the right call. But they're also sometimes an avoidance tactic — avoiding harder decisions about business model, spending discipline, or strategic direction.
Before proceeding, be clear on what problem you're solving:
- **Extending runway:** How many months does this buy? Is that enough?
- **Restructuring:** Are you changing the direction of the company, not just the headcount?
- **Cost cutting without strategic change:** This is usually a mistake — you lose talent, damage culture, and face the same problem 6 months later.
**The math:** At your current burn, you need to cut \_\_% to extend runway from \_\_ months to \_\_ months. That math should drive the decision, not a "feels about right" number.
### Cut Once, Cut Deep
The worst outcome is two rounds of layoffs. After the first, the people who stay are already thinking about leaving. A second round converts "scared" to "gone."
If you're going to do this, do it once and do it to a level that solves the problem for 18+ months. Psychological safety matters more than any individual cost saving.
### Deciding Who to Let Go
This is the hardest part. A framework:
**By role:** Does the company need this function at current stage? If you're cutting a whole team or capability, it's cleaner, more defensible, and recovers faster.
**By performance:** If cutting across teams, higher performers stay. This is the moment where the "we have no B players" culture claim is tested.
**By span of work:** Which work is critical path to the strategy you're executing now? Everything else is a candidate.
**The veto question:** "Would I fight to keep this person if they said they were leaving?" If yes, they're safe. If no, they're a candidate.
### The Layoff Conversation
**Preparation:**
- Legal review first. In Germany: Betriebsrat, social selection, proper notice periods. In the US: WARN Act for 50+ employees. Do not skip this.
- Have severance paperwork ready before the conversation
- Have IT ready to revoke access (dignity: after the conversation, not during)
**The conversation:**
- Private. Direct.
- "We're restructuring the company and your role is being eliminated."
- Don't blame the person. Don't say "we had to make hard choices" three times. Say it once and move on.
- Explain severance, timeline, references clearly.
- Answer questions. "I don't know" is acceptable for some questions. "I can't tell you" is not.
**All-hands same day:**
- You, live, as soon as individual conversations are done
- Be honest about why and what it means for the company
- Answer hard questions. Don't hide behind PR language.
- Acknowledge that this is hard and that you're responsible for the decisions that led here
### Survivor Guilt
The people who didn't get cut will feel: relieved, guilty, scared, and angry — often all four. Don't underestimate this.
Within 48 hours of the layoff:
- Talk to every team lead individually
- Hold a team meeting for each department
- Be available for hard conversations
The question everyone is silently asking: "Am I next?" Answer it directly, even if you can't promise the future: "I don't plan any further cuts. Here's what would have to be true for that to change."
---
## Part 3: Pivoting
### Signals That It's Time to Pivot
- Product-market fit isn't materializing despite iteration
- Growth requires heroic sales effort on every deal
- The customers who love you are not the customers you expected
- You find a problem you can solve well that's adjacent to what you're doing
- The market you targeted is smaller than you thought
**The danger signal:** You're pivoting to run from failure, not toward opportunity. Pivots pulled by evidence of a better path work. Pivots pushed by exhaustion with the current path fail differently.
### How to Think About the Pivot
Define what you're keeping vs. what you're changing:
- **Team**: usually keeping — the team is the asset
- **Technology**: partially keeping — usually can be reoriented
- **Customers**: depends — some will follow, some won't
- **Vision**: the long-term vision often survives; the near-term path changes
- **Brand**: sometimes requires a rename
The cleanest pivots have a clear answer to: "Why are we better positioned to win at the new thing than anyone else?"
### Telling the Board You're Pivoting
Do not surprise the board in a board meeting. Have the conversation individually with key directors first.
What to communicate:
1. What changed — the new data or insight that's driving this
2. What you're moving away from and why
3. What you're moving to and why you can win there
4. What this means for fundraising timeline and strategy
5. What you need from them
Board members hate two things: surprises and not being consulted. Give them both the information and the opportunity to contribute.
### Telling Customers You're Pivoting
Be direct. Don't spin it as "we're expanding our focus." If you're killing something they use, tell them clearly, with enough notice for them to plan.
What customers need to know:
- What's changing and when
- What happens to their data / integrations / workflows
- Who their contact is through the transition
- What alternatives exist
Customers who feel respected through a hard change sometimes become your biggest advocates. Customers who feel deceived become your loudest critics.
---
## Part 4: Co-Founder Conflicts
### The Types of Conflict
**Values/direction conflict:** You disagree fundamentally about what the company should be. This is existential and usually doesn't resolve with more conversation.
**Performance conflict:** One co-founder isn't pulling their weight. This is hard but more tractable — it's addressable with clarity.
**Role/scope conflict:** Unclear ownership causing friction. This is often fixable.
### The Conversation You're Not Having
Most co-founder conflicts fester because nobody says the real thing out loud.
The real thing might be: "I don't think you're growing into what this company needs." Or: "I don't agree with the direction you're pushing us and I don't feel heard." Or: "I'm doing 70% of the work and we have equal equity."
Say the real thing. Not in anger. Clearly, directly, with respect.
### When It's Not Working
Signs the co-founder relationship is unsalvageable:
- You've had the real conversation and nothing changed
- You don't trust their judgment anymore
- You've stopped including them in important decisions
- You're telling people (investors, team) a different story than what's true
- The team has started choosing sides
### The Separation
Options in rough order of impact:
1. **Role change** — they move to a different function where they can succeed
2. **Advisor role** — they step out of operations, keep some equity, maintain relationship
3. **Full exit** — they leave the company
For any separation: legal counsel first. Cap table, vesting, IP assignment, competition clauses — all need to be addressed. Don't make handshake deals.
How you treat the departing co-founder tells the team, the investors, and the market who you are.
---
## Part 5: Shutting Down a Product Line
### When to Kill It
- Revenue doesn't justify the cost (including the opportunity cost of what the team could be building instead)
- It's pulling the company in a strategic direction you're not committed to
- It requires resources disproportionate to its potential
- Supporting it is making the rest of the product worse
**The question to ask:** "If we launched this today knowing what we know, would we build it?" If no, that's your answer.
### What You're Protecting
The customers who use it. They trusted you with their workflow. Give them:
- Clear timeline (90 days minimum for anything with integration dependencies)
- Migration path to alternatives or your other products
- Data export
- A person they can contact with questions
### Internal Communication
The team that built it feels the loss personally. Acknowledge it. "This product represents real work and real care. Shutting it down is not a judgment of the team — it's a judgment about fit with where the company is going."
If team members are being reassigned, not let go — make that clear immediately. The fear of job loss will dominate every other concern until you address it.
FILE:scripts/decision_matrix_scorer.py
#!/usr/bin/env python3
"""
Decision Matrix Scorer — Executive Mentor Tool
Weighted multi-criteria decision analysis with sensitivity testing.
Answers: Which option wins? How fragile is that result? Where are the close calls?
Usage:
python decision_matrix_scorer.py # Run with sample data
python decision_matrix_scorer.py --interactive # Interactive mode
python decision_matrix_scorer.py --file data.json # Load from JSON file
JSON format:
{
"decision": "Description of the decision",
"criteria": [
{"name": "Criterion Name", "weight": 0.3, "description": "Optional"},
...
],
"options": [
{
"name": "Option Name",
"description": "Optional description",
"scores": {"Criterion Name": 8, "Another": 6, ...}
},
...
]
}
Scores: 1–10 scale. Weights: must sum to 1.0 (or will be normalized).
"""
import json
import sys
import argparse
from typing import List, Dict, Tuple
# ─────────────────────────────────────────────────────
# Core data structures
# ─────────────────────────────────────────────────────
def normalize_weights(criteria: List[Dict]) -> List[Dict]:
"""Ensure weights sum to 1.0."""
total = sum(c["weight"] for c in criteria)
if abs(total - 1.0) > 0.001:
for c in criteria:
c["weight"] = c["weight"] / total
return criteria
def score_option(option: Dict, criteria: List[Dict]) -> float:
"""Calculate weighted score for an option."""
total = 0.0
for c in criteria:
score = option["scores"].get(c["name"], 5) # Default to 5 if missing
total += score * c["weight"]
return round(total, 3)
def score_all(options: List[Dict], criteria: List[Dict]) -> List[Tuple[str, float]]:
"""Return sorted list of (option_name, weighted_score)."""
results = []
for opt in options:
s = score_option(opt, criteria)
results.append((opt["name"], s))
return sorted(results, key=lambda x: x[1], reverse=True)
# ─────────────────────────────────────────────────────
# Sensitivity analysis
# ─────────────────────────────────────────────────────
def sensitivity_analysis(options: List[Dict], criteria: List[Dict]) -> Dict:
"""
Test how result changes when each criterion's weight is varied ±30%.
Returns dict: criterion → {stable: bool, risk_of_flip: bool, details: str}
"""
baseline = score_all(options, criteria)
winner = baseline[0][0]
results = {}
for i, c in enumerate(criteria):
flips = []
for delta in [-0.30, -0.20, -0.10, +0.10, +0.20, +0.30]:
# Adjust weight of criterion i, redistribute remainder proportionally
test_criteria = [dict(cr) for cr in criteria]
new_weight = max(0.01, test_criteria[i]["weight"] + delta)
old_weight = test_criteria[i]["weight"]
diff = new_weight - old_weight
# Redistribute diff across other criteria
others = [j for j in range(len(test_criteria)) if j != i]
total_other = sum(test_criteria[j]["weight"] for j in others)
if total_other > 0:
for j in others:
proportion = test_criteria[j]["weight"] / total_other
test_criteria[j]["weight"] -= diff * proportion
test_criteria[j]["weight"] = max(0.01, test_criteria[j]["weight"])
test_criteria[i]["weight"] = new_weight
test_criteria = normalize_weights(test_criteria)
test_results = score_all(options, test_criteria)
if test_results[0][0] != winner:
flips.append((delta, test_results[0][0]))
if flips:
smallest_delta = min(abs(delta) for delta, _name in flips)
results[c["name"]] = {
"stable": False,
"flip_at": f"±{int(smallest_delta*100)}% weight change",
"flip_to": flips[0][1],
"importance": "HIGH — result depends heavily on this weight"
}
else:
results[c["name"]] = {
"stable": True,
"flip_at": None,
"flip_to": None,
"importance": "LOW — winner holds even with significant weight changes"
}
return results
def close_call_analysis(results: List[Tuple[str, float]]) -> List[Dict]:
"""Find options within 10% of winner score — these are close calls."""
if not results:
return []
winner_score = results[0][1]
close = []
for name, score in results[1:]:
gap = winner_score - score
gap_pct = (gap / winner_score * 100) if winner_score > 0 else 0
if gap_pct <= 15:
close.append({
"name": name,
"score": score,
"gap": round(gap, 3),
"gap_pct": round(gap_pct, 1),
"verdict": "Very close — recheck assumptions" if gap_pct <= 5 else "Close — worth a second look"
})
return close
def criterion_breakdown(options: List[Dict], criteria: List[Dict]) -> Dict:
"""Show per-criterion scores for each option."""
breakdown = {}
for opt in options:
breakdown[opt["name"]] = {}
for c in criteria:
raw = opt["scores"].get(c["name"], 5)
weighted = raw * c["weight"]
breakdown[opt["name"]][c["name"]] = {
"raw": raw,
"weighted": round(weighted, 3),
"weight": f"{round(c['weight']*100)}%"
}
return breakdown
# ─────────────────────────────────────────────────────
# Output formatting
# ─────────────────────────────────────────────────────
def hr(char="─", width=65):
return char * width
def print_report(data: Dict):
"""Print the full decision analysis report."""
decision = data.get("decision", "Unnamed Decision")
criteria = normalize_weights(data["criteria"])
options = data["options"]
print()
print(hr("═"))
print(f" DECISION MATRIX ANALYSIS")
print(f" {decision}")
print(hr("═"))
# ── Criteria summary
print()
print("CRITERIA & WEIGHTS")
print(hr())
for c in sorted(criteria, key=lambda x: x["weight"], reverse=True):
bar_len = int(c["weight"] * 30)
bar = "█" * bar_len
desc = f" — {c['description']}" if c.get("description") else ""
print(f" {c['name']:<25} {c['weight']*100:>5.1f}% {bar}{desc}")
# ── Scoring results
print()
print("RESULTS (ranked)")
print(hr())
results = score_all(options, criteria)
max_score = 10.0 # max possible weighted score
for rank, (name, score) in enumerate(results, 1):
pct = score / 10.0
bar_len = int(pct * 40)
bar = "█" * bar_len
medal = ["🥇", "🥈", "🥉"][rank-1] if rank <= 3 else f"#{rank} "
print(f" {medal} {name:<25} {score:>5.2f}/10 {bar}")
winner = results[0][0]
print()
print(f" ► Winner: {winner} (score: {results[0][1]:.2f})")
# ── Close calls
close = close_call_analysis(results)
if close:
print()
print("CLOSE CALLS")
print(hr())
for c in close:
print(f" ⚠ {c['name']}: {c['score']:.2f} (gap: {c['gap_pct']}% — {c['verdict']})")
# ── Per-criterion breakdown
print()
print("SCORE BREAKDOWN BY CRITERION")
print(hr())
breakdown = criterion_breakdown(options, criteria)
# Header
opt_names = [opt["name"][:16] for opt in options]
header = f" {'Criterion':<22}"
for n in opt_names:
header += f" {n:>10}"
print(header)
print(" " + hr("-", 63))
for c in criteria:
row = f" {c['name']:<22}"
for opt in options:
raw = opt["scores"].get(c["name"], 5)
row += f" {raw:>10}"
row += f" (weight {c['weight']*100:.0f}%)"
print(row)
# Weighted row
print(" " + hr("-", 63))
weighted_row = f" {'Weighted Total':<22}"
for name, score in results:
# Re-order by options list order
weighted_row += f" {score:>10.2f}"
# Actually print in options order
print(f" {'Weighted Total':<22}", end="")
for opt in options:
s = score_option(opt, criteria)
print(f" {s:>10.2f}", end="")
print()
# ── Sensitivity analysis
print()
print("SENSITIVITY ANALYSIS")
print(hr())
print(" How much does the winner change if we adjust criterion weights?")
print()
sensitivity = sensitivity_analysis(options, criteria)
for crit_name, result in sensitivity.items():
if result["stable"]:
print(f" ✓ {crit_name:<28} STABLE — winner holds at ±30% weight change")
else:
print(f" ⚠ {crit_name:<28} FRAGILE — flips to '{result['flip_to']}' at {result['flip_at']}")
# ── Recommendation
print()
print("RECOMMENDATION")
print(hr())
unstable = [k for k, v in sensitivity.items() if not v["stable"]]
if unstable:
print(f" Winner: {winner}")
print(f" Confidence: MEDIUM — result is sensitive to weights on: {', '.join(unstable)}")
print()
print(" Before committing:")
print(f" • Validate that your weighting of [{', '.join(unstable)}] is correct")
print(" • Consider whether the weight differences reflect genuine priorities")
print(" • If uncertain, run scenario with alternative weights")
else:
print(f" Winner: {winner}")
print(f" Confidence: HIGH — winner is stable across all weight scenarios")
print()
print(" The decision is clear. The main risk is whether your scoring")
print(" of each option on each criterion is accurate.")
print()
print(hr("═"))
print()
# ─────────────────────────────────────────────────────
# Interactive mode
# ─────────────────────────────────────────────────────
def interactive_mode():
"""Guided interactive data entry."""
print()
print(hr("═"))
print(" DECISION MATRIX — Interactive Mode")
print(hr("═"))
data = {}
data["decision"] = input("\nWhat decision are you making?\n> ").strip()
# Criteria
print("\nDefine criteria (what matters in this decision).")
print("Enter criteria one at a time. Empty line to finish.")
print("Weight: importance 0–10 (will be normalized to %).")
print()
criteria = []
while True:
name = input(f"Criterion {len(criteria)+1} name (or ENTER to finish): ").strip()
if not name:
if len(criteria) < 2:
print(" Need at least 2 criteria.")
continue
break
weight_str = input(f" Weight for '{name}' (0–10): ").strip()
try:
weight = float(weight_str)
except ValueError:
weight = 5.0
criteria.append({"name": name, "weight": weight})
data["criteria"] = criteria
# Options
print("\nDefine options (what you're choosing between).")
print("Enter options one at a time. Empty line to finish.")
print()
options = []
while True:
name = input(f"Option {len(options)+1} name (or ENTER to finish): ").strip()
if not name:
if len(options) < 2:
print(" Need at least 2 options.")
continue
break
print(f"\n Score each criterion for '{name}' (1=poor, 10=excellent):")
scores = {}
for c in criteria:
while True:
s = input(f" {c['name']}: ").strip()
try:
score = float(s)
if 1 <= score <= 10:
scores[c["name"]] = score
break
else:
print(" Score must be 1–10")
except ValueError:
print(" Enter a number 1–10")
options.append({"name": name, "scores": scores})
print()
data["options"] = options
print_report(data)
# ─────────────────────────────────────────────────────
# Sample data
# ─────────────────────────────────────────────────────
SAMPLE_DATA = {
"decision": "How to extend runway: Cut costs vs. Raise bridge vs. Accelerate revenue",
"criteria": [
{
"name": "Speed to impact",
"weight": 0.25,
"description": "How quickly does this improve our situation?"
},
{
"name": "Execution risk",
"weight": 0.30,
"description": "How likely is this to actually work? (10=low risk)"
},
{
"name": "Team morale impact",
"weight": 0.20,
"description": "Effect on team (10=positive, 1=very negative)"
},
{
"name": "Runway extension",
"weight": 0.15,
"description": "How much runway does this actually buy?"
},
{
"name": "Strategic fit",
"weight": 0.10,
"description": "Does this align with where we want to go?"
}
],
"options": [
{
"name": "Cost cut 25%",
"description": "Reduce headcount and discretionary spend by 25%",
"scores": {
"Speed to impact": 9,
"Execution risk": 8,
"Team morale impact": 2,
"Runway extension": 8,
"Strategic fit": 5
}
},
{
"name": "Bridge from investors",
"description": "Raise $500K bridge from existing investors to hit next milestone",
"scores": {
"Speed to impact": 6,
"Execution risk": 5,
"Team morale impact": 7,
"Runway extension": 6,
"Strategic fit": 7
}
},
{
"name": "Accelerate revenue",
"description": "Push 3 enterprise deals hard, offer incentives for Q4 close",
"scores": {
"Speed to impact": 4,
"Execution risk": 3,
"Team morale impact": 9,
"Runway extension": 9,
"Strategic fit": 10
}
},
{
"name": "Hybrid: cut 15% + bridge",
"description": "Smaller cuts combined with a modest bridge round",
"scores": {
"Speed to impact": 7,
"Execution risk": 6,
"Team morale impact": 5,
"Runway extension": 7,
"Strategic fit": 6
}
}
]
}
# ─────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="Decision Matrix Scorer — weighted analysis with sensitivity testing"
)
parser.add_argument(
"--interactive", "-i",
action="store_true",
help="Interactive mode: enter decision data manually"
)
parser.add_argument(
"--file", "-f",
type=str,
help="Load decision data from JSON file"
)
parser.add_argument(
"--sample",
action="store_true",
help="Show sample data structure and exit"
)
args = parser.parse_args()
if args.sample:
print(json.dumps(SAMPLE_DATA, indent=2))
return
if args.interactive:
interactive_mode()
return
if args.file:
try:
with open(args.file) as f:
data = json.load(f)
print_report(data)
except FileNotFoundError:
print(f"Error: File '{args.file}' not found.")
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in '{args.file}': {e}")
sys.exit(1)
return
# Default: run sample data
print()
print("Running with sample data. Use --interactive for custom input or --file for JSON.")
print_report(SAMPLE_DATA)
if __name__ == "__main__":
main()
FILE:scripts/stakeholder_mapper.py
#!/usr/bin/env python3
"""
Stakeholder Mapper — Executive Mentor Tool
Maps stakeholders by influence and alignment.
Identifies: champions, blockers, swing votes, and hidden risks.
Outputs: stakeholder grid with engagement strategy per quadrant.
Usage:
python stakeholder_mapper.py # Run with sample data
python stakeholder_mapper.py --interactive # Interactive mode
python stakeholder_mapper.py --file data.json # Load from JSON file
JSON format:
{
"initiative": "Name of the decision or initiative",
"stakeholders": [
{
"name": "Person/Group Name",
"role": "Their role or title",
"influence": 8, // 1–10: how much power they have over outcome
"alignment": 3, // 1–10: how supportive they are (10=champion, 1=blocker)
"interest": 7, // 1–10: how interested/engaged they are
"notes": "Optional context — what drives them, hidden concerns, relationships"
}
]
}
"""
import json
import sys
import argparse
from typing import List, Dict, Tuple, Optional
# ─────────────────────────────────────────────────────
# Quadrant classification
# ─────────────────────────────────────────────────────
def classify_stakeholder(influence: float, alignment: float) -> Dict:
"""
Classify into strategic quadrant based on influence and alignment.
Quadrants:
- Champions (high influence, high alignment): Your most valuable assets
- Blockers (high influence, low alignment): Your biggest risks
- Supporters (low influence, high alignment): Useful but less critical
- Bystanders (low influence, low alignment): Monitor, low priority
- Swing Votes (medium influence, medium alignment): Key to persuade
"""
mid_influence = 5.5
mid_alignment = 5.5
# Special case: swing votes — medium on both dimensions
if 4 <= influence <= 7 and 4 <= alignment <= 7:
return {
"quadrant": "Swing Vote",
"symbol": "⚡",
"priority": "HIGH",
"strategy": "Persuade — understand concerns, address directly, build relationship"
}
if influence >= mid_influence and alignment >= mid_alignment:
return {
"quadrant": "Champion",
"symbol": "★",
"priority": "HIGH",
"strategy": "Leverage — activate them as advocates, give them a role in the initiative"
}
elif influence >= mid_influence and alignment < mid_alignment:
return {
"quadrant": "Blocker",
"symbol": "✖",
"priority": "CRITICAL",
"strategy": "Address — understand their specific objections, find common ground or neutralize"
}
elif influence < mid_influence and alignment >= mid_alignment:
return {
"quadrant": "Supporter",
"symbol": "○",
"priority": "MEDIUM",
"strategy": "Maintain — keep informed and engaged, potentially increase their influence"
}
else:
return {
"quadrant": "Bystander",
"symbol": "·",
"priority": "LOW",
"strategy": "Monitor — minimal investment, keep informed with standard comms"
}
def risk_flags(stakeholder: Dict) -> List[str]:
"""Identify specific risk signals for a stakeholder."""
flags = []
influence = stakeholder["influence"]
alignment = stakeholder["alignment"]
interest = stakeholder.get("interest", 5)
if influence >= 7 and alignment <= 3:
flags.append("🔴 HIGH-POWER BLOCKER — can kill this initiative")
if influence >= 7 and alignment <= 5 and interest >= 7:
flags.append("🟡 ENGAGED SKEPTIC — high influence, paying close attention, not convinced")
if alignment <= 4 and interest >= 8:
flags.append("🟡 ACTIVE OPPOSITION — low alignment but highly engaged — may mobilize others")
if influence >= 6 and alignment >= 7 and interest <= 3:
flags.append("🟡 DISENGAGED CHAMPION — strong supporter but not paying attention — needs activation")
if influence >= 5 and 4 <= alignment <= 6:
flags.append("⚡ PERSUADABLE — medium influence, genuinely undecided — high ROI to engage")
return flags
# ─────────────────────────────────────────────────────
# Analysis
# ─────────────────────────────────────────────────────
def calculate_overall_alignment(stakeholders: List[Dict]) -> Dict:
"""Calculate weighted average alignment (weighted by influence)."""
if not stakeholders:
return {"score": 0, "verdict": "No data"}
total_influence = sum(s["influence"] for s in stakeholders)
if total_influence == 0:
return {"score": 0, "verdict": "No influence"}
weighted_alignment = sum(
s["alignment"] * s["influence"] for s in stakeholders
) / total_influence
if weighted_alignment >= 7:
verdict = "FAVORABLE — strong support among influential stakeholders"
elif weighted_alignment >= 5:
verdict = "MIXED — significant opposition needs to be addressed"
else:
verdict = "UNFAVORABLE — initiative faces significant headwinds"
return {
"score": round(weighted_alignment, 2),
"verdict": verdict
}
def find_critical_path(stakeholders: List[Dict]) -> List[Dict]:
"""
Identify the minimal set of stakeholders whose alignment is critical.
These are high-influence stakeholders — their position determines the outcome.
"""
high_influence = [s for s in stakeholders if s["influence"] >= 7]
return sorted(high_influence, key=lambda x: x["influence"], reverse=True)
def engagement_sequencing(stakeholders: List[Dict]) -> List[Dict]:
"""
Recommend engagement sequence.
Order: Fix blockers → Activate champions → Persuade swing votes → Maintain rest.
"""
classified = []
for s in stakeholders:
cls = classify_stakeholder(s["influence"], s["alignment"])
classified.append({**s, **cls})
# Sort by engagement priority
priority_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
classified.sort(key=lambda x: (priority_order[x["priority"]], -x["influence"]))
return classified
# ─────────────────────────────────────────────────────
# ASCII grid visualization
# ─────────────────────────────────────────────────────
def render_grid(stakeholders: List[Dict], width: int = 60) -> str:
"""
Render a 2D influence vs alignment grid with stakeholder positions.
Y-axis: Influence (top = high)
X-axis: Alignment (left = low, right = high)
"""
rows = 10
cols = 20
grid = [[' ' for _ in range(cols)] for _ in range(rows)]
for s in stakeholders:
influence = s["influence"]
alignment = s["alignment"]
# Map scores 1–10 to grid coordinates
col = int((alignment - 1) / 9 * (cols - 1))
row = rows - 1 - int((influence - 1) / 9 * (rows - 1))
col = max(0, min(cols - 1, col))
row = max(0, min(rows - 1, row))
initial = s["name"][0].upper()
if grid[row][col] == ' ':
grid[row][col] = initial
else:
grid[row][col] = '+' # Overlap
lines = []
lines.append(" STAKEHOLDER MAP (Influence ↑ | Alignment →)")
lines.append("")
lines.append(f" HIGH ┌{'─'*cols}┐")
for i, row in enumerate(grid):
if i == rows // 2:
prefix = " INFL "
else:
prefix = " "
lines.append(f"{prefix}│{''.join(row)}│")
lines.append(f" LOW └{'─'*cols}┘")
lines.append(f" {'BLOCKER':<12} {'SWING':<8} CHAMPION")
lines.append(f" Low alignment High alignment")
lines.append("")
# Legend
lines.append(" Legend (initials):")
for s in stakeholders:
cls = classify_stakeholder(s["influence"], s["alignment"])
lines.append(f" {s['name'][0].upper()} = {s['name']} ({cls['symbol']} {cls['quadrant']})")
return "\n".join(lines)
# ─────────────────────────────────────────────────────
# Output formatting
# ─────────────────────────────────────────────────────
def hr(char="─", width=65):
return char * width
def print_report(data: Dict):
initiative = data.get("initiative", "Unnamed Initiative")
stakeholders = data["stakeholders"]
# Validate and fill defaults
for s in stakeholders:
s.setdefault("interest", 5)
s.setdefault("notes", "")
s["influence"] = max(1, min(10, float(s["influence"])))
s["alignment"] = max(1, min(10, float(s["alignment"])))
s["interest"] = max(1, min(10, float(s["interest"])))
print()
print(hr("═"))
print(f" STAKEHOLDER ANALYSIS")
print(f" {initiative}")
print(hr("═"))
# Overall assessment
overall = calculate_overall_alignment(stakeholders)
print()
print("OVERALL ASSESSMENT")
print(hr())
print(f" Weighted alignment score: {overall['score']}/10")
print(f" Verdict: {overall['verdict']}")
# Grid visualization
print()
print(hr())
print(render_grid(stakeholders))
# Stakeholder profiles by quadrant
sequenced = engagement_sequencing(stakeholders)
# Group by quadrant
quadrants = {}
for s in sequenced:
q = s["quadrant"]
if q not in quadrants:
quadrants[q] = []
quadrants[q].append(s)
quadrant_order = ["Blocker", "Swing Vote", "Champion", "Supporter", "Bystander"]
print()
print("STAKEHOLDER PROFILES")
print(hr())
for q_name in quadrant_order:
if q_name not in quadrants:
continue
group = quadrants[q_name]
first = group[0]
print()
print(f" {first['symbol']} {q_name.upper()}S ({len(group)} stakeholder{'s' if len(group)>1 else ''})")
print(f" Strategy: {first['strategy']}")
print()
for s in group:
cls = classify_stakeholder(s["influence"], s["alignment"])
flags = risk_flags(s)
print(f" {s['name']}")
print(f" Role: {s.get('role', 'Not specified')}")
print(f" Influence: {'█'*int(s['influence']//2)}{'░'*(5-int(s['influence']//2))} {s['influence']:.0f}/10 "
f"Alignment: {'█'*int(s['alignment']//2)}{'░'*(5-int(s['alignment']//2))} {s['alignment']:.0f}/10 "
f"Interest: {'█'*int(s['interest']//2)}{'░'*(5-int(s['interest']//2))} {s['interest']:.0f}/10")
if flags:
for flag in flags:
print(f" {flag}")
if s.get("notes"):
print(f" Notes: {s['notes']}")
print()
# Engagement plan
print()
print("ENGAGEMENT PLAN (sequenced by priority)")
print(hr())
print()
print(f" {'#':<3} {'Name':<22} {'Quadrant':<14} {'Priority':<10} {'First Action'}")
print(f" {hr('-', 63)}")
actions = {
"Blocker": "Schedule 1:1 — understand specific objections",
"Swing Vote": "Coffee or informal conversation — listen first",
"Champion": "Brief them on the initiative — give them a role",
"Supporter": "Keep informed — monthly update or email",
"Bystander": "Include in standard comms only"
}
for i, s in enumerate(sequenced, 1):
action = actions.get(s["quadrant"], "Maintain standard communication")
print(f" {i:<3} {s['name']:<22} {s['quadrant']:<14} {s['priority']:<10} {action}")
# Risk summary
print()
print("RISK SUMMARY")
print(hr())
critical_path = find_critical_path(stakeholders)
if critical_path:
print()
print(" High-influence stakeholders (outcome depends on these):")
for s in critical_path:
cls = classify_stakeholder(s["influence"], s["alignment"])
alignment_label = "CHAMPION" if s["alignment"] >= 7 else "BLOCKER" if s["alignment"] <= 4 else "UNDECIDED"
print(f" {cls['symbol']} {s['name']:<25} influence {s['influence']:.0f}/10 → {alignment_label}")
# All risk flags
all_flags = []
for s in stakeholders:
flags = risk_flags(s)
for flag in flags:
all_flags.append((s["name"], flag))
if all_flags:
print()
print(" Risk flags:")
for name, flag in all_flags:
print(f" [{name}] {flag}")
print()
print(hr("═"))
print()
# ─────────────────────────────────────────────────────
# Interactive mode
# ─────────────────────────────────────────────────────
def interactive_mode():
print()
print(hr("═"))
print(" STAKEHOLDER MAPPER — Interactive Mode")
print(hr("═"))
data = {}
data["initiative"] = input("\nWhat initiative or decision are you mapping?\n> ").strip()
print("\nAdd stakeholders one at a time. Empty name to finish.")
print("Scores: 1=low, 10=high")
print()
stakeholders = []
while True:
name = input(f"Stakeholder {len(stakeholders)+1} name (or ENTER to finish): ").strip()
if not name:
if len(stakeholders) < 1:
print(" Need at least 1 stakeholder.")
continue
break
role = input(f" Role/title: ").strip()
def get_score(prompt, default=5):
while True:
s = input(f" {prompt} (1–10, default {default}): ").strip()
if not s:
return float(default)
try:
v = float(s)
if 1 <= v <= 10:
return v
print(" Must be 1–10")
except ValueError:
print(" Enter a number")
influence = get_score("Influence (power over this decision)")
alignment = get_score("Alignment (1=opposed, 10=champion)")
interest = get_score("Interest level (how engaged are they)")
notes = input(f" Notes (optional): ").strip()
stakeholders.append({
"name": name,
"role": role,
"influence": influence,
"alignment": alignment,
"interest": interest,
"notes": notes
})
print()
data["stakeholders"] = stakeholders
print_report(data)
# ─────────────────────────────────────────────────────
# Sample data
# ─────────────────────────────────────────────────────
SAMPLE_DATA = {
"initiative": "Migrate from monolith to microservices (18-month program)",
"stakeholders": [
{
"name": "Sarah Chen (CTO)",
"role": "Chief Technology Officer",
"influence": 10,
"alignment": 9,
"interest": 9,
"notes": "Driving force behind the initiative. Will fund and protect the team."
},
{
"name": "Marcus Webb (CFO)",
"role": "Chief Financial Officer",
"influence": 9,
"alignment": 3,
"interest": 6,
"notes": "Concerned about 18-month cost with no visible revenue return. Has budget veto."
},
{
"name": "Priya Agarwal (VP Eng)",
"role": "VP Engineering",
"influence": 8,
"alignment": 7,
"interest": 8,
"notes": "Supportive in principle, worried about team bandwidth alongside feature delivery."
},
{
"name": "Tom Briggs (VP Product)",
"role": "VP Product",
"influence": 7,
"alignment": 4,
"interest": 5,
"notes": "Concerned about roadmap slowdown. Hasn't been in the architecture discussions."
},
{
"name": "Elena Park (CEO)",
"role": "Chief Executive Officer",
"influence": 10,
"alignment": 6,
"interest": 4,
"notes": "Trusts the CTO but will back out if CFO and VP Product both push back hard."
},
{
"name": "Raj Patel (Lead Arch)",
"role": "Lead Architect",
"influence": 6,
"alignment": 10,
"interest": 10,
"notes": "Deep technical champion. Has proposed detailed migration plan."
},
{
"name": "Dev Team Leads (x4)",
"role": "Team Leads",
"influence": 5,
"alignment": 6,
"interest": 7,
"notes": "Mixed. Some excited, some worried about learning curve. Middle ground."
},
{
"name": "Board (investor reps)",
"role": "Board Directors",
"influence": 9,
"alignment": 5,
"interest": 3,
"notes": "Not paying attention unless CFO raises flags. Could become blockers if CFO escalates."
}
]
}
# ─────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="Stakeholder Mapper — influence, alignment, and engagement strategy"
)
parser.add_argument(
"--interactive", "-i",
action="store_true",
help="Interactive mode: enter stakeholder data manually"
)
parser.add_argument(
"--file", "-f",
type=str,
help="Load stakeholder data from JSON file"
)
parser.add_argument(
"--sample",
action="store_true",
help="Print sample JSON structure and exit"
)
args = parser.parse_args()
if args.sample:
print(json.dumps(SAMPLE_DATA, indent=2))
return
if args.interactive:
interactive_mode()
return
if args.file:
try:
with open(args.file) as f:
data = json.load(f)
print_report(data)
except FileNotFoundError:
print(f"Error: File '{args.file}' not found.")
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in '{args.file}': {e}")
sys.exit(1)
return
# Default: sample data
print()
print("Running with sample data. Use --interactive for custom input or --file for JSON.")
print_report(SAMPLE_DATA)
if __name__ == "__main__":
main()
Biến mẫu đã kiểm chứng hoặc cách gỡ lỗi thành skill độc lập có thể tái sử dụng, gồm SKILL.md, tài liệu tham khảo và ví dụ.
---
name: "extract"
description: "Turn a proven pattern or debugging solution into a standalone reusable skill with SKILL.md, reference docs, and examples."
---
# /si:extract — Create Skills from Patterns
Transforms a recurring pattern or debugging solution into a standalone, portable skill that can be installed in any project.
## Usage
```
/si:extract <pattern description> # Interactive extraction
/si:extract <pattern> --name docker-m1-fixes # Specify skill name
/si:extract <pattern> --output ./skills/ # Custom output directory
/si:extract <pattern> --dry-run # Preview without creating files
```
## When to Extract
A learning qualifies for skill extraction when ANY of these are true:
| Criterion | Signal |
|---|---|
| **Recurring** | Same issue across 2+ projects |
| **Non-obvious** | Required real debugging to discover |
| **Broadly applicable** | Not tied to one specific codebase |
| **Complex solution** | Multi-step fix that's easy to forget |
| **User-flagged** | "Save this as a skill", "I want to reuse this" |
## Workflow
### Step 1: Identify the pattern
Read the user's description. Search auto-memory for related entries:
```bash
MEMORY_DIR="$HOME/.claude/projects/$(pwd | sed 's|/|%2F|g; s|%2F|/|; s|^/||')/memory"
grep -rni "<keywords>" "$MEMORY_DIR/"
```
If found in auto-memory, use those entries as source material. If not, use the user's description directly.
### Step 2: Determine skill scope
Ask (max 2 questions):
- "What problem does this solve?" (if not clear)
- "Should this include code examples?" (if applicable)
### Step 3: Generate skill name
Rules for naming:
- Lowercase, hyphens between words
- Descriptive but concise (2-4 words)
- Examples: `docker-m1-fixes`, `api-timeout-patterns`, `pnpm-workspace-setup`
**Reserved fragments — must NOT appear in the skill name:**
- `claude`
- `anthropic`
For skills about Claude Code itself, use the `cc-` prefix instead:
- ❌ `claude-code-settings` → ✅ `cc-settings`
- ❌ `claude-code-maintenance` → ✅ `cc-maintenance`
- ❌ `claude-mcp-tools` → ✅ `cc-mcp-tools`
- ❌ `claude-plugin-development` → ✅ `cc-plugin-development`
Before writing the skill directory, check the proposed name against this list.
If a reserved fragment is present, transform it (drop the fragment or replace
the `claude*`/`anthropic*` prefix with `cc-`) and confirm with the user.
### Step 4: Create the skill files
**Spawn the `skill-extractor` agent** for the actual file generation.
The agent creates:
```
<skill-name>/
├── SKILL.md # Main skill file with frontmatter
├── README.md # Human-readable overview
└── reference/ # (optional) Supporting documentation
└── examples.md # Concrete examples and edge cases
```
### Step 5: SKILL.md structure
The generated SKILL.md must follow this format:
```markdown
---
name: "skill-name"
description: "<one-line description>. Use when: <trigger conditions>."
---
# <Skill Title>
> One-line summary of what this skill solves.
## Quick Reference
| Problem | Solution |
|---------|----------|
| {{problem 1}} | {{solution 1}} |
| {{problem 2}} | {{solution 2}} |
## The Problem
{{2-3 sentences explaining what goes wrong and why it's non-obvious.}}
## Solutions
### Option 1: {{Name}} (Recommended)
{{Step-by-step with code examples.}}
### Option 2: {{Alternative}}
{{For when Option 1 doesn't apply.}}
## Trade-offs
| Approach | Pros | Cons |
|----------|------|------|
| Option 1 | {{pros}} | {{cons}} |
| Option 2 | {{pros}} | {{cons}} |
## Edge Cases
- {{edge case 1 and how to handle it}}
- {{edge case 2 and how to handle it}}
```
### Step 6: Quality gates
Before finalizing, verify:
- [ ] SKILL.md has valid YAML frontmatter with `name` and `description`
- [ ] `name` matches the folder name (lowercase, hyphens)
- [ ] `name` does NOT contain reserved fragments `claude` or `anthropic` (use `cc-` prefix for Claude Code skills)
- [ ] Description includes "Use when:" trigger conditions
- [ ] Solutions are self-contained (no external context needed)
- [ ] Code examples are complete and copy-pasteable
- [ ] No project-specific hardcoded values (paths, URLs, credentials)
- [ ] No unnecessary dependencies
### Step 7: Report
```
✅ Skill extracted: {{skill-name}}
Files created:
{{path}}/SKILL.md ({{lines}} lines)
{{path}}/README.md ({{lines}} lines)
{{path}}/reference/examples.md ({{lines}} lines)
Install: /plugin install (copy to your skills directory)
Publish: clawhub publish {{path}}
Source: MEMORY.md entries at lines {{n, m, ...}} (retained — the skill is portable, the memory is project-specific)
```
## Examples
### Extracting a debugging pattern
```
/si:extract "Fix for Docker builds failing on Apple Silicon with platform mismatch"
```
Creates `docker-m1-fixes/SKILL.md` with:
- The platform mismatch error message
- Three solutions (build flag, Dockerfile, docker-compose)
- Trade-offs table
- Performance note about Rosetta 2 emulation
### Extracting a workflow pattern
```
/si:extract "Always regenerate TypeScript API client after modifying OpenAPI spec"
```
Creates `api-client-regen/SKILL.md` with:
- Why manual regen is needed
- The exact command sequence
- CI integration snippet
- Common failure modes
## Tips
- Extract patterns that would save time in a *different* project
- Keep skills focused — one problem per skill
- Include the error messages people would search for
- Test the skill by reading it without the original context — does it make sense?
Tư vấn FDA cho công ty thiết bị y tế: lộ trình 510(k)/PMA/De Novo, tuân thủ QSR (21 CFR 820), HIPAA và an ninh mạng thiết bị.
---
name: "fda-consultant-specialist"
description: FDA regulatory consultant for medical device companies. Provides 510(k)/PMA/De Novo pathway guidance, QSR (21 CFR 820) compliance, HIPAA assessments, and device cybersecurity. Use when user mentions FDA submission, 510(k), PMA, De Novo, QSR, premarket, predicate device, substantial equivalence, HIPAA medical device, or FDA cybersecurity.
---
# FDA Consultant Specialist
FDA regulatory consulting for medical device manufacturers covering submission pathways, Quality System Regulation (QSR), HIPAA compliance, and device cybersecurity requirements.
## Table of Contents
- [FDA Pathway Selection](#fda-pathway-selection)
- [510(k) Submission Process](#510k-submission-process)
- [QSR Compliance](#qsr-compliance)
- [HIPAA for Medical Devices](#hipaa-for-medical-devices)
- [Device Cybersecurity](#device-cybersecurity)
- [Resources](#resources)
---
## FDA Pathway Selection
Determine the appropriate FDA regulatory pathway based on device classification and predicate availability.
### Decision Framework
```
Predicate device exists?
├── YES → Substantially equivalent?
│ ├── YES → 510(k) Pathway
│ │ ├── No design changes → Abbreviated 510(k)
│ │ ├── Manufacturing only → Special 510(k)
│ │ └── Design/performance → Traditional 510(k)
│ └── NO → PMA or De Novo
└── NO → Novel device?
├── Low-to-moderate risk → De Novo
└── High risk (Class III) → PMA
```
### Pathway Comparison
| Pathway | When to Use | Timeline | Cost |
|---------|-------------|----------|------|
| 510(k) Traditional | Predicate exists, design changes | 90 days | $21,760 |
| 510(k) Special | Manufacturing changes only | 30 days | $21,760 |
| 510(k) Abbreviated | Guidance/standard conformance | 30 days | $21,760 |
| De Novo | Novel, low-moderate risk | 150 days | $134,676 |
| PMA | Class III, no predicate | 180+ days | $425,000+ |
### Pre-Submission Strategy
1. Identify product code and classification
2. Search 510(k) database for predicates
3. Assess substantial equivalence feasibility
4. Prepare Q-Sub questions for FDA
5. Schedule Pre-Sub meeting if needed
**Reference:** See [fda_submission_guide.md](references/fda_submission_guide.md) for pathway decision matrices and submission requirements.
---
## 510(k) Submission Process
### Workflow
```
Phase 1: Planning
├── Step 1: Identify predicate device(s)
├── Step 2: Compare intended use and technology
├── Step 3: Determine testing requirements
└── Checkpoint: SE argument feasible?
Phase 2: Preparation
├── Step 4: Complete performance testing
├── Step 5: Prepare device description
├── Step 6: Document SE comparison
├── Step 7: Finalize labeling
└── Checkpoint: All required sections complete?
Phase 3: Submission
├── Step 8: Assemble submission package
├── Step 9: Submit via eSTAR
├── Step 10: Track acknowledgment
└── Checkpoint: Submission accepted?
Phase 4: Review
├── Step 11: Monitor review status
├── Step 12: Respond to AI requests
├── Step 13: Receive decision
└── Verification: SE letter received?
```
### Required Sections (21 CFR 807.87)
| Section | Content |
|---------|---------|
| Cover Letter | Submission type, device ID, contact info |
| Form 3514 | CDRH premarket review cover sheet |
| Device Description | Physical description, principles of operation |
| Indications for Use | Form 3881, patient population, use environment |
| SE Comparison | Side-by-side comparison with predicate |
| Performance Testing | Bench, biocompatibility, electrical safety |
| Software Documentation | Level of concern, hazard analysis (IEC 62304) |
| Labeling | IFU, package labels, warnings |
| 510(k) Summary | Public summary of submission |
### Common RTA Issues
| Issue | Prevention |
|-------|------------|
| Missing user fee | Verify payment before submission |
| Incomplete Form 3514 | Review all fields, ensure signature |
| No predicate identified | Confirm K-number in FDA database |
| Inadequate SE comparison | Address all technological characteristics |
---
## QSR Compliance
Quality System Regulation (21 CFR Part 820) requirements for medical device manufacturers.
### Key Subsystems
| Section | Title | Focus |
|---------|-------|-------|
| 820.20 | Management Responsibility | Quality policy, org structure, management review |
| 820.30 | Design Controls | Input, output, review, verification, validation |
| 820.40 | Document Controls | Approval, distribution, change control |
| 820.50 | Purchasing Controls | Supplier qualification, purchasing data |
| 820.70 | Production Controls | Process validation, environmental controls |
| 820.100 | CAPA | Root cause analysis, corrective actions |
| 820.181 | Device Master Record | Specifications, procedures, acceptance criteria |
### Design Controls Workflow (820.30)
```
Step 1: Design Input
└── Capture user needs, intended use, regulatory requirements
Verification: Inputs reviewed and approved?
Step 2: Design Output
└── Create specifications, drawings, software architecture
Verification: Outputs traceable to inputs?
Step 3: Design Review
└── Conduct reviews at each phase milestone
Verification: Review records with signatures?
Step 4: Design Verification
└── Perform testing against specifications
Verification: All tests pass acceptance criteria?
Step 5: Design Validation
└── Confirm device meets user needs in actual use conditions
Verification: Validation report approved?
Step 6: Design Transfer
└── Release to production with DMR complete
Verification: Transfer checklist complete?
```
### CAPA Process (820.100)
1. **Identify**: Document nonconformity or potential problem
2. **Investigate**: Perform root cause analysis (5 Whys, Fishbone)
3. **Plan**: Define corrective/preventive actions
4. **Implement**: Execute actions, update documentation
5. **Verify**: Confirm implementation complete
6. **Effectiveness**: Monitor for recurrence (30-90 days)
7. **Close**: Management approval and closure
**Reference:** See [qsr_compliance_requirements.md](references/qsr_compliance_requirements.md) for detailed QSR implementation guidance.
---
## HIPAA for Medical Devices
HIPAA requirements for devices that create, store, transmit, or access Protected Health Information (PHI).
### Applicability
| Device Type | HIPAA Applies |
|-------------|---------------|
| Standalone diagnostic (no data transmission) | No |
| Connected device transmitting patient data | Yes |
| Device with EHR integration | Yes |
| SaMD storing patient information | Yes |
| Wellness app (no diagnosis) | Only if stores PHI |
### Required Safeguards
```
Administrative (§164.308)
├── Security officer designation
├── Risk analysis and management
├── Workforce training
├── Incident response procedures
└── Business associate agreements
Physical (§164.310)
├── Facility access controls
├── Workstation security
└── Device disposal procedures
Technical (§164.312)
├── Access control (unique IDs, auto-logoff)
├── Audit controls (logging)
├── Integrity controls (checksums, hashes)
├── Authentication (MFA recommended)
└── Transmission security (TLS 1.2+)
```
### Risk Assessment Steps
1. Inventory all systems handling ePHI
2. Document data flows (collection, storage, transmission)
3. Identify threats and vulnerabilities
4. Assess likelihood and impact
5. Determine risk levels
6. Implement controls
7. Document residual risk
**Reference:** See [hipaa_compliance_framework.md](references/hipaa_compliance_framework.md) for implementation checklists and BAA templates.
---
## Device Cybersecurity
FDA cybersecurity requirements for connected medical devices.
### Premarket Requirements
| Element | Description |
|---------|-------------|
| Threat Model | STRIDE analysis, attack trees, trust boundaries |
| Security Controls | Authentication, encryption, access control |
| SBOM | Software Bill of Materials (CycloneDX or SPDX) |
| Security Testing | Penetration testing, vulnerability scanning |
| Vulnerability Plan | Disclosure process, patch management |
### Device Tier Classification
**Tier 1 (Higher Risk):**
- Connects to network/internet
- Cybersecurity incident could cause patient harm
**Tier 2 (Standard Risk):**
- All other connected devices
### Postmarket Obligations
1. Monitor NVD and ICS-CERT for vulnerabilities
2. Assess applicability to device components
3. Develop and test patches
4. Communicate with customers
5. Report to FDA per guidance
### Coordinated Vulnerability Disclosure
```
Researcher Report
↓
Acknowledgment (48 hours)
↓
Initial Assessment (5 days)
↓
Fix Development
↓
Coordinated Public Disclosure
```
**Reference:** See [device_cybersecurity_guidance.md](references/device_cybersecurity_guidance.md) for SBOM format examples and threat modeling templates.
---
## Resources
### scripts/
| Script | Purpose |
|--------|---------|
| `fda_submission_tracker.py` | Track 510(k)/PMA/De Novo submission milestones and timelines |
| `qsr_compliance_checker.py` | Assess 21 CFR 820 compliance against project documentation |
| `hipaa_risk_assessment.py` | Evaluate HIPAA safeguards in medical device software |
### references/
| File | Content |
|------|---------|
| `fda_submission_guide.md` | 510(k), De Novo, PMA submission requirements and checklists |
| `qsr_compliance_requirements.md` | 21 CFR 820 implementation guide with templates |
| `hipaa_compliance_framework.md` | HIPAA Security Rule safeguards and BAA requirements |
| `device_cybersecurity_guidance.md` | FDA cybersecurity requirements, SBOM, threat modeling |
| `fda_capa_requirements.md` | CAPA process, root cause analysis, effectiveness verification |
### Usage Examples
```bash
# Track FDA submission status
python scripts/fda_submission_tracker.py /path/to/project --type 510k
# Assess QSR compliance
python scripts/qsr_compliance_checker.py /path/to/project --section 820.30
# Run HIPAA risk assessment
python scripts/hipaa_risk_assessment.py /path/to/project --category technical
```
FILE:references/device_cybersecurity_guidance.md
# Medical Device Cybersecurity Guidance
Complete framework for FDA cybersecurity requirements based on FDA guidance documents and recognized consensus standards.
---
## Table of Contents
- [Regulatory Framework](#regulatory-framework)
- [Premarket Cybersecurity](#premarket-cybersecurity)
- [Postmarket Cybersecurity](#postmarket-cybersecurity)
- [Threat Modeling](#threat-modeling)
- [Security Controls](#security-controls)
- [Software Bill of Materials](#software-bill-of-materials)
- [Vulnerability Management](#vulnerability-management)
- [Documentation Requirements](#documentation-requirements)
---
## Regulatory Framework
### FDA Guidance Documents
| Document | Scope | Key Requirements |
|----------|-------|------------------|
| Premarket Cybersecurity (2023) | 510(k), PMA, De Novo | Security design, SBOM, threat modeling |
| Postmarket Management (2016) | All marketed devices | Vulnerability monitoring, patching |
| Content of Premarket Submissions | Submission format | Documentation structure |
### PATCH Act Requirements (2023)
**Cyber Device Definition:**
- Contains software
- Can connect to internet
- May be vulnerable to cybersecurity threats
**Manufacturer Obligations:**
1. Submit plan to monitor, identify, and address vulnerabilities
2. Design, develop, and maintain processes to ensure device security
3. Provide software bill of materials (SBOM)
4. Comply with other requirements under section 524B
### Recognized Consensus Standards
| Standard | Scope | FDA Recognition |
|----------|-------|-----------------|
| IEC 62443 | Industrial automation security | Recognized |
| NIST Cybersecurity Framework | Security framework | Referenced |
| UL 2900 | Software cybersecurity | Recognized |
| AAMI TIR57 | Medical device cybersecurity | Referenced |
| IEC 81001-5-1 | Health software security | Recognized |
---
## Premarket Cybersecurity
### Cybersecurity Documentation Requirements
```
Cybersecurity Documentation Package:
├── 1. Security Risk Assessment
│ ├── Threat model
│ ├── Vulnerability assessment
│ ├── Risk analysis
│ └── Risk mitigation
├── 2. Security Architecture
│ ├── System diagram
│ ├── Data flow diagram
│ ├── Trust boundaries
│ └── Security controls
├── 3. Cybersecurity Testing
│ ├── Penetration testing
│ ├── Vulnerability scanning
│ ├── Fuzz testing
│ └── Security code review
├── 4. SBOM
│ ├── Software components
│ ├── Versions
│ └── Known vulnerabilities
├── 5. Vulnerability Management Plan
│ ├── Monitoring process
│ ├── Disclosure process
│ └── Patch management
└── 6. Labeling
├── Security instructions
└── End-of-life plan
```
### Device Tier Classification
**Tier 1 - Higher Cybersecurity Risk:**
- Device can connect to another product or network
- A cybersecurity incident could directly result in patient harm
**Tier 2 - Standard Cybersecurity Risk:**
- Device NOT a Tier 1 device
- Still requires cybersecurity documentation
**Documentation Depth by Tier:**
| Element | Tier 1 | Tier 2 |
|---------|--------|--------|
| Threat model | Comprehensive | Basic |
| Penetration testing | Required | Recommended |
| SBOM | Required | Required |
| Security testing | Full suite | Core testing |
### Security by Design Principles
```markdown
## Secure Product Development Framework (SPDF)
### 1. Security Risk Management
- Integrate security into QMS
- Apply throughout product lifecycle
- Document security decisions
### 2. Security Architecture
- Defense in depth
- Least privilege
- Secure defaults
- Fail securely
### 3. Cybersecurity Testing
- Verify security controls
- Test for known vulnerabilities
- Validate threat mitigations
### 4. Cybersecurity Transparency
- SBOM provision
- Vulnerability disclosure
- Coordinated vulnerability disclosure
### 5. Cybersecurity Maintenance
- Monitor for vulnerabilities
- Provide timely updates
- Support throughout lifecycle
```
---
## Postmarket Cybersecurity
### Vulnerability Monitoring
**Sources to Monitor:**
- National Vulnerability Database (NVD)
- ICS-CERT advisories
- Third-party component vendors
- Security researcher reports
- Customer/user reports
**Monitoring Process:**
```
Daily/Weekly Monitoring:
├── NVD feed check
├── Vendor security bulletins
├── Security mailing lists
└── ISAC notifications
Monthly Review:
├── Component vulnerability analysis
├── Risk re-assessment
├── Patch status review
└── Trending threat analysis
Quarterly Assessment:
├── Comprehensive vulnerability scan
├── Third-party security audit
├── Update threat model
└── Security metrics review
```
### Vulnerability Assessment and Response
**CVSS-Based Triage:**
| CVSS Score | Severity | Response Timeframe |
|------------|----------|-------------------|
| 9.0-10.0 | Critical | 24-48 hours assessment |
| 7.0-8.9 | High | 1 week assessment |
| 4.0-6.9 | Medium | 30 days assessment |
| 0.1-3.9 | Low | Quarterly review |
**Exploitability Assessment:**
```markdown
## Vulnerability Exploitation Assessment
### Device-Specific Factors
- [ ] Is the vulnerability reachable in device configuration?
- [ ] Are mitigating controls in place?
- [ ] What is the attack surface exposure?
- [ ] What is the potential patient harm?
### Environment Factors
- [ ] Is exploit code publicly available?
- [ ] Is the vulnerability being actively exploited?
- [ ] What is the typical deployment environment?
### Risk Determination
Uncontrolled Risk = Exploitability × Impact × Exposure
| Risk Level | Action |
|------------|--------|
| Unacceptable | Immediate remediation |
| Elevated | Prioritized remediation |
| Acceptable | Monitor, routine update |
```
### Patch and Update Management
**Update Classification:**
| Type | Description | Regulatory Path |
|------|-------------|-----------------|
| Security patch | Addresses vulnerability only | May not require new submission |
| Software update | New features + security | Evaluate per guidance |
| Major upgrade | Significant changes | New 510(k) evaluation |
**FDA's Cybersecurity Policies:**
1. **Routine Updates:** Generally do not require premarket review
2. **Remediation of Vulnerabilities:** No premarket review if:
- No new risks introduced
- No changes to intended use
- Adequate design controls followed
---
## Threat Modeling
### STRIDE Methodology
| Threat | Description | Device Example |
|--------|-------------|----------------|
| **S**poofing | Pretending to be someone/something else | Fake device identity |
| **T**ampering | Modifying data or code | Altering dosage parameters |
| **R**epudiation | Denying actions | Hiding malicious commands |
| **I**nformation Disclosure | Exposing information | PHI data leak |
| **D**enial of Service | Making resource unavailable | Device becomes unresponsive |
| **E**levation of Privilege | Gaining unauthorized access | Admin access from user |
### Threat Model Template
```markdown
## Device Threat Model
### 1. System Description
Device Name: _____________________
Device Type: _____________________
Intended Use: ____________________
### 2. Architecture Diagram
[Include system diagram with trust boundaries]
### 3. Data Flow Diagram
[Document data flows and data types]
### 4. Entry Points
| Entry Point | Protocol | Authentication | Data Type |
|-------------|----------|----------------|-----------|
| USB port | USB HID | None | Config data |
| Network | HTTPS | Certificate | PHI |
| Bluetooth | BLE | Pairing | Commands |
### 5. Assets
| Asset | Sensitivity | Integrity | Availability |
|-------|-------------|-----------|--------------|
| Patient data | High | High | Medium |
| Device firmware | High | Critical | High |
| Configuration | Medium | High | Medium |
### 6. Threat Analysis
| Threat ID | STRIDE | Entry Point | Asset | Mitigation |
|-----------|--------|-------------|-------|------------|
| T-001 | Spoofing | Network | Auth | Mutual TLS |
| T-002 | Tampering | USB | Firmware | Secure boot |
| T-003 | Information | Network | PHI | Encryption |
### 7. Risk Assessment
| Threat | Likelihood | Impact | Risk | Accept/Mitigate |
|--------|------------|--------|------|-----------------|
| T-001 | Medium | High | High | Mitigate |
| T-002 | Low | Critical | High | Mitigate |
| T-003 | Medium | High | High | Mitigate |
```
### Attack Trees
**Example: Unauthorized Access to Device**
```
Goal: Gain Unauthorized Access
├── 1. Physical Access Attack
│ ├── 1.1 Steal device
│ ├── 1.2 Access debug port
│ └── 1.3 Extract storage media
├── 2. Network Attack
│ ├── 2.1 Exploit unpatched vulnerability
│ ├── 2.2 Man-in-the-middle attack
│ └── 2.3 Credential theft
├── 3. Social Engineering
│ ├── 3.1 Phishing for credentials
│ └── 3.2 Insider threat
└── 4. Supply Chain Attack
├── 4.1 Compromised component
└── 4.2 Malicious update
```
---
## Security Controls
### Authentication and Access Control
**Authentication Requirements:**
| Access Level | Authentication | Session Management |
|--------------|----------------|-------------------|
| Patient | PIN/biometric | Auto-logout |
| Clinician | Password + MFA | Timeout 15 min |
| Service | Certificate | Per-session |
| Admin | MFA + approval | Audit logged |
**Password Requirements:**
- Minimum 8 characters (12+ recommended)
- Complexity requirements
- Secure storage (hashed, salted)
- Account lockout after failed attempts
- Forced change on first use
### Encryption Requirements
**Data at Rest:**
- AES-256 for sensitive data
- Secure key storage (TPM, secure enclave)
- Key rotation procedures
**Data in Transit:**
- TLS 1.2 or higher
- Strong cipher suites
- Certificate validation
- Perfect forward secrecy
**Encryption Implementation Checklist:**
```markdown
## Encryption Controls
### Key Management
- [ ] Keys stored in hardware security module or equivalent
- [ ] Key generation uses cryptographically secure RNG
- [ ] Key rotation procedures documented
- [ ] Key revocation procedures documented
- [ ] Key escrow/recovery procedures (if applicable)
### Algorithm Selection
- [ ] AES-256 for symmetric encryption
- [ ] RSA-2048+ or ECDSA P-256+ for asymmetric
- [ ] SHA-256 or better for hashing
- [ ] No deprecated algorithms (MD5, SHA-1, DES)
### Implementation
- [ ] Using well-vetted cryptographic libraries
- [ ] Proper initialization vector handling
- [ ] Protection against timing attacks
- [ ] Secure key zeroing after use
```
### Secure Communications
**Network Security Controls:**
| Layer | Control | Implementation |
|-------|---------|----------------|
| Transport | TLS 1.2+ | Mutual authentication |
| Network | Firewall | Whitelist only |
| Application | API security | Rate limiting, validation |
| Data | Encryption | End-to-end |
### Code Integrity
**Secure Boot Chain:**
```
Root of Trust (Hardware)
↓
Bootloader (Signed)
↓
Operating System (Verified)
↓
Application (Authenticated)
↓
Configuration (Integrity-checked)
```
**Software Integrity Controls:**
- Code signing for all software
- Signature verification before execution
- Anti-rollback protection
- Secure update mechanism
---
## Software Bill of Materials
### SBOM Requirements
**NTIA Minimum Elements:**
1. Supplier name
2. Component name
3. Version of component
4. Other unique identifiers (PURL, CPE)
5. Dependency relationship
6. Author of SBOM data
7. Timestamp
### SBOM Formats
| Format | Standard | Use Case |
|--------|----------|----------|
| SPDX | ISO/IEC 5962:2021 | Comprehensive |
| CycloneDX | OWASP | Security-focused |
| SWID | ISO/IEC 19770-2 | Asset management |
### SBOM Template (CycloneDX)
```xml
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.4">
<metadata>
<timestamp>2024-01-15T00:00:00Z</timestamp>
<tools>
<tool>
<vendor>Manufacturer</vendor>
<name>SBOM Generator</name>
<version>1.0.0</version>
</tool>
</tools>
<component type="device">
<name>Medical Device XYZ</name>
<version>2.0.0</version>
<supplier>
<name>Device Manufacturer</name>
</supplier>
</component>
</metadata>
<components>
<component type="library">
<name>openssl</name>
<version>1.1.1k</version>
<purl>pkg:generic/openssl@1.1.1k</purl>
<licenses>
<license>
<id>Apache-2.0</id>
</license>
</licenses>
</component>
<!-- Additional components -->
</components>
<dependencies>
<dependency ref="device-xyz">
<dependency ref="openssl"/>
</dependency>
</dependencies>
</bom>
```
### SBOM Management Process
```
1. Initial SBOM Creation
└── During development, before submission
2. Vulnerability Monitoring
└── Continuous monitoring against NVD
3. SBOM Updates
└── With each software release
4. Customer Communication
└── SBOM provided on request
5. FDA Submission
└── Included in premarket submission
```
---
## Vulnerability Management
### Vulnerability Disclosure
**Coordinated Vulnerability Disclosure (CVD):**
```markdown
## Vulnerability Disclosure Policy
### Reporting
- Security contact: security@manufacturer.com
- PGP key available at: [URL]
- Bug bounty program: [if applicable]
### Response Timeline
- Acknowledgment: Within 48 hours
- Initial assessment: Within 5 business days
- Status updates: Every 30 days
- Target remediation: Per severity
### Public Disclosure
- Coordinated with reporter
- After remediation available
- Include mitigations if patch delayed
### Safe Harbor
[Statement on not pursuing legal action against good-faith reporters]
```
### Vulnerability Response Process
```
Discovery
↓
Triage (CVSS + Exploitability)
↓
Risk Assessment
↓
Remediation Development
↓
Testing and Validation
↓
Deployment/Communication
↓
Verification
↓
Closure
```
### Customer Communication
**Security Advisory Template:**
```markdown
## Security Advisory
### Advisory ID: [ID]
### Published: [Date]
### Severity: [Critical/High/Medium/Low]
### Affected Products
- Product A, versions 1.0-2.0
- Product B, versions 3.0-3.5
### Description
[Description of vulnerability without exploitation details]
### Impact
[What could happen if exploited]
### Mitigation
[Steps to reduce risk before patch available]
### Remediation
- Patch version: X.X.X
- Download: [URL]
- Installation instructions: [Link]
### Credits
[Acknowledge reporter if agreed]
### References
- CVE-XXXX-XXXX
- Manufacturer reference: [ID]
```
---
## Documentation Requirements
### Premarket Submission Checklist
```markdown
## Cybersecurity Documentation for Premarket Submission
### Device Description (Tier 1 and 2)
- [ ] Cybersecurity risk level justification
- [ ] Global system diagram
- [ ] Data flow diagram
### Security Risk Management (Tier 1 and 2)
- [ ] Threat model
- [ ] Security risk assessment
- [ ] Traceability matrix
### Security Architecture (Tier 1 and 2)
- [ ] Defense-in-depth description
- [ ] Security controls list
- [ ] Trust boundaries identified
### Testing Documentation
#### Tier 1
- [ ] Penetration test report
- [ ] Vulnerability scan results
- [ ] Fuzz testing results
- [ ] Static code analysis
- [ ] Third-party component testing
#### Tier 2
- [ ] Security testing summary
- [ ] Known vulnerability analysis
### SBOM (Tier 1 and 2)
- [ ] Complete component inventory
- [ ] Known vulnerability assessment
- [ ] Support and update plan
### Vulnerability Management (Tier 1 and 2)
- [ ] Vulnerability handling policy
- [ ] Coordinated disclosure process
- [ ] Security update plan
### Labeling (Tier 1 and 2)
- [ ] User security instructions
- [ ] End-of-support date
- [ ] Security contact information
```
### Recommended File Structure
```
Cybersecurity_Documentation/
├── 01_Executive_Summary.pdf
├── 02_Device_Description/
│ ├── System_Diagram.pdf
│ └── Data_Flow_Diagram.pdf
├── 03_Security_Risk_Assessment/
│ ├── Threat_Model.pdf
│ ├── Risk_Assessment.pdf
│ └── Traceability_Matrix.xlsx
├── 04_Security_Architecture/
│ ├── Architecture_Description.pdf
│ ├── Security_Controls.pdf
│ └── Trust_Boundary_Analysis.pdf
├── 05_Security_Testing/
│ ├── Penetration_Test_Report.pdf
│ ├── Vulnerability_Scan_Results.pdf
│ ├── Fuzz_Testing_Report.pdf
│ └── Code_Analysis_Report.pdf
├── 06_SBOM/
│ ├── SBOM.xml (CycloneDX)
│ └── Vulnerability_Analysis.pdf
├── 07_Vulnerability_Management/
│ ├── Vulnerability_Policy.pdf
│ └── Disclosure_Process.pdf
└── 08_Labeling/
└── Security_Instructions.pdf
```
---
## Quick Reference
### Common Cybersecurity Deficiencies
| Deficiency | Resolution |
|------------|------------|
| Incomplete threat model | Document all entry points, assets, threats |
| No SBOM provided | Generate using automated tools |
| Weak authentication | Implement MFA, strong passwords |
| Missing encryption | Add TLS 1.2+, AES-256 |
| No vulnerability management plan | Create monitoring and response procedures |
| Insufficient testing | Conduct penetration testing |
### Security Testing Requirements
| Test Type | Tier 1 | Tier 2 | Tools |
|-----------|--------|--------|-------|
| Penetration testing | Required | Recommended | Manual + automated |
| Vulnerability scanning | Required | Required | Nessus, OpenVAS |
| Fuzz testing | Required | Recommended | AFL, Peach |
| Static analysis | Required | Recommended | SonarQube, Coverity |
| Dynamic analysis | Required | Recommended | Burp Suite, ZAP |
### Recognized Standards Mapping
| FDA Requirement | IEC 62443 | NIST CSF |
|-----------------|-----------|----------|
| Threat modeling | SR 3 | ID.RA |
| Access control | SR 1, SR 2 | PR.AC |
| Encryption | SR 4 | PR.DS |
| Audit logging | SR 6 | PR.PT, DE.AE |
| Patch management | SR 7 | PR.MA |
| Incident response | SR 6 | RS.RP |
FILE:references/fda_capa_requirements.md
# FDA CAPA Requirements
Complete guide to Corrective and Preventive Action requirements per 21 CFR 820.100.
---
## Table of Contents
- [CAPA Regulation Overview](#capa-regulation-overview)
- [CAPA Sources](#capa-sources)
- [CAPA Process](#capa-process)
- [Root Cause Analysis](#root-cause-analysis)
- [Action Implementation](#action-implementation)
- [Effectiveness Verification](#effectiveness-verification)
- [Documentation Requirements](#documentation-requirements)
- [FDA Inspection Focus Areas](#fda-inspection-focus-areas)
---
## CAPA Regulation Overview
### 21 CFR 820.100 Requirements
```
§820.100 Corrective and preventive action
(a) Each manufacturer shall establish and maintain procedures for
implementing corrective and preventive action. The procedures shall
include requirements for:
(1) Analyzing processes, work operations, concessions, quality audit
reports, quality records, service records, complaints, returned
product, and other sources of quality data to identify existing
and potential causes of nonconforming product, or other quality
problems.
(2) Investigating the cause of nonconformities relating to product,
processes, and the quality system.
(3) Identifying the action(s) needed to correct and prevent recurrence
of nonconforming product and other quality problems.
(4) Verifying or validating the corrective and preventive action to
ensure that such action is effective and does not adversely affect
the finished device.
(5) Implementing and recording changes in methods and procedures needed
to correct and prevent identified quality problems.
(6) Ensuring that information related to quality problems or nonconforming
product is disseminated to those directly responsible for assuring
the quality of such product or the prevention of such problems.
(7) Submitting relevant information on identified quality problems, as
well as corrective and preventive actions, for management review.
```
### Definitions
| Term | Definition |
|------|------------|
| **Correction** | Action to eliminate a detected nonconformity |
| **Corrective Action** | Action to eliminate the cause of a detected nonconformity to prevent recurrence |
| **Preventive Action** | Action to eliminate the cause of a potential nonconformity to prevent occurrence |
| **Root Cause** | The fundamental reason for the occurrence of a problem |
| **Effectiveness** | Confirmation that actions achieved intended results |
### CAPA vs. Correction
```
Problem Detected
├── Correction (Immediate)
│ └── Fix the immediate issue
│ Example: Replace defective part
│
└── CAPA (Systemic)
└── Address root cause
Example: Fix process that caused defect
```
---
## CAPA Sources
### Data Sources for CAPA Input
**Internal Sources:**
- Nonconforming product reports (NCRs)
- Internal audit findings
- Process deviations
- Manufacturing data trends
- Equipment failures
- Employee observations
- Training deficiencies
**External Sources:**
- Customer complaints
- Service records
- Returned product
- Regulatory feedback (483s, warning letters)
- Adverse event reports (MDRs)
- Field safety corrective actions
### CAPA Threshold Criteria
**Mandatory CAPA Triggers:**
| Source | Threshold |
|--------|-----------|
| Audit findings | All major/critical findings |
| Customer complaints | Any safety-related |
| NCRs | Recurring (3+ occurrences) |
| Regulatory feedback | All observations |
| MDR/vigilance | All reportable events |
**Discretionary CAPA Evaluation:**
| Source | Consideration |
|--------|---------------|
| Trend data | Statistical significance |
| Process deviations | Impact assessment |
| Minor audit findings | Risk-based |
| Supplier issues | Frequency and severity |
### Trend Analysis
**Statistical Process Control:**
```markdown
## Monthly CAPA Trend Review
### Complaint Trending
- [ ] Complaints by product
- [ ] Complaints by failure mode
- [ ] Geographic distribution
- [ ] Customer type analysis
### NCR Trending
- [ ] NCRs by product/process
- [ ] NCRs by cause code
- [ ] NCRs by supplier
- [ ] Scrap/rework rates
### Threshold Monitoring
| Metric | Threshold | Current | Status |
|--------|-----------|---------|--------|
| Complaints/month | <10 | | |
| NCR rate | <2% | | |
| Recurring issues | 0 | | |
```
---
## CAPA Process
### CAPA Workflow
```
1. Initiation
├── Problem identification
├── Initial assessment
└── CAPA determination
2. Investigation
├── Data collection
├── Root cause analysis
└── Impact assessment
3. Action Planning
├── Correction (if applicable)
├── Corrective action
└── Preventive action
4. Implementation
├── Execute actions
├── Document changes
└── Train affected personnel
5. Verification
├── Verify implementation
├── Validate effectiveness
└── Monitor for recurrence
6. Closure
├── Management approval
├── Final documentation
└── Trend data update
```
### CAPA Form Template
```markdown
## CAPA Record
### Section 1: Identification
CAPA Number: ________________
Initiated By: ________________
Date Initiated: ______________
Priority: ☐ Critical ☐ Major ☐ Minor
Source:
☐ Audit Finding ☐ Complaint ☐ NCR
☐ Service Record ☐ MDR ☐ Trend Data
☐ Regulatory ☐ Other: ____________
### Section 2: Problem Description
Products Affected: _______________________
Processes Affected: _____________________
Quantity/Scope: _________________________
Problem Statement:
[Clear, specific description of the nonconformity or potential problem]
### Section 3: Immediate Correction
Correction Taken: _______________________
Date Completed: _________________________
Verified By: ____________________________
### Section 4: Investigation
Investigation Lead: _____________________
Investigation Start Date: _______________
Data Collected:
☐ Complaint records ☐ Production records
☐ Test data ☐ Training records
☐ Process documentation ☐ Supplier data
Root Cause Analysis Method:
☐ 5 Whys ☐ Fishbone ☐ Fault Tree ☐ Other
Root Cause Statement:
[Specific, factual statement of the root cause]
Contributing Factors:
1. _____________________________________
2. _____________________________________
### Section 5: Action Plan
#### Corrective Actions
| Action | Owner | Target Date | Status |
|--------|-------|-------------|--------|
| | | | |
#### Preventive Actions
| Action | Owner | Target Date | Status |
|--------|-------|-------------|--------|
| | | | |
### Section 6: Verification
Verification Method: ____________________
Verification Criteria: __________________
Verification Date: _____________________
Verified By: ___________________________
Verification Results:
☐ Actions implemented as planned
☐ No adverse effects identified
☐ Documentation updated
### Section 7: Effectiveness Review
Effectiveness Review Date: ______________
Review Period: ________________________
Reviewer: _____________________________
Effectiveness Criteria:
[Specific, measurable criteria for success]
Results:
☐ Effective - problem has not recurred
☐ Not Effective - additional action required
Evidence:
[Reference to data showing effectiveness]
### Section 8: Closure
Closure Date: _________________________
Approved By: __________________________
Management Review Submitted: ☐ Yes ☐ No
Date: ________________________________
```
---
## Root Cause Analysis
### 5 Whys Technique
**Example: Device Fails Final Test**
```
Problem: 5% of devices fail functional test at final inspection
Why 1: Component X is out of tolerance
Why 2: Component X was accepted at incoming inspection
Why 3: Incoming inspection sampling missed defective lot
Why 4: Sampling plan inadequate for component criticality
Why 5: Risk classification of component not updated after design change
Root Cause: Risk classification process did not include design change trigger
```
**5 Whys Template:**
```markdown
## 5 Whys Analysis
Problem Statement: _________________________________
Why 1: _____________________________________________
Evidence: __________________________________________
Why 2: _____________________________________________
Evidence: __________________________________________
Why 3: _____________________________________________
Evidence: __________________________________________
Why 4: _____________________________________________
Evidence: __________________________________________
Why 5: _____________________________________________
Evidence: __________________________________________
Root Cause: ________________________________________
Verification: How do we know this is the root cause?
________________________________________________
```
### Fishbone (Ishikawa) Diagram
**Categories for Medical Device Manufacturing:**
```
┌─────────────────────────────────────────┐
│ PROBLEM │
└─────────────────────────────────────────┘
▲
┌───────────────────────────┼───────────────────────────┐
│ │ │
┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐
│ PERSONNEL │ │ METHODS │ │ MATERIALS │
│ │ │ │ │ │
│ • Training │ │ • SOP gaps │ │ • Supplier │
│ • Skills │ │ • Process │ │ • Specs │
│ • Attention │ │ • Sequence │ │ • Storage │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
└───────────────────────────┼───────────────────────────┘
│
┌──────────────┐ ┌──────┴──────┐ ┌──────────────┐
│ MEASUREMENT │ │ EQUIPMENT │ │ ENVIRONMENT │
│ │ │ │ │ │
│ • Calibration│ │ • Maintenance│ │ • Temperature│
│ • Method │ │ • Capability │ │ • Humidity │
│ • Accuracy │ │ • Tooling │ │ • Cleanliness│
└──────────────┘ └─────────────┘ └──────────────┘
```
### Fault Tree Analysis
**For Complex Failures:**
```
Top Event: Device Failure
│
┌───────────────┼───────────────┐
│ │ │
AND/OR AND/OR AND/OR
│ │ │
┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
│ Component │ │ Software │ │ User │
│ Failure │ │ Failure │ │ Error │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
Basic Events Basic Events Basic Events
```
### Root Cause Categories
| Category | Examples | Evidence Sources |
|----------|----------|------------------|
| Design | Specification error, tolerance stack-up | DHF, design review records |
| Process | Procedure inadequate, sequence error | Process validation, work instructions |
| Personnel | Training gap, human error | Training records, interviews |
| Equipment | Calibration drift, maintenance | Calibration records, logs |
| Material | Supplier quality, storage | Incoming inspection, COCs |
| Environment | Temperature, contamination | Environmental monitoring |
| Management | Resource allocation, priorities | Management review records |
---
## Action Implementation
### Corrective Action Requirements
**Effective Corrective Actions:**
1. Address identified root cause
2. Are specific and measurable
3. Have assigned ownership
4. Have realistic target dates
5. Consider impact on other processes
6. Include verification method
**Action Types:**
| Type | Description | Example |
|------|-------------|---------|
| Process change | Modify procedure or method | Update SOP with additional step |
| Design change | Modify product design | Add tolerance specification |
| Training | Improve personnel capability | Conduct retraining |
| Equipment | Modify or replace equipment | Upgrade inspection equipment |
| Supplier | Address supplier quality | Audit supplier, add requirements |
| Documentation | Improve or add documentation | Create work instruction |
### Change Control Integration
```
CAPA Action Identified
│
▼
Change Request Initiated
│
▼
Impact Assessment
├── Regulatory impact
├── Product impact
├── Process impact
└── Documentation impact
│
▼
Change Approved
│
▼
Implementation
├── Document updates
├── Training
├── Validation (if required)
└── Effective date
│
▼
CAPA Verification
```
### Training Requirements
**When Training is Required:**
- New or revised procedures
- New equipment or tools
- Process changes
- Findings related to personnel performance
**Training Documentation:**
```markdown
## CAPA-Related Training Record
CAPA Number: _______________
Training Subject: ___________
Training Date: ______________
Trainer: ___________________
Attendees:
| Name | Signature | Date |
|------|-----------|------|
| | | |
Training Content:
- [ ] Root cause explanation
- [ ] Process/procedure changes
- [ ] New requirements
- [ ] Competency verification
Competency Verified By: _______________
Date: _______________
```
---
## Effectiveness Verification
### Verification vs. Validation
| Verification | Validation |
|--------------|------------|
| Actions implemented correctly | Actions achieved intended results |
| Short-term check | Long-term monitoring |
| Process-focused | Outcome-focused |
### Effectiveness Criteria
**SMART Criteria:**
- **S**pecific: Clearly defined outcome
- **M**easurable: Quantifiable metrics
- **A**chievable: Realistic expectations
- **R**elevant: Related to root cause
- **T**ime-bound: Defined monitoring period
**Examples:**
| Problem | Root Cause | Action | Effectiveness Criteria |
|---------|------------|--------|----------------------|
| 5% test failures | Inadequate sampling | Increase sampling | <1% failure rate for 3 months |
| Customer complaints | Unclear instructions | Revise IFU | Zero complaints on topic for 6 months |
| NCRs from supplier | No incoming inspection | Add inspection | Zero supplier NCRs for 90 days |
### Effectiveness Review Template
```markdown
## CAPA Effectiveness Review
CAPA Number: _______________
Review Date: _______________
Reviewer: __________________
### Review Criteria
Original Problem: _________________
Effectiveness Metric: ______________
Success Threshold: ________________
Review Period: ____________________
### Data Analysis
| Period | Metric Value | Threshold | Pass/Fail |
|--------|--------------|-----------|-----------|
| Month 1 | | | |
| Month 2 | | | |
| Month 3 | | | |
### Conclusion
☐ Effective - Criteria met, CAPA may be closed
☐ Partially Effective - Additional monitoring required
☐ Not Effective - Additional actions required
### Evidence
[Reference to supporting data: complaint logs, NCR reports, audit results, etc.]
### Next Steps (if not effective)
___________________________________
___________________________________
### Approval
Reviewer Signature: _______________ Date: _______
Quality Approval: _________________ Date: _______
```
### Monitoring Period Guidelines
| CAPA Type | Minimum Monitoring |
|-----------|-------------------|
| Product quality | 3 production lots or 90 days |
| Process | 3 months of production |
| Complaints | 6 months |
| Audit findings | Until next audit |
| Supplier | 3 lots or 90 days |
---
## Documentation Requirements
### CAPA File Contents
```
CAPA File Structure:
├── CAPA Form (all sections completed)
├── Investigation Records
│ ├── Data collected
│ ├── Root cause analysis worksheets
│ └── Impact assessment
├── Action Documentation
│ ├── Action plans
│ ├── Change requests (if applicable)
│ └── Training records
├── Verification Evidence
│ ├── Implementation verification
│ ├── Effectiveness data
│ └── Trend analysis
└── Closure Documentation
├── Closure approval
└── Management review submission
```
### Record Retention
Per 21 CFR 820.180:
- Records shall be retained for the design and expected life of the device
- Minimum of 2 years from date of release for commercial distribution
**CAPA Record Retention:**
- Retain for lifetime of product + 2 years
- Include all supporting documentation
- Maintain audit trail for changes
### Traceability
**Required Traceability:**
- CAPA to source (complaint, NCR, audit finding)
- CAPA to affected products/lots
- CAPA to corrective actions taken
- CAPA to verification evidence
- CAPA to management review
---
## FDA Inspection Focus Areas
### Common 483 Observations
| Observation | Prevention |
|-------------|------------|
| CAPA not initiated when required | Define clear CAPA triggers |
| Root cause analysis inadequate | Use structured RCA methods |
| Actions don't address root cause | Verify action-cause linkage |
| Effectiveness not verified | Define measurable criteria |
| CAPA not timely | Set and track target dates |
| Trend analysis not performed | Implement monthly trending |
| Management review missing CAPA input | Include in management review agenda |
### Inspection Preparation
**CAPA Readiness Checklist:**
```markdown
## FDA Inspection CAPA Preparation
### Documentation Review
- [ ] All CAPAs have complete documentation
- [ ] No overdue CAPAs
- [ ] Root cause documented with evidence
- [ ] Effectiveness verified and documented
- [ ] All open CAPAs have current status
### Metrics Available
- [ ] CAPA by source
- [ ] CAPA cycle time
- [ ] Overdue CAPA trend
- [ ] Effectiveness rate
- [ ] Recurring issues
### Process Evidence
- [ ] CAPA procedure current
- [ ] Training records complete
- [ ] Trend analysis documented
- [ ] Management review records show CAPA input
### Common Questions Prepared
- How do you initiate a CAPA?
- How do you determine root cause?
- How do you verify effectiveness?
- Show me your overdue CAPAs
- Show me CAPAs from complaints
```
### CAPA Metrics Dashboard
| Metric | Target | Calculation |
|--------|--------|-------------|
| On-time initiation | 100% | CAPAs initiated within 30 days |
| On-time closure | >90% | CAPAs closed by target date |
| Effectiveness rate | >85% | Effective at first review / Total |
| Average cycle time | <90 days | Average days to closure |
| Overdue CAPAs | 0 | CAPAs past target date |
| Recurring issues | <5% | Repeat CAPAs / Total |
---
## Quick Reference
### CAPA Decision Tree
```
Quality Issue Identified
│
▼
Is it an isolated incident?
├── YES → Correction only (document, may not need CAPA)
│ Evaluate for trend
│
└── NO → Is it a systemic issue?
├── YES → Initiate CAPA
│ Determine if Corrective or Preventive
│
└── MAYBE → Investigate further
Monitor for recurrence
May escalate to CAPA
```
### Root Cause vs. Symptom
| Symptom (NOT root cause) | Root Cause (Address this) |
|--------------------------|---------------------------|
| "Operator made error" | Training inadequate for task |
| "Component was defective" | Incoming inspection ineffective |
| "SOP not followed" | SOP unclear or impractical |
| "Equipment malfunctioned" | Maintenance schedule inadequate |
| "Supplier shipped wrong part" | Purchasing requirements unclear |
### Action Effectiveness Verification
| Action Type | Verification Method | Timeframe |
|-------------|---------------------|-----------|
| Procedure change | Audit for compliance | 30-60 days |
| Training | Competency assessment | Immediate |
| Design change | Product testing | Per protocol |
| Supplier action | Incoming inspection data | 3 lots |
| Equipment | Calibration/performance | Per schedule |
### Integration with Other Systems
| System | CAPA Integration Point |
|--------|------------------------|
| Complaints | Trigger for CAPA, complaint closure after CAPA |
| NCR | Trend to CAPA, NCR references CAPA |
| Audit | Findings generate CAPA, CAPA closure audit |
| Design Control | Design change via CAPA, DHF update |
| Supplier | Supplier CAPA, supplier audit findings |
| Risk Management | Risk file update post-CAPA |
FILE:references/fda_submission_guide.md
# FDA Submission Guide
Complete framework for 510(k), De Novo, and PMA submissions to the FDA.
---
## Table of Contents
- [Submission Pathway Selection](#submission-pathway-selection)
- [510(k) Premarket Notification](#510k-premarket-notification)
- [De Novo Classification](#de-novo-classification)
- [PMA Premarket Approval](#pma-premarket-approval)
- [Pre-Submission Program](#pre-submission-program)
- [FDA Review Timeline](#fda-review-timeline)
---
## Submission Pathway Selection
### Decision Matrix
```
Is there a legally marketed predicate device?
├── YES → Is your device substantially equivalent?
│ ├── YES → 510(k) Pathway
│ │ ├── No changes from predicate → Abbreviated 510(k)
│ │ ├── Manufacturing changes only → Special 510(k)
│ │ └── Design/performance changes → Traditional 510(k)
│ └── NO → PMA or De Novo
└── NO → Is it a novel low-to-moderate risk device?
├── YES → De Novo Classification Request
└── NO → PMA Pathway (Class III)
```
### Classification Determination
| Class | Risk Level | Pathway | Examples |
|-------|------------|---------|----------|
| I | Low | Exempt or 510(k) | Bandages, stethoscopes |
| II | Moderate | 510(k) | Powered wheelchairs, pregnancy tests |
| III | High | PMA | Pacemakers, heart valves |
### Predicate Device Search
**Database Sources:**
1. FDA 510(k) Database: https://www.accessdata.fda.gov/scripts/cdrh/cfdocs/cfpmn/pmn.cfm
2. FDA Product Classification Database
3. FDA PMA Database
4. FDA De Novo Database
**Search Criteria:**
- Product code (3-letter code)
- Device name keywords
- Intended use similarity
- Technological characteristics
---
## 510(k) Premarket Notification
### Required Sections (21 CFR 807.87)
#### 1. Administrative Information
```
Cover Letter
├── Submission type (Traditional/Special/Abbreviated)
├── Device name and classification
├── Predicate device(s) identification
├── Contact information
└── Signature of authorized representative
CDRH Premarket Review Submission Cover Sheet (FDA Form 3514)
├── Section A: Applicant Information
├── Section B: Device Information
├── Section C: Submission Information
└── Section D: Truth and Accuracy Statement
```
#### 2. Device Description
| Element | Required Content |
|---------|------------------|
| Device Name | Trade name, common name, classification name |
| Intended Use | Disease/condition, patient population, use environment |
| Physical Description | Materials, dimensions, components |
| Principles of Operation | How the device achieves intended use |
| Accessories | Included items, optional components |
| Variants/Models | All versions included in submission |
#### 3. Substantial Equivalence Comparison
```
Comparison Table Format:
┌────────────────────┬─────────────────┬─────────────────┐
│ Characteristic │ Subject Device │ Predicate │
├────────────────────┼─────────────────┼─────────────────┤
│ Intended Use │ [Your device] │ [Predicate] │
│ Technological │ │ │
│ Characteristics │ │ │
│ Performance │ │ │
│ Safety │ │ │
└────────────────────┴─────────────────┴─────────────────┘
Substantial Equivalence Argument:
1. Same intended use? YES/NO
2. Same technological characteristics? YES/NO
3. If different technology, does it raise new safety/effectiveness questions? YES/NO
4. Performance data demonstrates equivalence? YES/NO
```
#### 4. Performance Testing
**Bench Testing:**
- Mechanical/structural testing
- Electrical safety (IEC 60601-1 if applicable)
- Biocompatibility (ISO 10993 series)
- Sterilization validation
- Shelf life/stability testing
- Software verification (IEC 62304 if applicable)
**Clinical Data (if required):**
- Clinical study summaries
- Literature review
- Adverse event data
#### 5. Labeling
**Required Elements:**
- Instructions for Use (IFU)
- Device labeling (package, carton)
- Indications for Use statement
- Contraindications, warnings, precautions
- Advertising materials (if applicable)
### 510(k) Acceptance Checklist
```markdown
## Pre-Submission Verification
- [ ] FDA Form 3514 complete and signed
- [ ] User fee payment ($21,760 for FY2024, small business exemptions available)
- [ ] Device description complete
- [ ] Predicate device identified with 510(k) number
- [ ] Substantial equivalence comparison table
- [ ] Indications for Use statement (FDA Form 3881)
- [ ] Performance data summary
- [ ] Labeling (IFU, device labels)
- [ ] 510(k) summary or statement
- [ ] Truthful and Accuracy statement signed
- [ ] Environmental assessment or categorical exclusion
```
---
## De Novo Classification
### Eligibility Criteria
1. Novel device with no legally marketed predicate
2. Low-to-moderate risk (would be Class I or II if predicate existed)
3. General controls alone (Class I) or with special controls (Class II) provide reasonable assurance of safety and effectiveness
### Required Content
#### Risk Assessment
```
Risk Analysis Requirements:
├── Hazard Identification
│ ├── Biological hazards
│ ├── Mechanical hazards
│ ├── Electrical hazards
│ ├── Use-related hazards
│ └── Cybersecurity hazards (if applicable)
├── Risk Estimation
│ ├── Probability of occurrence
│ ├── Severity of harm
│ └── Risk level (High/Medium/Low)
├── Risk Evaluation
│ ├── Acceptability criteria
│ └── Benefit-risk analysis
└── Risk Control Measures
├── Design controls
├── Protective measures
└── Information for safety
```
#### Proposed Classification
| Classification | Controls | Rationale |
|----------------|----------|-----------|
| Class I | General controls only | Low risk, general controls adequate |
| Class II | General + Special controls | Moderate risk, special controls needed |
#### Special Controls (for Class II)
Define specific controls such as:
- Performance testing requirements
- Labeling requirements
- Post-market surveillance
- Patient registry
- Design specifications
---
## PMA Premarket Approval
### PMA Application Contents
#### Technical Sections
1. **Device Description and Intended Use**
- Detailed design specifications
- Operating principles
- Complete indications for use
2. **Manufacturing Information**
- Manufacturing process description
- Quality system information
- Facility registration
3. **Nonclinical Laboratory Studies**
- Bench testing results
- Animal studies (if applicable)
- Biocompatibility testing
4. **Clinical Investigation**
- IDE number and approval date
- Clinical protocol
- Clinical study results
- Statistical analysis
- Adverse events
5. **Labeling**
- Complete labeling
- Patient labeling (if applicable)
#### Clinical Data Requirements
```
Clinical Study Design:
├── Study Objectives
│ ├── Primary endpoint(s)
│ └── Secondary endpoint(s)
├── Study Population
│ ├── Inclusion criteria
│ ├── Exclusion criteria
│ └── Sample size justification
├── Study Design
│ ├── Randomized controlled trial
│ ├── Single-arm study with OPC
│ └── Other design with justification
├── Statistical Analysis Plan
│ ├── Analysis populations
│ ├── Statistical methods
│ └── Handling of missing data
└── Safety Monitoring
├── Adverse event definitions
├── Stopping rules
└── DSMB oversight
```
### IDE (Investigational Device Exemption)
**When Required:**
- Significant risk device clinical studies
- Studies not exempt under 21 CFR 812.2
**IDE Application Content:**
- Investigational plan
- Manufacturing information
- Investigator agreements
- IRB approvals
- Informed consent forms
- Labeling
- Risk analysis
---
## Pre-Submission Program
### Q-Submission Types
| Type | Purpose | FDA Response |
|------|---------|--------------|
| Pre-Sub | Feedback on planned submission | Written feedback or meeting |
| Informational | Share information, no feedback | Acknowledgment only |
| Study Risk | Determination of study risk level | Risk determination |
| Agreement/Determination | Binding agreement on specific issue | Formal agreement |
### Pre-Sub Meeting Preparation
```
Pre-Submission Package:
1. Cover letter with meeting request
2. Device description
3. Regulatory history (if any)
4. Proposed submission pathway
5. Specific questions (maximum 5-6)
6. Supporting data/information
Meeting Types:
- Written response only (default)
- Teleconference (90 minutes)
- In-person meeting (90 minutes)
```
### Effective Question Formulation
**Good Question Format:**
```
Question: Does FDA agree that [specific proposal] is acceptable for [specific purpose]?
Background: [Brief context - 1-2 paragraphs]
Proposal: [Your specific proposal - detailed but concise]
Rationale: [Why you believe this is appropriate]
```
**Avoid:**
- Open-ended questions ("What should we do?")
- Multiple questions combined
- Questions already answered in guidance
---
## FDA Review Timeline
### Standard Review Times
| Submission Type | FDA Goal | Typical Range |
|----------------|----------|---------------|
| 510(k) Traditional | 90 days | 90-150 days |
| 510(k) Special | 30 days | 30-60 days |
| 510(k) Abbreviated | 30 days | 30-60 days |
| De Novo | 150 days | 150-300 days |
| PMA | 180 days | 12-24 months |
| Pre-Sub Response | 70-75 days | 60-90 days |
### Review Process Stages
```
510(k) Review Timeline:
Day 0: Submission received
Day 1-15: Acceptance review
├── Accept → Substantive review begins
└── Refuse to Accept (RTA) → 180 days to respond
Day 15-90: Substantive review
├── Additional Information (AI) request stops clock
├── Interactive review may occur
└── Decision by Day 90 goal
Decision:
├── Substantially Equivalent (SE) → Clearance letter
├── Not Substantially Equivalent (NSE) → Appeal or new submission
└── Withdrawn
```
### Additional Information Requests
**Response Best Practices:**
- Respond within 30-60 days
- Use FDA's question numbering
- Provide complete responses
- Include amended sections clearly marked
- Reference specific guidance documents
---
## Submission Best Practices
### Document Formatting
- Use PDF format (PDF/A preferred)
- Bookmarks for each section
- Hyperlinks to cross-references
- Table of contents with page numbers
- Consistent headers/footers
### eSTAR (Electronic Submission Template)
FDA's recommended electronic submission format for 510(k):
- Structured data entry
- Built-in validation
- Automatic formatting
- Reduced RTA rate
### Common Refuse to Accept (RTA) Issues
| Issue | Prevention |
|-------|------------|
| Missing user fee | Verify payment before submission |
| Incomplete Form 3514 | Review all fields, ensure signature |
| Missing predicate | Confirm predicate is legally marketed |
| Inadequate device description | Include all models, accessories |
| Missing Indications for Use | Use FDA Form 3881 |
| Incomplete SE comparison | Address all characteristics |
FILE:references/hipaa_compliance_framework.md
# HIPAA Compliance Framework for Medical Devices
Complete guide to HIPAA requirements for medical device manufacturers and software developers.
---
## Table of Contents
- [HIPAA Overview](#hipaa-overview)
- [Privacy Rule Requirements](#privacy-rule-requirements)
- [Security Rule Requirements](#security-rule-requirements)
- [Medical Device Considerations](#medical-device-considerations)
- [Risk Assessment](#risk-assessment)
- [Implementation Specifications](#implementation-specifications)
- [Business Associate Agreements](#business-associate-agreements)
- [Breach Notification](#breach-notification)
---
## HIPAA Overview
### Applicability to Medical Devices
| Entity Type | HIPAA Applicability |
|-------------|---------------------|
| Healthcare providers | Covered Entity (CE) |
| Health plans | Covered Entity (CE) |
| Healthcare clearinghouses | Covered Entity (CE) |
| Device manufacturers | Business Associate (BA) if handling PHI |
| SaMD developers | Business Associate (BA) if handling PHI |
| Cloud service providers | Business Associate (BA) |
### Protected Health Information (PHI)
**PHI Definition:** Individually identifiable health information transmitted or maintained in any form.
**18 HIPAA Identifiers:**
```
1. Names
2. Geographic data (smaller than state)
3. Dates (except year) related to individual
4. Phone numbers
5. Fax numbers
6. Email addresses
7. Social Security numbers
8. Medical record numbers
9. Health plan beneficiary numbers
10. Account numbers
11. Certificate/license numbers
12. Vehicle identifiers
13. Device identifiers and serial numbers
14. Web URLs
15. IP addresses
16. Biometric identifiers
17. Full face photos
18. Any other unique identifying number
```
### Electronic PHI (ePHI)
PHI that is created, stored, transmitted, or received in electronic form. Most relevant for:
- Connected medical devices
- Medical device software (SaMD)
- Mobile health applications
- Cloud-based healthcare systems
---
## Privacy Rule Requirements
### Minimum Necessary Standard
**Principle:** Limit PHI access, use, and disclosure to the minimum necessary to accomplish the intended purpose.
**Implementation:**
- Role-based access controls
- Access audit logging
- Data segmentation
- Need-to-know policies
### Patient Rights
| Right | Device Implication |
|-------|---------------------|
| Access | Provide mechanism to view/export data |
| Amendment | Allow corrections to patient data |
| Accounting of disclosures | Log all PHI disclosures |
| Restriction requests | Support data sharing restrictions |
| Confidential communications | Secure communication channels |
### Use and Disclosure
**Permitted Uses:**
- Treatment, Payment, Healthcare Operations (TPO)
- With patient authorization
- Public health activities
- Required by law
- Health oversight activities
**Medical Device Context:**
- Device data for treatment: Permitted
- Data analytics by manufacturer: Requires BAA or de-identification
- Research use: Requires authorization or IRB waiver
---
## Security Rule Requirements
### Administrative Safeguards
#### Security Management Process (§164.308(a)(1))
**Required Specifications:**
```markdown
## Security Management Process
### Risk Analysis
- [ ] Identify systems with ePHI
- [ ] Document potential threats and vulnerabilities
- [ ] Assess likelihood and impact
- [ ] Document current controls
- [ ] Determine risk levels
### Risk Management
- [ ] Implement security measures
- [ ] Document residual risk
- [ ] Management approval
### Sanction Policy
- [ ] Define workforce sanctions
- [ ] Document enforcement procedures
### Information System Activity Review
- [ ] Define audit procedures
- [ ] Review logs regularly
- [ ] Document findings
```
#### Workforce Security (§164.308(a)(3))
| Specification | Type | Implementation |
|---------------|------|----------------|
| Authorization/supervision | Addressable | Access approval process |
| Workforce clearance | Addressable | Background checks |
| Termination procedures | Addressable | Access revocation |
#### Information Access Management (§164.308(a)(4))
**Access Control Elements:**
- Access authorization
- Access establishment and modification
- Unique user identification
- Automatic logoff
#### Security Awareness and Training (§164.308(a)(5))
**Training Topics:**
- Security reminders
- Protection from malicious software
- Login monitoring
- Password management
#### Security Incident Procedures (§164.308(a)(6))
**Incident Response Requirements:**
1. Identify and document incidents
2. Report security incidents
3. Respond to mitigate harmful effects
4. Document outcomes
#### Contingency Plan (§164.308(a)(7))
```markdown
## Contingency Plan Components
### Data Backup Plan (Required)
- Backup frequency: _____
- Backup verification: _____
- Off-site storage: _____
### Disaster Recovery Plan (Required)
- Recovery time objective: _____
- Recovery point objective: _____
- Recovery procedures: _____
### Emergency Mode Operation (Required)
- Critical functions: _____
- Manual procedures: _____
- Communication plan: _____
### Testing and Revision (Addressable)
- Test frequency: _____
- Last test date: _____
- Revision history: _____
### Applications and Data Criticality (Addressable)
- Critical systems: _____
- Priority recovery order: _____
```
### Physical Safeguards
#### Facility Access Controls (§164.310(a)(1))
| Specification | Type | Implementation |
|---------------|------|----------------|
| Contingency operations | Addressable | Physical access during emergency |
| Facility security plan | Addressable | Physical access policies |
| Access control/validation | Addressable | Visitor management |
| Maintenance records | Addressable | Physical maintenance logs |
#### Workstation Use (§164.310(b))
**Requirements:**
- Policies for workstation use
- Physical environment considerations
- Secure positioning
- Screen privacy
#### Workstation Security (§164.310(c))
**Physical Safeguards:**
- Cable locks
- Restricted areas
- Surveillance
- Clean desk policy
#### Device and Media Controls (§164.310(d)(1))
**Critical for Medical Devices:**
```markdown
## Device and Media Controls
### Disposal (Required)
- [ ] Wipe procedures for devices with ePHI
- [ ] Certificate of destruction
- [ ] Media sanitization per NIST 800-88
### Media Re-use (Required)
- [ ] Sanitization before re-use
- [ ] Verification of removal
- [ ] Documentation
### Accountability (Addressable)
- [ ] Hardware inventory
- [ ] Movement tracking
- [ ] Responsibility assignment
### Data Backup and Storage (Addressable)
- [ ] Retrievable copies
- [ ] Secure storage location
- [ ] Access controls on backup media
```
### Technical Safeguards
#### Access Control (§164.312(a)(1))
| Specification | Type | Implementation |
|---------------|------|----------------|
| Unique user identification | Required | Individual accounts |
| Emergency access | Required | Break-glass procedures |
| Automatic logoff | Addressable | Session timeout |
| Encryption and decryption | Addressable | At-rest encryption |
#### Audit Controls (§164.312(b))
**Audit Log Contents:**
- User identification
- Event type
- Date and time
- Success/failure
- Affected data
**Medical Device Considerations:**
- Log all access to patient data
- Protect logs from tampering
- Retain logs per policy (minimum 6 years)
- Real-time alerting for critical events
#### Integrity (§164.312(c)(1))
**ePHI Integrity Controls:**
- Hash verification
- Digital signatures
- Version control
- Change detection
#### Person or Entity Authentication (§164.312(d))
**Authentication Methods:**
- Passwords (strong requirements)
- Biometrics
- Hardware tokens
- Multi-factor authentication (recommended)
#### Transmission Security (§164.312(e)(1))
| Specification | Type | Implementation |
|---------------|------|----------------|
| Integrity controls | Addressable | TLS, message authentication |
| Encryption | Addressable | TLS 1.2+, AES-256 |
---
## Medical Device Considerations
### Connected Medical Device Security
**Data Flow Analysis:**
```
Device → Local Network → Internet → Cloud → EHR
│ │ │ │ │
└─ ePHI at rest ePHI in transit ePHI at rest
Encrypt Encrypt TLS Encrypt + Access Control
```
### SaMD (Software as a Medical Device)
**HIPAA Requirements for SaMD:**
1. Encryption of stored patient data
2. Secure authentication
3. Audit logging
4. Access controls
5. Secure communication protocols
6. Backup and recovery
7. Incident response
### Mobile Medical Applications
**Additional Considerations:**
- Device loss/theft protection
- Remote wipe capability
- App sandboxing
- Secure data storage
- API security
### Cloud-Based Devices
**Cloud Provider Requirements:**
- BAA with cloud provider
- Data residency (US only for HIPAA)
- Encryption key management
- Audit log access
- Incident notification
---
## Risk Assessment
### HIPAA Risk Assessment Process
```
Step 1: Scope Definition
├── Identify systems with ePHI
├── Document data flows
└── Identify business associates
Step 2: Threat Identification
├── Natural threats (fire, flood)
├── Human threats (hackers, insiders)
├── Environmental threats (power, HVAC)
└── Technical threats (malware, system failure)
Step 3: Vulnerability Assessment
├── Administrative controls
├── Physical controls
├── Technical controls
└── Gap analysis
Step 4: Risk Analysis
├── Likelihood assessment
├── Impact assessment
├── Risk level determination
└── Risk prioritization
Step 5: Risk Treatment
├── Accept
├── Mitigate
├── Transfer
└── Avoid
Step 6: Documentation
├── Risk register
├── Risk management plan
└── Remediation tracking
```
### Risk Assessment Template
```markdown
## HIPAA Risk Assessment
### System Information
System Name: _____________________
System Owner: ____________________
Date: ___________________________
### Asset Inventory
| Asset | ePHI Type | Location | Classification |
|-------|-----------|----------|----------------|
| | | | |
### Threat Analysis
| Threat | Likelihood (1-5) | Impact (1-5) | Risk Score |
|--------|------------------|--------------|------------|
| | | | |
### Vulnerability Assessment
| Safeguard Category | Gap Identified | Severity | Remediation |
|--------------------|----------------|----------|-------------|
| Administrative | | | |
| Physical | | | |
| Technical | | | |
### Risk Treatment Plan
| Risk | Treatment | Owner | Timeline | Status |
|------|-----------|-------|----------|--------|
| | | | | |
### Approval
Risk Assessment Approved: _______________ Date: _______
Next Assessment Due: _______________
```
---
## Implementation Specifications
### Required vs. Addressable
**Required:** Must be implemented as specified
**Addressable:**
1. Implement as specified, OR
2. Implement alternative measure, OR
3. Not implement if not reasonable and appropriate (document rationale)
### Implementation Status Matrix
| Safeguard | Specification | Type | Status | Evidence |
|-----------|---------------|------|--------|----------|
| §164.308(a)(1)(ii)(A) | Risk analysis | R | ☐ | |
| §164.308(a)(1)(ii)(B) | Risk management | R | ☐ | |
| §164.308(a)(3)(ii)(A) | Authorization/supervision | A | ☐ | |
| §164.308(a)(5)(ii)(A) | Security reminders | A | ☐ | |
| §164.310(a)(2)(i) | Contingency operations | A | ☐ | |
| §164.310(d)(2)(i) | Disposal | R | ☐ | |
| §164.312(a)(2)(i) | Unique user ID | R | ☐ | |
| §164.312(a)(2)(ii) | Emergency access | R | ☐ | |
| §164.312(a)(2)(iv) | Encryption (at rest) | A | ☐ | |
| §164.312(e)(2)(ii) | Encryption (transit) | A | ☐ | |
---
## Business Associate Agreements
### When Required
BAA required when business associate:
- Creates, receives, maintains, or transmits PHI
- Provides services involving PHI use/disclosure
### BAA Requirements
**Required Provisions:**
1. Permitted and required uses of PHI
2. Subcontractor requirements
3. Appropriate safeguards
4. Breach notification
5. Termination provisions
6. Return or destruction of PHI
### Medical Device Manufacturer BAA Template
```markdown
## Business Associate Agreement
This Agreement is entered into as of [Date] between:
COVERED ENTITY: [Healthcare Provider/Plan Name]
BUSINESS ASSOCIATE: [Device Manufacturer Name]
### 1. Definitions
[Standard HIPAA definitions]
### 2. Obligations of Business Associate
Business Associate agrees to:
a) Not use or disclose PHI other than as permitted
b) Use appropriate safeguards to prevent improper use/disclosure
c) Report any security incident or breach
d) Ensure subcontractors agree to same restrictions
e) Make PHI available for individual access
f) Make PHI available for amendment
g) Document and make available disclosures
h) Make internal practices available to HHS
i) Return or destroy PHI at termination
### 3. Permitted Uses and Disclosures
Business Associate may:
a) Use PHI for device operation and maintenance
b) Use PHI for quality improvement
c) De-identify PHI per HIPAA standards
d) Create aggregate data
e) Report to FDA as required
### 4. Security Requirements
Business Associate shall implement:
a) Administrative safeguards per §164.308
b) Physical safeguards per §164.310
c) Technical safeguards per §164.312
### 5. Breach Notification
Business Associate shall:
a) Report breaches within [60 days/contractual period]
b) Provide information for breach notification
c) Mitigate harmful effects
### 6. Term and Termination
[Standard termination provisions]
### Signatures
COVERED ENTITY: _________________ Date: _______
BUSINESS ASSOCIATE: _____________ Date: _______
```
---
## Breach Notification
### Breach Definition
**Breach:** Acquisition, access, use, or disclosure of unsecured PHI in a manner not permitted that compromises security or privacy.
**Exceptions:**
1. Unintentional acquisition by workforce member acting in good faith
2. Inadvertent disclosure between authorized persons
3. Good faith belief that unauthorized person couldn't retain information
### Risk Assessment for Breach
**Factors to Consider:**
1. Nature and extent of PHI involved
2. Unauthorized person who received PHI
3. Whether PHI was actually acquired/viewed
4. Extent to which risk has been mitigated
### Notification Requirements
| Audience | Timing | Method |
|----------|--------|--------|
| Individuals | 60 days from discovery | First-class mail or email |
| HHS | 60 days (if >500) | HHS breach portal |
| HHS | Annual (if <500) | Annual report |
| Media | 60 days (if >500 in state) | Prominent media outlet |
### Breach Response Procedure
```markdown
## Breach Response Procedure
### Phase 1: Detection and Containment (Immediate)
- [ ] Identify scope of breach
- [ ] Contain breach (stop ongoing access)
- [ ] Preserve evidence
- [ ] Notify incident response team
- [ ] Document timeline
### Phase 2: Investigation (1-14 days)
- [ ] Determine what PHI was involved
- [ ] Identify affected individuals
- [ ] Assess risk of harm
- [ ] Document investigation findings
### Phase 3: Risk Assessment (15-30 days)
- [ ] Apply four-factor risk assessment
- [ ] Determine if notification required
- [ ] Document decision rationale
### Phase 4: Notification (Within 60 days)
- [ ] Prepare individual notification letters
- [ ] Submit to HHS (if required)
- [ ] Media notification (if required)
- [ ] Retain copies of notifications
### Phase 5: Remediation (Ongoing)
- [ ] Implement corrective actions
- [ ] Update policies and procedures
- [ ] Train workforce
- [ ] Monitor for additional impact
```
### Breach Notification Content
**Individual Notification Must Include:**
1. Description of what happened
2. Types of PHI involved
3. Steps individuals should take
4. What entity is doing to investigate
5. What entity is doing to prevent future breaches
6. Contact information for questions
---
## Compliance Checklist
### Administrative Safeguards Checklist
```markdown
## Administrative Safeguards
- [ ] Security Management Process
- [ ] Risk analysis completed and documented
- [ ] Risk management plan in place
- [ ] Sanction policy documented
- [ ] Information system activity review conducted
- [ ] Assigned Security Responsibility
- [ ] Security Officer designated
- [ ] Contact information documented
- [ ] Workforce Security
- [ ] Authorization procedures
- [ ] Background checks (if applicable)
- [ ] Termination procedures
- [ ] Information Access Management
- [ ] Access authorization policies
- [ ] Access establishment procedures
- [ ] Access modification procedures
- [ ] Security Awareness and Training
- [ ] Training program established
- [ ] Security reminders distributed
- [ ] Protection from malicious software training
- [ ] Password management training
- [ ] Security Incident Procedures
- [ ] Incident response plan
- [ ] Incident documentation procedures
- [ ] Reporting mechanisms
- [ ] Contingency Plan
- [ ] Data backup plan
- [ ] Disaster recovery plan
- [ ] Emergency mode operation plan
- [ ] Testing and revision procedures
```
### Technical Safeguards Checklist
```markdown
## Technical Safeguards
- [ ] Access Control
- [ ] Unique user identification
- [ ] Emergency access procedure
- [ ] Automatic logoff
- [ ] Encryption (at rest)
- [ ] Audit Controls
- [ ] Audit logging implemented
- [ ] Log review procedures
- [ ] Log retention policy
- [ ] Integrity
- [ ] Mechanism to authenticate ePHI
- [ ] Integrity controls in place
- [ ] Authentication
- [ ] Person/entity authentication
- [ ] Strong password policy
- [ ] Transmission Security
- [ ] Integrity controls (in transit)
- [ ] Encryption (TLS 1.2+)
```
---
## Quick Reference
### Common HIPAA Violations
| Violation | Prevention |
|-----------|------------|
| Unauthorized access | Role-based access, MFA |
| Lost/stolen devices | Encryption, remote wipe |
| Improper disposal | NIST 800-88 sanitization |
| Insufficient training | Annual training program |
| Missing BAAs | BA inventory and tracking |
| Insufficient audit logs | Comprehensive logging |
### Penalty Structure
| Tier | Knowledge | Per Violation | Annual Maximum |
|------|-----------|---------------|----------------|
| 1 | Unknown | $100-$50,000 | $1,500,000 |
| 2 | Reasonable cause | $1,000-$50,000 | $1,500,000 |
| 3 | Willful neglect (corrected) | $10,000-$50,000 | $1,500,000 |
| 4 | Willful neglect (not corrected) | $50,000 | $1,500,000 |
### FDA-HIPAA Intersection
| Device Scenario | FDA | HIPAA |
|-----------------|-----|-------|
| Standalone diagnostic | 510(k)/PMA | If transmits PHI |
| Connected insulin pump | Class III PMA | Yes (patient data) |
| Wellness app (no diagnosis) | Exempt | If stores PHI |
| EHR-integrated device | May apply | Yes |
| Research device | IDE | IRB may waive |
FILE:references/qsr_compliance_requirements.md
# Quality System Regulation (QSR) Compliance
Complete guide to 21 CFR Part 820 requirements for medical device manufacturers.
---
## Table of Contents
- [QSR Overview](#qsr-overview)
- [Management Responsibility (820.20)](#management-responsibility-82020)
- [Design Controls (820.30)](#design-controls-82030)
- [Document Controls (820.40)](#document-controls-82040)
- [Purchasing Controls (820.50)](#purchasing-controls-82050)
- [Production and Process Controls (820.70-75)](#production-and-process-controls-82070-75)
- [CAPA (820.100)](#capa-820100)
- [Device Master Record (820.181)](#device-master-record-820181)
- [FDA Inspection Readiness](#fda-inspection-readiness)
---
## QSR Overview
### Applicability
The QSR applies to:
- Finished device manufacturers
- Specification developers
- Initial distributors of imported devices
- Contract manufacturers
- Repackagers and relabelers
### Exemptions
| Device Class | Exemption Status |
|--------------|------------------|
| Class I (most) | Exempt from design controls (820.30) |
| Class I (listed) | Fully exempt from QSR |
| Class II | Full QSR compliance |
| Class III | Full QSR compliance |
### QSR Structure
```
21 CFR Part 820 Subparts:
├── A - General Provisions (820.1-5)
├── B - Quality System Requirements (820.20-25)
├── C - Design Controls (820.30)
├── D - Document Controls (820.40)
├── E - Purchasing Controls (820.50)
├── F - Identification and Traceability (820.60-65)
├── G - Production and Process Controls (820.70-75)
├── H - Acceptance Activities (820.80-86)
├── I - Nonconforming Product (820.90)
├── J - Corrective and Preventive Action (820.100)
├── K - Labeling and Packaging Control (820.120-130)
├── L - Handling, Storage, Distribution, Installation (820.140-170)
├── M - Records (820.180-198)
├── N - Servicing (820.200)
└── O - Statistical Techniques (820.250)
```
---
## Management Responsibility (820.20)
### Quality Policy
**Requirements:**
- Documented quality policy
- Objectives for quality
- Commitment to meeting requirements
- Communicated throughout organization
**Quality Policy Template:**
```markdown
## Quality Policy Statement
[Company Name] is committed to designing, manufacturing, and distributing
medical devices that meet customer requirements and applicable regulatory
standards. We achieve this through:
1. Maintaining an effective Quality Management System
2. Continuous improvement of our processes
3. Compliance with 21 CFR Part 820 and applicable standards
4. Training and empowering employees
5. Supplier quality management
Approved by: _______________ Date: _______________
Management Representative
```
### Organization
| Role | Responsibilities | Documentation |
|------|------------------|---------------|
| Management Representative | QMS oversight, FDA liaison | Org chart, job description |
| Quality Manager | Day-to-day QMS operations | Procedures, authority matrix |
| Design Authority | Design control decisions | DHF sign-offs |
| Production Manager | Manufacturing compliance | Process documentation |
### Management Review
**Frequency:** At least annually (more frequently recommended)
**Required Inputs:**
1. Audit results (internal and external)
2. Customer feedback and complaints
3. Process performance metrics
4. Product conformity data
5. CAPA status
6. Changes affecting QMS
7. Recommendations for improvement
**Required Outputs:**
- Decisions on improvement actions
- Resource needs
- Quality objectives updates
**Management Review Agenda Template:**
```markdown
## Management Review Meeting
Date: _______________
Attendees: _______________
### Agenda Items
1. Review of previous action items
2. Quality objectives and metrics
3. Internal audit results
4. Customer complaints summary
5. CAPA status report
6. Supplier quality performance
7. Regulatory updates
8. Resource requirements
9. Improvement opportunities
### Decisions and Actions
| Item | Decision | Owner | Due Date |
|------|----------|-------|----------|
| | | | |
### Next Review Date: _______________
```
---
## Design Controls (820.30)
### When Required
Design controls are required for:
- Class II devices (most)
- Class III devices (all)
- Class I devices with software
- Class I devices on exemption list exceptions
### Design Control Process Flow
```
Design Input (820.30c)
↓
Design Output (820.30d)
↓
Design Review (820.30e)
↓
Design Verification (820.30f)
↓
Design Validation (820.30g)
↓
Design Transfer (820.30h)
↓
Design Changes (820.30i)
↓
Design History File (820.30j)
```
### Design Input Requirements
**Must Include:**
- Intended use and user requirements
- Patient population
- Performance requirements
- Safety requirements
- Regulatory requirements
- Risk management requirements
**Verification Criteria:**
- Complete (all requirements captured)
- Unambiguous (clear interpretation)
- Not conflicting
- Verifiable or validatable
### Design Output Requirements
| Output Type | Examples | Verification Method |
|-------------|----------|---------------------|
| Device specifications | Drawings, BOMs | Inspection, testing |
| Manufacturing specs | Process parameters | Process validation |
| Software specs | Source code, architecture | Software V&V |
| Labeling | IFU, labels | Review against inputs |
**Essential Requirements:**
- Traceable to design inputs
- Contains acceptance criteria
- Identifies critical characteristics
### Design Review
**Review Stages:**
1. Concept review (feasibility)
2. Design input review (requirements complete)
3. Preliminary design review (architecture)
4. Critical design review (detailed design)
5. Final design review (transfer readiness)
**Participants:**
- Representative of each design function
- Other specialists as needed
- Independent reviewers (no direct design responsibility)
**Documentation:**
- Meeting minutes
- Issues identified
- Resolution actions
- Approval signatures
### Design Verification
**Methods:**
- Inspections and measurements
- Bench testing
- Analysis and calculations
- Simulations
- Comparisons to similar designs
**Verification Matrix Template:**
```markdown
| Req ID | Requirement | Verification Method | Pass Criteria | Result |
|--------|-------------|---------------------|---------------|--------|
| REQ-001 | Dimension tolerance | Measurement | ±0.5mm | |
| REQ-002 | Tensile strength | Testing per ASTM | >500 MPa | |
| REQ-003 | Software function | Unit testing | 100% pass | |
```
### Design Validation
**Definition:** Confirmation that device meets user needs and intended uses
**Validation Requirements:**
- Use initial production units (or equivalent)
- Simulated or actual use conditions
- Includes software validation
**Validation Types:**
1. **Bench validation** - Laboratory simulated use
2. **Clinical validation** - Human subjects (may require IDE)
3. **Usability validation** - Human factors testing
### Design Transfer
**Transfer Checklist:**
```markdown
## Design Transfer Verification
- [ ] DMR complete and approved
- [ ] Manufacturing processes validated
- [ ] Training completed
- [ ] Inspection procedures established
- [ ] Supplier qualifications complete
- [ ] Labeling approved
- [ ] Risk analysis updated
- [ ] Regulatory clearance/approval obtained
```
### Design History File (DHF)
**Contents:**
- Design and development plan
- Design input records
- Design output records
- Design review records
- Design verification records
- Design validation records
- Design transfer records
- Design change records
- Risk management file
---
## Document Controls (820.40)
### Document Approval and Distribution
**Requirements:**
- Documents reviewed and approved before use
- Approved documents available at point of use
- Obsolete documents removed or marked
- Changes reviewed and approved
### Document Control Matrix
| Document Type | Author | Reviewer | Approver | Distribution |
|---------------|--------|----------|----------|--------------|
| SOPs | Process owner | QA | Quality Manager | Controlled |
| Work Instructions | Supervisor | QA | Manager | Controlled |
| Forms | QA | QA | Quality Manager | Controlled |
| Drawings | Engineer | Peer | Design Authority | Controlled |
### Change Control
**Change Request Process:**
```
1. Initiate Change Request
└── Description, justification, impact assessment
2. Technical Review
└── Engineering, quality, regulatory assessment
3. Change Classification
├── Minor: No regulatory impact
├── Moderate: May affect compliance
└── Major: Regulatory submission required
4. Approval
└── Change Control Board (CCB) or designated authority
5. Implementation
└── Training, document updates, inventory actions
6. Verification
└── Confirm change implemented correctly
7. Close Change Request
└── Documentation complete
```
---
## Purchasing Controls (820.50)
### Supplier Qualification
**Qualification Criteria:**
- Quality system capability
- Product/service quality history
- Financial stability
- Regulatory compliance history
**Qualification Methods:**
| Method | When Used | Documentation |
|--------|-----------|---------------|
| On-site audit | Critical suppliers, high risk | Audit report |
| Questionnaire | Initial screening | Completed form |
| Certification review | ISO certified suppliers | Cert copies |
| Product qualification | Incoming inspection data | Test results |
### Approved Supplier List (ASL)
**ASL Requirements:**
- Supplier name and contact
- Products/services approved
- Qualification date and method
- Qualification status
- Re-evaluation schedule
### Purchasing Data
**Purchase Order Requirements:**
- Complete product specifications
- Quality requirements
- Applicable standards
- Inspection/acceptance requirements
- Right of access for verification
---
## Production and Process Controls (820.70-75)
### Process Validation (820.75)
**When Required:**
- Process output cannot be fully verified
- Deficiencies would only appear after use
- Examples: sterilization, welding, molding
**Validation Protocol Elements:**
```markdown
## Process Validation Protocol
### 1. Protocol Approval
Prepared by: _______________ Date: _______________
Approved by: _______________ Date: _______________
### 2. Process Description
[Describe process, equipment, materials, parameters]
### 3. Acceptance Criteria
| Parameter | Specification | Test Method |
|-----------|---------------|-------------|
| | | |
### 4. Equipment Qualification
- IQ (Installation Qualification): _______________
- OQ (Operational Qualification): _______________
- PQ (Performance Qualification): _______________
### 5. Validation Runs
Number of runs: _____ (minimum 3)
Lot sizes: _____
### 6. Results Summary
| Run | Date | Parameters | Results | Pass/Fail |
|-----|------|------------|---------|-----------|
| 1 | | | | |
| 2 | | | | |
| 3 | | | | |
### 7. Conclusion
Process validated: Yes / No
Revalidation triggers: _____
```
### Environmental Controls (820.70(c))
**Controlled Conditions:**
- Temperature and humidity
- Particulate contamination (cleanrooms)
- ESD (electrostatic discharge)
- Lighting levels
**Monitoring Requirements:**
- Continuous or periodic monitoring
- Documented limits
- Out-of-specification procedures
- Calibrated equipment
### Personnel (820.70(d))
**Training Requirements:**
- Job-specific training
- Competency verification
- Retraining for significant changes
- Training records maintained
**Training Record Template:**
```markdown
## Training Record
Employee: _______________ ID: _______________
Position: _______________
| Training Topic | Trainer | Date | Method | Competency Verified |
|----------------|---------|------|--------|---------------------|
| | | | | Signature: ________ |
```
### Equipment (820.70(g))
**Requirements:**
- Maintenance schedule
- Calibration program
- Adjustment limits documented
- Inspection before use
### Calibration (820.72)
**Calibration Program Elements:**
1. Equipment identification
2. Calibration frequency
3. Calibration procedures
4. Accuracy requirements
5. Traceability to NIST standards
6. Out-of-tolerance actions
---
## CAPA (820.100)
### CAPA Sources
- Customer complaints
- Nonconforming product
- Audit findings
- Process monitoring
- Returned products
- MDR/Vigilance reports
- Trend analysis
### CAPA Process
```
1. Identification
└── Problem statement, data collection
2. Investigation
└── Root cause analysis (5 Whys, Fishbone, etc.)
3. Action Determination
├── Correction: Immediate fix
└── Corrective/Preventive: Address root cause
4. Implementation
└── Action execution, documentation
5. Verification
└── Confirm actions completed
6. Effectiveness Review
└── Problem recurrence check (30-90 days)
7. Closure
└── Management approval
```
### Root Cause Analysis Tools
**5 Whys Example:**
```
Problem: Device failed during use
Why 1: Component failed
Why 2: Component was out of specification
Why 3: Incoming inspection did not detect
Why 4: Inspection procedure inadequate
Why 5: Procedure not updated for new component
Root Cause: Document control failure - procedure not updated
```
**Fishbone Categories:**
- Man (People)
- Machine (Equipment)
- Method (Process)
- Material
- Measurement
- Environment
### CAPA Metrics
| Metric | Target | Frequency |
|--------|--------|-----------|
| CAPA on-time closure | >90% | Monthly |
| Overdue CAPAs | <5 | Monthly |
| Effectiveness rate | >85% | Quarterly |
| Average days to closure | <60 | Monthly |
---
## Device Master Record (820.181)
### DMR Contents
```
Device Master Record
├── Device specifications
│ ├── Drawings
│ ├── Composition/formulation
│ └── Component specifications
├── Production process specifications
│ ├── Manufacturing procedures
│ ├── Assembly instructions
│ └── Process parameters
├── Quality assurance procedures
│ ├── Acceptance criteria
│ ├── Inspection procedures
│ └── Test methods
├── Packaging and labeling specifications
│ ├── Package drawings
│ ├── Label content
│ └── IFU content
├── Installation, maintenance, servicing procedures
└── Environmental requirements
```
### Device History Record (DHR) - 820.184
**DHR Contents:**
- Dates of manufacture
- Quantity manufactured
- Quantity released for distribution
- Acceptance records
- Primary identification label
- Device identification and control numbers
### Quality System Record (QSR) - 820.186
**QSR Contents:**
- Procedures and changes
- Calibration records
- Distribution records
- Complaint files
- CAPA records
- Audit reports
---
## FDA Inspection Readiness
### Pre-Inspection Preparation
**30-Day Readiness Checklist:**
```markdown
## FDA Inspection Readiness
### Documentation Review
- [ ] Quality manual current
- [ ] SOPs reviewed and approved
- [ ] Training records complete
- [ ] CAPA files complete
- [ ] Complaint files organized
- [ ] DMR/DHR accessible
- [ ] Management review records current
### Facility Review
- [ ] Controlled areas properly identified
- [ ] Equipment calibration current
- [ ] Environmental monitoring records available
- [ ] Storage conditions appropriate
- [ ] Quarantine areas clearly marked
### Personnel Preparation
- [ ] Escort team identified
- [ ] Subject matter experts briefed
- [ ] Front desk/reception notified
- [ ] Conference room reserved
- [ ] FDA credentials verification process
### Record Accessibility
- [ ] Electronic records accessible
- [ ] Backup copies available
- [ ] Audit trail functional
- [ ] Archive records retrievable
```
### During Inspection
**Escort Guidelines:**
1. One designated escort with investigator at all times
2. Answer questions truthfully and concisely
3. Don't volunteer information not requested
4. Request clarification if question unclear
5. Get help from SME for technical questions
6. Document all requests and commitments
**Record Request Tracking:**
| Request # | Date | Document Requested | Provided By | Date Provided |
|-----------|------|-------------------|-------------|---------------|
| | | | | |
### Post-Inspection
**FDA 483 Response:**
- Due within 15 business days
- Address each observation specifically
- Include corrective actions and timeline
- Provide evidence of completion where possible
**Response Format:**
```markdown
## Observation [Number]
### FDA Observation:
[Copy verbatim from Form 483]
### Company Response:
#### Understanding of Observation:
[Demonstrate understanding of the concern]
#### Immediate Correction:
[Actions already taken]
#### Root Cause Analysis:
[Investigation findings]
#### Corrective Actions:
| Action | Responsible | Target Date | Status |
|--------|-------------|-------------|--------|
| | | | |
#### Preventive Actions:
[Systemic improvements]
#### Verification:
[How effectiveness will be verified]
```
---
## Compliance Metrics Dashboard
### Key Performance Indicators
| Category | Metric | Target | Current |
|----------|--------|--------|---------|
| CAPA | On-time closure rate | >90% | |
| CAPA | Effectiveness rate | >85% | |
| Complaints | Response time (days) | <5 | |
| Training | Compliance rate | 100% | |
| Calibration | On-time rate | 100% | |
| Audit | Findings closure rate | >95% | |
| NCR | Recurring issues | <5% | |
| Supplier | Quality rate | >98% | |
### Trend Analysis
**Monthly Review Items:**
- Complaint trends by product/failure mode
- NCR trends by cause code
- CAPA effectiveness
- Supplier quality
- Production yields
- Customer feedback
---
## Quick Reference
### Common 483 Observations
| Observation | Prevention |
|-------------|------------|
| CAPA not effective | Verify effectiveness before closure |
| Training incomplete | Competency-based training records |
| Document control gaps | Regular procedure reviews |
| Complaint investigation | Thorough, documented investigations |
| Supplier controls weak | Robust qualification and monitoring |
| Validation inadequate | Follow IQ/OQ/PQ protocols |
### Regulatory Cross-References
| QSR Section | ISO 13485 Clause |
|-------------|------------------|
| 820.20 | 5.1, 5.5, 5.6 |
| 820.30 | 7.3 |
| 820.40 | 4.2.4 |
| 820.50 | 7.4 |
| 820.70 | 7.5.1 |
| 820.75 | 7.5.6 |
| 820.100 | 8.5.2, 8.5.3 |
FILE:scripts/fda_submission_tracker.py
#!/usr/bin/env python3
"""
FDA Submission Tracker
Tracks FDA submission status, calculates timelines, and monitors regulatory milestones
for 510(k), De Novo, and PMA submissions.
Usage:
python fda_submission_tracker.py <project_dir>
python fda_submission_tracker.py <project_dir> --type 510k
python fda_submission_tracker.py <project_dir> --json
"""
import argparse
import json
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, List, Optional, Any
# FDA review timeline targets (calendar days)
FDA_TIMELINES = {
"510k_traditional": {
"acceptance_review": 15,
"substantive_review": 90,
"total_goal": 90,
"ai_response": 180 # Days to respond to Additional Information
},
"510k_special": {
"acceptance_review": 15,
"substantive_review": 30,
"total_goal": 30,
"ai_response": 180
},
"510k_abbreviated": {
"acceptance_review": 15,
"substantive_review": 30,
"total_goal": 30,
"ai_response": 180
},
"de_novo": {
"acceptance_review": 60,
"substantive_review": 150,
"total_goal": 150,
"ai_response": 180
},
"pma": {
"acceptance_review": 45,
"substantive_review": 180,
"total_goal": 180,
"ai_response": 180
},
"pma_supplement": {
"acceptance_review": 15,
"substantive_review": 180,
"total_goal": 180,
"ai_response": 180
}
}
# Submission milestones by type
MILESTONES = {
"510k": [
{"id": "predicate_identified", "name": "Predicate Device Identified", "phase": "planning"},
{"id": "testing_complete", "name": "Performance Testing Complete", "phase": "preparation"},
{"id": "documentation_complete", "name": "Submission Documentation Complete", "phase": "preparation"},
{"id": "submission_sent", "name": "Submission Sent to FDA", "phase": "submission"},
{"id": "acknowledgment_received", "name": "FDA Acknowledgment Received", "phase": "review"},
{"id": "acceptance_decision", "name": "Acceptance Review Complete", "phase": "review"},
{"id": "ai_request", "name": "Additional Information Request", "phase": "review", "optional": True},
{"id": "ai_response", "name": "AI Response Submitted", "phase": "review", "optional": True},
{"id": "se_decision", "name": "Substantial Equivalence Decision", "phase": "decision"},
{"id": "clearance_letter", "name": "510(k) Clearance Letter Received", "phase": "decision"}
],
"de_novo": [
{"id": "classification_determined", "name": "Classification Determination", "phase": "planning"},
{"id": "special_controls_defined", "name": "Special Controls Defined", "phase": "preparation"},
{"id": "risk_assessment_complete", "name": "Risk Assessment Complete", "phase": "preparation"},
{"id": "testing_complete", "name": "Performance Testing Complete", "phase": "preparation"},
{"id": "submission_sent", "name": "Submission Sent to FDA", "phase": "submission"},
{"id": "acknowledgment_received", "name": "FDA Acknowledgment Received", "phase": "review"},
{"id": "acceptance_decision", "name": "Acceptance Review Complete", "phase": "review"},
{"id": "ai_request", "name": "Additional Information Request", "phase": "review", "optional": True},
{"id": "ai_response", "name": "AI Response Submitted", "phase": "review", "optional": True},
{"id": "classification_decision", "name": "De Novo Classification Decision", "phase": "decision"}
],
"pma": [
{"id": "ide_approved", "name": "IDE Approval (if required)", "phase": "planning", "optional": True},
{"id": "clinical_complete", "name": "Clinical Study Complete", "phase": "preparation"},
{"id": "clinical_report_complete", "name": "Clinical Study Report Complete", "phase": "preparation"},
{"id": "documentation_complete", "name": "PMA Documentation Complete", "phase": "preparation"},
{"id": "submission_sent", "name": "PMA Submission Sent to FDA", "phase": "submission"},
{"id": "acknowledgment_received", "name": "FDA Acknowledgment Received", "phase": "review"},
{"id": "filing_decision", "name": "Filing Decision", "phase": "review"},
{"id": "ai_request", "name": "Major Deficiency Letter", "phase": "review", "optional": True},
{"id": "ai_response", "name": "Deficiency Response Submitted", "phase": "review", "optional": True},
{"id": "panel_meeting", "name": "Advisory Committee Meeting", "phase": "review", "optional": True},
{"id": "approval_decision", "name": "PMA Approval Decision", "phase": "decision"}
]
}
def find_submission_config(project_dir: Path) -> Optional[Dict]:
"""Find and load submission configuration file."""
config_paths = [
project_dir / "fda_submission.json",
project_dir / "regulatory" / "fda_submission.json",
project_dir / ".fda" / "submission.json"
]
for config_path in config_paths:
if config_path.exists():
try:
with open(config_path) as f:
return json.load(f)
except json.JSONDecodeError:
continue
return None
def calculate_timeline_status(submission_type: str, milestones: Dict[str, str]) -> Dict:
"""Calculate timeline status based on submission type and milestone dates."""
timeline_config = FDA_TIMELINES.get(submission_type, FDA_TIMELINES["510k_traditional"])
result = {
"submission_type": submission_type,
"timeline_config": timeline_config,
"status": "not_started",
"days_elapsed": 0,
"days_remaining": None,
"projected_decision_date": None,
"on_track": None
}
# Check if submission has been sent
if "submission_sent" in milestones:
try:
submission_date = datetime.strptime(milestones["submission_sent"], "%Y-%m-%d")
today = datetime.now()
result["days_elapsed"] = (today - submission_date).days
# Check for AI hold
ai_hold_days = 0
if "ai_request" in milestones and "ai_response" in milestones:
ai_request_date = datetime.strptime(milestones["ai_request"], "%Y-%m-%d")
ai_response_date = datetime.strptime(milestones["ai_response"], "%Y-%m-%d")
ai_hold_days = (ai_response_date - ai_request_date).days
elif "ai_request" in milestones and "ai_response" not in milestones:
ai_request_date = datetime.strptime(milestones["ai_request"], "%Y-%m-%d")
ai_hold_days = (today - ai_request_date).days
result["status"] = "ai_hold"
# Calculate review days (excluding AI hold)
review_days = result["days_elapsed"] - ai_hold_days
# Determine status
if "se_decision" in milestones or "approval_decision" in milestones or "classification_decision" in milestones:
result["status"] = "complete"
elif "acceptance_decision" in milestones:
result["status"] = "substantive_review"
elif "acknowledgment_received" in milestones:
result["status"] = "acceptance_review"
else:
result["status"] = "submitted"
# Calculate projected decision date
if result["status"] not in ["complete", "ai_hold"]:
goal_days = timeline_config["total_goal"]
result["days_remaining"] = max(0, goal_days - review_days)
result["projected_decision_date"] = (submission_date + timedelta(days=goal_days + ai_hold_days)).strftime("%Y-%m-%d")
result["on_track"] = review_days <= goal_days
except ValueError:
pass
return result
def analyze_milestone_status(submission_type: str, completed_milestones: Dict[str, str]) -> List[Dict]:
"""Analyze milestone completion status."""
milestone_list = MILESTONES.get(submission_type.split("_")[0], MILESTONES["510k"])
results = []
for milestone in milestone_list:
status = {
"id": milestone["id"],
"name": milestone["name"],
"phase": milestone["phase"],
"optional": milestone.get("optional", False),
"completed": milestone["id"] in completed_milestones,
"completion_date": completed_milestones.get(milestone["id"])
}
results.append(status)
return results
def calculate_submission_readiness(project_dir: Path, submission_type: str) -> Dict:
"""Check submission readiness by looking for required documentation."""
required_docs = {
"510k": [
{"name": "Device Description", "patterns": ["device_description*", "device_desc*"]},
{"name": "Indications for Use", "patterns": ["indications*", "ifu*"]},
{"name": "Substantial Equivalence", "patterns": ["substantial_equiv*", "se_comparison*", "predicate*"]},
{"name": "Performance Testing", "patterns": ["performance*", "test_report*", "bench_test*"]},
{"name": "Biocompatibility", "patterns": ["biocompat*", "iso_10993*"]},
{"name": "Labeling", "patterns": ["label*", "ifu*", "instructions*"]},
{"name": "Software Documentation", "patterns": ["software*", "iec_62304*"], "optional": True},
{"name": "Sterilization Validation", "patterns": ["steriliz*", "sterility*"], "optional": True}
],
"de_novo": [
{"name": "Device Description", "patterns": ["device_description*", "device_desc*"]},
{"name": "Risk Assessment", "patterns": ["risk*", "hazard*"]},
{"name": "Special Controls", "patterns": ["special_control*"]},
{"name": "Performance Testing", "patterns": ["performance*", "test_report*"]},
{"name": "Labeling", "patterns": ["label*", "ifu*"]}
],
"pma": [
{"name": "Device Description", "patterns": ["device_description*"]},
{"name": "Manufacturing Information", "patterns": ["manufacturing*", "production*"]},
{"name": "Clinical Study Report", "patterns": ["clinical*", "csr*"]},
{"name": "Nonclinical Testing", "patterns": ["nonclinical*", "bench*", "preclinical*"]},
{"name": "Risk Analysis", "patterns": ["risk*", "fmea*"]},
{"name": "Labeling", "patterns": ["label*", "ifu*"]}
]
}
docs_to_check = required_docs.get(submission_type.split("_")[0], required_docs["510k"])
# Search common documentation directories
doc_dirs = [
project_dir / "regulatory",
project_dir / "regulatory" / "fda",
project_dir / "docs",
project_dir / "documentation",
project_dir / "dhf",
project_dir
]
results = []
for doc in docs_to_check:
found = False
found_path = None
for doc_dir in doc_dirs:
if not doc_dir.exists():
continue
for pattern in doc["patterns"]:
matches = list(doc_dir.glob(f"**/{pattern}"))
matches.extend(list(doc_dir.glob(f"**/{pattern.upper()}")))
if matches:
found = True
found_path = str(matches[0].relative_to(project_dir))
break
if found:
break
results.append({
"name": doc["name"],
"required": not doc.get("optional", False),
"found": found,
"path": found_path
})
required_found = sum(1 for r in results if r["required"] and r["found"])
required_total = sum(1 for r in results if r["required"])
return {
"documents": results,
"required_complete": required_found,
"required_total": required_total,
"readiness_percentage": round((required_found / required_total) * 100, 1) if required_total > 0 else 0
}
def generate_sample_config() -> Dict:
"""Generate sample submission configuration."""
return {
"submission_type": "510k_traditional",
"device_name": "Example Medical Device",
"product_code": "ABC",
"predicate_device": {
"name": "Predicate Device Name",
"k_number": "K123456"
},
"milestones": {
"predicate_identified": "2024-01-15",
"testing_complete": "2024-03-01",
"documentation_complete": "2024-03-15"
},
"contacts": {
"regulatory_lead": "Name",
"quality_lead": "Name"
},
"notes": "Add milestone dates as they are completed"
}
def print_text_report(result: Dict) -> None:
"""Print human-readable report."""
print("=" * 60)
print("FDA SUBMISSION TRACKER REPORT")
print("=" * 60)
if "error" in result:
print(f"\nError: {result['error']}")
print(f"\nTo create a configuration file, run with --init")
return
# Basic info
print(f"\nDevice: {result.get('device_name', 'Unknown')}")
print(f"Submission Type: {result['submission_type']}")
print(f"Product Code: {result.get('product_code', 'N/A')}")
# Timeline status
timeline = result["timeline_status"]
print(f"\n--- Timeline Status ---")
print(f"Status: {timeline['status'].upper()}")
print(f"Days Elapsed: {timeline['days_elapsed']}")
if timeline["days_remaining"] is not None:
print(f"Days Remaining (FDA goal): {timeline['days_remaining']}")
if timeline["projected_decision_date"]:
print(f"Projected Decision Date: {timeline['projected_decision_date']}")
if timeline["on_track"] is not None:
status = "ON TRACK" if timeline["on_track"] else "BEHIND SCHEDULE"
print(f"Timeline Status: {status}")
# Milestones
print(f"\n--- Milestones ---")
for ms in result["milestones"]:
status = "[X]" if ms["completed"] else "[ ]"
optional = " (optional)" if ms["optional"] else ""
date = f" - {ms['completion_date']}" if ms["completion_date"] else ""
print(f" {status} {ms['name']}{optional}{date}")
# Readiness
if "readiness" in result:
print(f"\n--- Submission Readiness ---")
readiness = result["readiness"]
print(f"Readiness: {readiness['readiness_percentage']}% ({readiness['required_complete']}/{readiness['required_total']} required docs)")
print("\n Documents:")
for doc in readiness["documents"]:
status = "[X]" if doc["found"] else "[ ]"
req = "(required)" if doc["required"] else "(optional)"
path = f" - {doc['path']}" if doc["path"] else ""
print(f" {status} {doc['name']} {req}{path}")
# Recommendations
if result.get("recommendations"):
print(f"\n--- Recommendations ---")
for i, rec in enumerate(result["recommendations"], 1):
print(f" {i}. {rec}")
print("\n" + "=" * 60)
def generate_recommendations(result: Dict) -> List[str]:
"""Generate actionable recommendations based on status."""
recommendations = []
timeline = result["timeline_status"]
# Timeline recommendations
if timeline["status"] == "ai_hold":
recommendations.append("Priority: Respond to FDA Additional Information request within 180 days")
elif timeline["on_track"] is False:
recommendations.append("Warning: Submission is behind FDA review schedule - consider contacting FDA")
# Milestone recommendations
completed_phases = set()
for ms in result["milestones"]:
if ms["completed"]:
completed_phases.add(ms["phase"])
if "submission" not in completed_phases and "preparation" in completed_phases:
recommendations.append("Ready for submission: Documentation complete, proceed with FDA submission")
# Readiness recommendations
if "readiness" in result:
missing_required = [d for d in result["readiness"]["documents"] if d["required"] and not d["found"]]
if missing_required:
docs = ", ".join(d["name"] for d in missing_required[:3])
recommendations.append(f"Missing required documentation: {docs}")
return recommendations
def analyze_submission(project_dir: Path, submission_type: Optional[str] = None) -> Dict:
"""Main analysis function."""
# Try to find existing configuration
config = find_submission_config(project_dir)
if config is None:
# No config found - do basic analysis
sub_type = submission_type or "510k_traditional"
result = {
"submission_type": sub_type,
"config_found": False,
"timeline_status": calculate_timeline_status(sub_type, {}),
"milestones": analyze_milestone_status(sub_type, {}),
"readiness": calculate_submission_readiness(project_dir, sub_type)
}
else:
# Config found - full analysis
sub_type = config.get("submission_type", submission_type or "510k_traditional")
milestones = config.get("milestones", {})
result = {
"submission_type": sub_type,
"device_name": config.get("device_name"),
"product_code": config.get("product_code"),
"predicate_device": config.get("predicate_device"),
"config_found": True,
"timeline_status": calculate_timeline_status(sub_type, milestones),
"milestones": analyze_milestone_status(sub_type, milestones),
"readiness": calculate_submission_readiness(project_dir, sub_type)
}
# Generate recommendations
result["recommendations"] = generate_recommendations(result)
return result
def main():
parser = argparse.ArgumentParser(
description="FDA Submission Tracker - Monitor 510(k), De Novo, and PMA submissions"
)
parser.add_argument(
"project_dir",
nargs="?",
default=".",
help="Project directory to analyze (default: current directory)"
)
parser.add_argument(
"--type",
choices=["510k", "510k_traditional", "510k_special", "510k_abbreviated",
"de_novo", "pma", "pma_supplement"],
help="Submission type (overrides config file)"
)
parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format"
)
parser.add_argument(
"--init",
action="store_true",
help="Create sample configuration file"
)
args = parser.parse_args()
project_dir = Path(args.project_dir).resolve()
if not project_dir.exists():
print(f"Error: Directory not found: {project_dir}", file=sys.stderr)
sys.exit(1)
if args.init:
config_path = project_dir / "fda_submission.json"
if config_path.exists():
print(f"Configuration file already exists: {config_path}")
sys.exit(1)
sample = generate_sample_config()
if args.type:
sample["submission_type"] = args.type
with open(config_path, "w") as f:
json.dump(sample, f, indent=2)
print(f"Created sample configuration: {config_path}")
print("Edit this file with your submission details and milestone dates.")
return
result = analyze_submission(project_dir, args.type)
if args.json:
print(json.dumps(result, indent=2))
else:
print_text_report(result)
if __name__ == "__main__":
main()
FILE:scripts/hipaa_risk_assessment.py
#!/usr/bin/env python3
"""
HIPAA Risk Assessment Tool
Evaluates HIPAA compliance for medical device software and connected devices
by analyzing code and documentation for security safeguards.
Usage:
python hipaa_risk_assessment.py <project_dir>
python hipaa_risk_assessment.py <project_dir> --category technical
python hipaa_risk_assessment.py <project_dir> --json
"""
import argparse
import json
import os
import re
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Any, Tuple
# HIPAA Security Rule safeguards
HIPAA_SAFEGUARDS = {
"administrative": {
"title": "Administrative Safeguards (§164.308)",
"controls": {
"security_management": {
"title": "Security Management Process",
"requirement": "Risk analysis, risk management, sanction policy",
"doc_patterns": ["risk_assessment*", "security_policy*", "sanction*"],
"code_patterns": [],
"weight": 10
},
"security_officer": {
"title": "Assigned Security Responsibility",
"requirement": "Designated security official",
"doc_patterns": ["security_officer*", "hipaa_officer*", "privacy_officer*"],
"code_patterns": [],
"weight": 5
},
"workforce_security": {
"title": "Workforce Security",
"requirement": "Authorization/supervision, clearance, termination procedures",
"doc_patterns": ["access_control*", "termination*", "hr_security*"],
"code_patterns": [],
"weight": 5
},
"access_management": {
"title": "Information Access Management",
"requirement": "Access authorization, establishment, modification",
"doc_patterns": ["access_management*", "role_definition*", "access_control*"],
"code_patterns": [r"role.*based", r"permission", r"authorization"],
"weight": 8
},
"security_training": {
"title": "Security Awareness and Training",
"requirement": "Training program, security reminders",
"doc_patterns": ["training*", "security_awareness*"],
"code_patterns": [],
"weight": 5
},
"incident_procedures": {
"title": "Security Incident Procedures",
"requirement": "Incident response and reporting",
"doc_patterns": ["incident*", "breach*", "security_event*"],
"code_patterns": [r"incident.*report", r"security.*alert", r"breach.*notify"],
"weight": 8
},
"contingency_plan": {
"title": "Contingency Plan",
"requirement": "Backup, disaster recovery, emergency mode",
"doc_patterns": ["contingency*", "disaster_recovery*", "backup*", "dr_plan*"],
"code_patterns": [r"backup", r"recovery", r"failover"],
"weight": 8
},
"evaluation": {
"title": "Evaluation",
"requirement": "Periodic security evaluations",
"doc_patterns": ["security_audit*", "hipaa_audit*", "compliance_review*"],
"code_patterns": [],
"weight": 5
},
"baa": {
"title": "Business Associate Contracts",
"requirement": "Written contracts with business associates",
"doc_patterns": ["baa*", "business_associate*", "vendor_agreement*"],
"code_patterns": [],
"weight": 5
}
}
},
"physical": {
"title": "Physical Safeguards (§164.310)",
"controls": {
"facility_access": {
"title": "Facility Access Controls",
"requirement": "Physical access procedures and controls",
"doc_patterns": ["facility_access*", "physical_security*", "access_control*"],
"code_patterns": [],
"weight": 5
},
"workstation_use": {
"title": "Workstation Use",
"requirement": "Policies for workstation use and security",
"doc_patterns": ["workstation*", "endpoint*", "device_policy*"],
"code_patterns": [],
"weight": 3
},
"device_media": {
"title": "Device and Media Controls",
"requirement": "Disposal, media re-use, accountability",
"doc_patterns": ["media_disposal*", "device_disposal*", "data_sanitization*"],
"code_patterns": [r"secure.*delete", r"wipe", r"sanitize"],
"weight": 5
}
}
},
"technical": {
"title": "Technical Safeguards (§164.312)",
"controls": {
"access_control": {
"title": "Access Control",
"requirement": "Unique user ID, emergency access, auto logoff, encryption",
"doc_patterns": ["access_control*", "authentication*", "session*"],
"code_patterns": [
r"authentication",
r"authorize",
r"session.*timeout",
r"auto.*logout",
r"unique.*id",
r"user.*id"
],
"weight": 10
},
"audit_controls": {
"title": "Audit Controls",
"requirement": "Record and examine activity in systems with ePHI",
"doc_patterns": ["audit_log*", "access_log*", "security_log*"],
"code_patterns": [
r"audit.*log",
r"access.*log",
r"log.*access",
r"security.*event",
r"logger"
],
"weight": 10
},
"integrity": {
"title": "Integrity Controls",
"requirement": "Mechanism to authenticate ePHI",
"doc_patterns": ["data_integrity*", "checksum*", "hash*"],
"code_patterns": [
r"checksum",
r"hash",
r"hmac",
r"integrity.*check",
r"digital.*signature"
],
"weight": 8
},
"authentication": {
"title": "Person or Entity Authentication",
"requirement": "Verify identity of person or entity seeking access",
"doc_patterns": ["authentication*", "identity*", "mfa*", "2fa*"],
"code_patterns": [
r"authenticate",
r"mfa",
r"two.*factor",
r"2fa",
r"multi.*factor",
r"oauth",
r"jwt"
],
"weight": 10
},
"transmission_security": {
"title": "Transmission Security",
"requirement": "Encryption during transmission",
"doc_patterns": ["encryption*", "tls*", "ssl*", "transport_security*"],
"code_patterns": [
r"https",
r"tls",
r"ssl",
r"encrypt.*transit",
r"secure.*connection"
],
"weight": 10
}
}
}
}
# PHI data patterns to detect in code
PHI_PATTERNS = [
(r"patient.*name", "Patient Name"),
(r"ssn|social.*security", "Social Security Number"),
(r"date.*of.*birth|dob", "Date of Birth"),
(r"medical.*record", "Medical Record Number"),
(r"health.*plan", "Health Plan ID"),
(r"diagnosis|icd.*code", "Diagnosis/ICD Code"),
(r"prescription|medication", "Medication/Prescription"),
(r"insurance", "Insurance Information"),
(r"phone.*number|telephone", "Phone Number"),
(r"email.*address", "Email Address"),
(r"address|street|city|zip", "Physical Address"),
(r"biometric", "Biometric Data")
]
# Security vulnerability patterns (dynamic code execution, hardcoded secrets)
VULNERABILITY_PATTERNS = [
(r"password.*=.*['\"]", "Hardcoded password"),
(r"api.*key.*=.*['\"]", "Hardcoded API key"),
(r"secret.*=.*['\"]", "Hardcoded secret"),
(r"http://(?!localhost)", "Unencrypted HTTP connection"),
(r"verify.*=.*False", "SSL verification disabled"),
(r"dynamic.*code.*execution", "Dynamic code execution risk"),
(r"disable.*ssl", "SSL disabled"),
(r"insecure", "Insecure configuration")
]
def scan_documentation(project_dir: Path, patterns: List[str]) -> List[str]:
"""Scan for documentation matching patterns."""
found = []
doc_dirs = [
project_dir / "docs",
project_dir / "documentation",
project_dir / "policies",
project_dir / "compliance",
project_dir / "hipaa",
project_dir
]
for doc_dir in doc_dirs:
if not doc_dir.exists():
continue
for pattern in patterns:
for ext in ["*.md", "*.pdf", "*.docx", "*.doc", "*.txt"]:
try:
for match in doc_dir.glob(f"**/{pattern}{ext}"):
rel_path = str(match.relative_to(project_dir))
if rel_path not in found:
found.append(rel_path)
except Exception:
continue
return found
def scan_code_patterns(project_dir: Path, patterns: List[str]) -> List[Dict]:
"""Scan source code for patterns."""
matches = []
code_extensions = ["*.py", "*.js", "*.ts", "*.java", "*.cs", "*.go", "*.rb"]
src_dirs = [
project_dir / "src",
project_dir / "app",
project_dir / "lib",
project_dir
]
for src_dir in src_dirs:
if not src_dir.exists():
continue
for ext in code_extensions:
try:
for file_path in src_dir.glob(f"**/{ext}"):
# Skip node_modules, venv, etc.
if any(skip in str(file_path) for skip in ["node_modules", "venv", ".venv", "__pycache__", ".git"]):
continue
try:
content = file_path.read_text(encoding='utf-8', errors='ignore')
for pattern in patterns:
if re.search(pattern, content, re.IGNORECASE):
rel_path = str(file_path.relative_to(project_dir))
matches.append({
"file": rel_path,
"pattern": pattern
})
break # One match per file per control is enough
except Exception:
continue
except Exception:
continue
return matches
def detect_phi_handling(project_dir: Path) -> Dict:
"""Detect potential PHI handling in code."""
phi_found = []
code_extensions = ["*.py", "*.js", "*.ts", "*.java", "*.cs", "*.go"]
for ext in code_extensions:
try:
for file_path in project_dir.glob(f"**/{ext}"):
if any(skip in str(file_path) for skip in ["node_modules", "venv", ".venv", "__pycache__", ".git"]):
continue
try:
content = file_path.read_text(encoding='utf-8', errors='ignore')
rel_path = str(file_path.relative_to(project_dir))
for pattern, phi_type in PHI_PATTERNS:
if re.search(pattern, content, re.IGNORECASE):
phi_found.append({
"file": rel_path,
"phi_type": phi_type
})
break
except Exception:
continue
except Exception:
continue
return {
"phi_detected": len(phi_found) > 0,
"files_with_phi": phi_found,
"phi_types": list(set(p["phi_type"] for p in phi_found))
}
def detect_security_vulnerabilities(project_dir: Path) -> List[Dict]:
"""Scan for security vulnerabilities."""
vulnerabilities = []
code_extensions = ["*.py", "*.js", "*.ts", "*.java", "*.cs", "*.go", "*.yaml", "*.yml", "*.json"]
for ext in code_extensions:
try:
for file_path in project_dir.glob(f"**/{ext}"):
if any(skip in str(file_path) for skip in ["node_modules", "venv", ".venv", "__pycache__", ".git"]):
continue
try:
content = file_path.read_text(encoding='utf-8', errors='ignore')
rel_path = str(file_path.relative_to(project_dir))
for pattern, vuln_type in VULNERABILITY_PATTERNS:
matches = re.findall(pattern, content, re.IGNORECASE)
if matches:
vulnerabilities.append({
"file": rel_path,
"vulnerability": vuln_type,
"count": len(matches)
})
except Exception:
continue
except Exception:
continue
return vulnerabilities
def assess_control(project_dir: Path, control_id: str, control_data: Dict) -> Dict:
"""Assess a single HIPAA control."""
doc_evidence = scan_documentation(project_dir, control_data["doc_patterns"])
code_evidence = scan_code_patterns(project_dir, control_data["code_patterns"]) if control_data["code_patterns"] else []
# Determine compliance status
has_docs = len(doc_evidence) > 0
has_code = len(code_evidence) > 0
if has_docs and (has_code or not control_data["code_patterns"]):
status = "implemented"
score = 100
elif has_docs or has_code:
status = "partial"
score = 50
else:
status = "gap"
score = 0
return {
"control_id": control_id,
"title": control_data["title"],
"requirement": control_data["requirement"],
"status": status,
"score": score,
"weight": control_data["weight"],
"weighted_score": (score * control_data["weight"]) / 100,
"documentation": doc_evidence,
"code_evidence": [e["file"] for e in code_evidence]
}
def assess_category(project_dir: Path, category_id: str, category_data: Dict) -> Dict:
"""Assess a HIPAA safeguard category."""
control_results = []
total_weight = 0
weighted_score = 0
for control_id, control_data in category_data["controls"].items():
result = assess_control(project_dir, control_id, control_data)
control_results.append(result)
total_weight += control_data["weight"]
weighted_score += result["weighted_score"]
category_score = round((weighted_score / total_weight) * 100, 1) if total_weight > 0 else 0
return {
"category": category_id,
"title": category_data["title"],
"score": category_score,
"controls": control_results,
"compliant": sum(1 for c in control_results if c["status"] == "implemented"),
"partial": sum(1 for c in control_results if c["status"] == "partial"),
"gaps": sum(1 for c in control_results if c["status"] == "gap")
}
def calculate_risk_level(overall_score: float, vulnerabilities: List[Dict], phi_data: Dict) -> Dict:
"""Calculate overall HIPAA risk level."""
# Base risk from compliance score
if overall_score >= 80:
base_risk = "LOW"
base_score = 1
elif overall_score >= 60:
base_risk = "MEDIUM"
base_score = 2
elif overall_score >= 40:
base_risk = "HIGH"
base_score = 3
else:
base_risk = "CRITICAL"
base_score = 4
# Adjust for vulnerabilities
critical_vulns = sum(1 for v in vulnerabilities if "password" in v["vulnerability"].lower() or "secret" in v["vulnerability"].lower())
if critical_vulns > 0:
base_score = min(4, base_score + 1)
# Adjust for PHI handling
if phi_data["phi_detected"] and base_score < 4:
base_score = min(4, base_score + 0.5)
# Map back to risk level
risk_levels = {1: "LOW", 2: "MEDIUM", 3: "HIGH", 4: "CRITICAL"}
final_risk = risk_levels.get(int(base_score), "HIGH")
return {
"risk_level": final_risk,
"compliance_score": overall_score,
"vulnerability_count": len(vulnerabilities),
"phi_handling_detected": phi_data["phi_detected"]
}
def generate_recommendations(assessment: Dict) -> List[str]:
"""Generate prioritized recommendations."""
recommendations = []
# Technical safeguards first (highest priority for software)
for cat in assessment["categories"]:
if cat["category"] == "technical":
for control in cat["controls"]:
if control["status"] == "gap":
recommendations.append(f"CRITICAL: Implement {control['title']} - {control['requirement']}")
elif control["status"] == "partial":
recommendations.append(f"HIGH: Complete {control['title']} implementation")
# Administrative safeguards
for cat in assessment["categories"]:
if cat["category"] == "administrative":
for control in cat["controls"]:
if control["status"] == "gap":
recommendations.append(f"MEDIUM: Document {control['title']} procedures")
# Vulnerabilities
for vuln in assessment.get("vulnerabilities", [])[:5]:
recommendations.append(f"SECURITY: Fix {vuln['vulnerability']} in {vuln['file']}")
return recommendations[:10] # Top 10
def print_text_report(result: Dict) -> None:
"""Print human-readable report."""
print("=" * 70)
print("HIPAA SECURITY RULE COMPLIANCE ASSESSMENT")
print("=" * 70)
# Risk summary
risk = result["risk_assessment"]
print(f"\nRISK LEVEL: {risk['risk_level']}")
print(f"Compliance Score: {risk['compliance_score']}%")
print(f"Vulnerabilities Found: {risk['vulnerability_count']}")
print(f"PHI Handling Detected: {'Yes' if risk['phi_handling_detected'] else 'No'}")
# Category scores
print("\n--- SAFEGUARD CATEGORIES ---")
for cat in result["categories"]:
status = "OK" if cat["score"] >= 70 else "NEEDS ATTENTION"
print(f" {cat['title']}: {cat['score']}% [{status}]")
print(f" Implemented: {cat['compliant']}, Partial: {cat['partial']}, Gaps: {cat['gaps']}")
# Gaps
print("\n--- COMPLIANCE GAPS ---")
gap_count = 0
for cat in result["categories"]:
for control in cat["controls"]:
if control["status"] == "gap":
gap_count += 1
print(f" [{cat['category'].upper()}] {control['title']}")
print(f" Requirement: {control['requirement']}")
if gap_count == 0:
print(" No critical gaps identified")
# PHI Detection
if result["phi_detection"]["phi_detected"]:
print("\n--- PHI HANDLING DETECTED ---")
print(f" PHI Types: {', '.join(result['phi_detection']['phi_types'])}")
print(f" Files: {len(result['phi_detection']['files_with_phi'])}")
# Vulnerabilities
if result["vulnerabilities"]:
print("\n--- SECURITY VULNERABILITIES ---")
for vuln in result["vulnerabilities"][:10]:
print(f" - {vuln['vulnerability']}: {vuln['file']}")
# Recommendations
if result["recommendations"]:
print("\n--- RECOMMENDATIONS ---")
for i, rec in enumerate(result["recommendations"], 1):
print(f" {i}. {rec}")
print("\n" + "=" * 70)
print(f"Assessment Date: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 70)
def main():
parser = argparse.ArgumentParser(
description="HIPAA Risk Assessment Tool for Medical Device Software"
)
parser.add_argument(
"project_dir",
nargs="?",
default=".",
help="Project directory to analyze (default: current directory)"
)
parser.add_argument(
"--category",
choices=["administrative", "physical", "technical"],
help="Assess specific safeguard category only"
)
parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format"
)
parser.add_argument(
"--detailed",
action="store_true",
help="Include detailed evidence in output"
)
args = parser.parse_args()
project_dir = Path(args.project_dir).resolve()
if not project_dir.exists():
print(f"Error: Directory not found: {project_dir}", file=sys.stderr)
sys.exit(1)
# Filter categories if specific one requested
categories_to_assess = HIPAA_SAFEGUARDS
if args.category:
categories_to_assess = {args.category: HIPAA_SAFEGUARDS[args.category]}
# Perform assessment
category_results = []
total_weight = 0
weighted_score = 0
for cat_id, cat_data in categories_to_assess.items():
cat_result = assess_category(project_dir, cat_id, cat_data)
category_results.append(cat_result)
# Calculate weighted average
cat_weight = sum(c["weight"] for c in cat_data["controls"].values())
total_weight += cat_weight
weighted_score += (cat_result["score"] * cat_weight) / 100
overall_score = round((weighted_score / total_weight) * 100, 1) if total_weight > 0 else 0
# Additional scans
phi_detection = detect_phi_handling(project_dir)
vulnerabilities = detect_security_vulnerabilities(project_dir)
# Risk assessment
risk_assessment = calculate_risk_level(overall_score, vulnerabilities, phi_detection)
result = {
"project_dir": str(project_dir),
"assessment_date": datetime.now().isoformat(),
"overall_score": overall_score,
"risk_assessment": risk_assessment,
"categories": category_results if args.detailed else [
{
"category": c["category"],
"title": c["title"],
"score": c["score"],
"compliant": c["compliant"],
"partial": c["partial"],
"gaps": c["gaps"]
}
for c in category_results
],
"phi_detection": phi_detection,
"vulnerabilities": vulnerabilities,
"recommendations": []
}
result["recommendations"] = generate_recommendations(result)
if args.json:
print(json.dumps(result, indent=2))
else:
print_text_report(result)
if __name__ == "__main__":
main()
FILE:scripts/qsr_compliance_checker.py
#!/usr/bin/env python3
"""
QSR Compliance Checker
Assesses compliance with 21 CFR Part 820 (Quality System Regulation) by analyzing
project documentation and identifying gaps.
Usage:
python qsr_compliance_checker.py <project_dir>
python qsr_compliance_checker.py <project_dir> --section 820.30
python qsr_compliance_checker.py <project_dir> --json
"""
import argparse
import json
import os
import re
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Any
# QSR sections and requirements
QSR_REQUIREMENTS = {
"820.20": {
"title": "Management Responsibility",
"subsections": {
"820.20(a)": {
"title": "Quality Policy",
"required_evidence": ["quality_policy", "quality_manual", "quality_objectives"],
"doc_patterns": ["quality_policy*", "quality_manual*", "qms_manual*"],
"keywords": ["quality policy", "quality objectives", "management commitment"]
},
"820.20(b)": {
"title": "Organization",
"required_evidence": ["org_chart", "job_descriptions", "authority_matrix"],
"doc_patterns": ["org_chart*", "organization*", "job_desc*", "authority*"],
"keywords": ["organizational structure", "responsibility", "authority"]
},
"820.20(c)": {
"title": "Management Review",
"required_evidence": ["management_review_procedure", "management_review_records"],
"doc_patterns": ["management_review*", "mgmt_review*", "qmr*"],
"keywords": ["management review", "review meeting", "quality system effectiveness"]
}
}
},
"820.30": {
"title": "Design Controls",
"subsections": {
"820.30(a)": {
"title": "Design and Development Planning",
"required_evidence": ["design_plan", "development_plan"],
"doc_patterns": ["design_plan*", "dev_plan*", "development_plan*"],
"keywords": ["design planning", "development phases", "design milestones"]
},
"820.30(b)": {
"title": "Design Input",
"required_evidence": ["design_input", "requirements_specification"],
"doc_patterns": ["design_input*", "requirement*", "srs*", "prs*"],
"keywords": ["design input", "requirements", "user needs", "intended use"]
},
"820.30(c)": {
"title": "Design Output",
"required_evidence": ["design_output", "specifications", "drawings"],
"doc_patterns": ["design_output*", "specification*", "drawing*", "bom*"],
"keywords": ["design output", "specifications", "acceptance criteria"]
},
"820.30(d)": {
"title": "Design Review",
"required_evidence": ["design_review_procedure", "design_review_records"],
"doc_patterns": ["design_review*", "dr_record*", "dr_minutes*"],
"keywords": ["design review", "review meeting", "design evaluation"]
},
"820.30(e)": {
"title": "Design Verification",
"required_evidence": ["verification_plan", "verification_results"],
"doc_patterns": ["verification*", "test_report*", "dv_*"],
"keywords": ["verification", "testing", "design verification"]
},
"820.30(f)": {
"title": "Design Validation",
"required_evidence": ["validation_plan", "validation_results"],
"doc_patterns": ["validation*", "clinical*", "usability*", "val_*"],
"keywords": ["validation", "user needs", "intended use", "clinical evaluation"]
},
"820.30(g)": {
"title": "Design Transfer",
"required_evidence": ["transfer_checklist", "transfer_verification"],
"doc_patterns": ["transfer*", "production_release*"],
"keywords": ["design transfer", "manufacturing", "production"]
},
"820.30(h)": {
"title": "Design Changes",
"required_evidence": ["change_control_procedure", "change_records"],
"doc_patterns": ["change_control*", "ecn*", "eco*", "dcr*"],
"keywords": ["design change", "change control", "modification"]
},
"820.30(i)": {
"title": "Design History File",
"required_evidence": ["dhf_index", "dhf"],
"doc_patterns": ["dhf*", "design_history*"],
"keywords": ["design history file", "DHF", "design records"]
}
}
},
"820.40": {
"title": "Document Controls",
"subsections": {
"820.40(a)": {
"title": "Document Approval and Distribution",
"required_evidence": ["document_control_procedure"],
"doc_patterns": ["document_control*", "doc_control*", "sop_document*"],
"keywords": ["document approval", "document distribution", "controlled documents"]
},
"820.40(b)": {
"title": "Document Changes",
"required_evidence": ["document_change_procedure", "revision_history"],
"doc_patterns": ["revision_history*", "document_change*"],
"keywords": ["document change", "revision", "document modification"]
}
}
},
"820.50": {
"title": "Purchasing Controls",
"subsections": {
"820.50(a)": {
"title": "Evaluation of Suppliers",
"required_evidence": ["supplier_qualification_procedure", "approved_supplier_list"],
"doc_patterns": ["supplier*", "asl*", "vendor*"],
"keywords": ["supplier evaluation", "approved supplier", "vendor qualification"]
},
"820.50(b)": {
"title": "Purchasing Data",
"required_evidence": ["purchasing_procedure", "purchase_order_requirements"],
"doc_patterns": ["purchas*", "procurement*"],
"keywords": ["purchasing data", "specifications", "quality requirements"]
}
}
},
"820.70": {
"title": "Production and Process Controls",
"subsections": {
"820.70(a)": {
"title": "General Process Controls",
"required_evidence": ["manufacturing_procedures", "work_instructions"],
"doc_patterns": ["manufacturing*", "production*", "work_instruction*", "wi_*"],
"keywords": ["manufacturing process", "production", "process parameters"]
},
"820.70(b)": {
"title": "Production and Process Changes",
"required_evidence": ["process_change_procedure"],
"doc_patterns": ["process_change*", "manufacturing_change*"],
"keywords": ["process change", "production change", "change control"]
},
"820.70(c)": {
"title": "Environmental Control",
"required_evidence": ["environmental_control_procedure", "monitoring_records"],
"doc_patterns": ["environmental*", "cleanroom*", "env_monitoring*"],
"keywords": ["environmental control", "cleanroom", "contamination"]
},
"820.70(d)": {
"title": "Personnel",
"required_evidence": ["training_procedure", "training_records"],
"doc_patterns": ["training*", "personnel*", "competency*"],
"keywords": ["training", "personnel qualification", "competency"]
},
"820.70(e)": {
"title": "Contamination Control",
"required_evidence": ["contamination_control_procedure"],
"doc_patterns": ["contamination*", "cleaning*", "hygiene*"],
"keywords": ["contamination", "cleaning", "hygiene"]
},
"820.70(f)": {
"title": "Buildings",
"required_evidence": ["facility_requirements"],
"doc_patterns": ["facility*", "building*"],
"keywords": ["facility", "buildings", "manufacturing area"]
},
"820.70(g)": {
"title": "Equipment",
"required_evidence": ["equipment_maintenance_procedure", "maintenance_records"],
"doc_patterns": ["equipment*", "maintenance*", "preventive_maintenance*"],
"keywords": ["equipment", "maintenance", "calibration"]
},
"820.70(h)": {
"title": "Manufacturing Material",
"required_evidence": ["material_handling_procedure"],
"doc_patterns": ["material*", "handling*", "storage*"],
"keywords": ["manufacturing material", "handling", "storage"]
},
"820.70(i)": {
"title": "Automated Processes",
"required_evidence": ["software_validation", "automated_process_validation"],
"doc_patterns": ["software_val*", "csv*", "automation*"],
"keywords": ["software validation", "automated", "computer system"]
}
}
},
"820.72": {
"title": "Inspection, Measuring, and Test Equipment",
"subsections": {
"820.72(a)": {
"title": "Calibration",
"required_evidence": ["calibration_procedure", "calibration_records"],
"doc_patterns": ["calibration*", "cal_*"],
"keywords": ["calibration", "accuracy", "measurement"]
},
"820.72(b)": {
"title": "Calibration Standards",
"required_evidence": ["calibration_standards", "traceability_records"],
"doc_patterns": ["calibration_standard*", "nist*", "traceability*"],
"keywords": ["calibration standards", "NIST", "traceability"]
}
}
},
"820.75": {
"title": "Process Validation",
"subsections": {
"820.75(a)": {
"title": "Process Validation Requirements",
"required_evidence": ["process_validation_procedure", "validation_protocols"],
"doc_patterns": ["process_validation*", "pv_*", "validation_protocol*"],
"keywords": ["process validation", "IQ", "OQ", "PQ"]
},
"820.75(b)": {
"title": "Validation Monitoring",
"required_evidence": ["validation_monitoring", "revalidation_criteria"],
"doc_patterns": ["revalidation*", "validation_monitoring*"],
"keywords": ["monitoring", "revalidation", "process performance"]
}
}
},
"820.90": {
"title": "Nonconforming Product",
"subsections": {
"820.90(a)": {
"title": "Nonconforming Product Control",
"required_evidence": ["ncr_procedure", "nonconforming_records"],
"doc_patterns": ["ncr*", "nonconform*", "nc_*"],
"keywords": ["nonconforming", "NCR", "disposition"]
},
"820.90(b)": {
"title": "Nonconformance Review",
"required_evidence": ["ncr_review_procedure"],
"doc_patterns": ["ncr_review*", "mrb*"],
"keywords": ["review", "disposition", "concession"]
}
}
},
"820.100": {
"title": "Corrective and Preventive Action",
"subsections": {
"820.100(a)": {
"title": "CAPA Procedure",
"required_evidence": ["capa_procedure", "capa_records"],
"doc_patterns": ["capa*", "corrective*", "preventive*"],
"keywords": ["CAPA", "corrective action", "preventive action", "root cause"]
}
}
},
"820.120": {
"title": "Device Labeling",
"subsections": {
"820.120": {
"title": "Labeling Controls",
"required_evidence": ["labeling_procedure", "label_inspection"],
"doc_patterns": ["label*", "labeling*"],
"keywords": ["labeling", "label inspection", "UDI"]
}
}
},
"820.180": {
"title": "General Requirements - Records",
"subsections": {
"820.180": {
"title": "Records Requirements",
"required_evidence": ["records_management_procedure", "retention_schedule"],
"doc_patterns": ["record*", "retention*", "archive*"],
"keywords": ["records", "retention", "archive", "backup"]
}
}
},
"820.181": {
"title": "Device Master Record",
"subsections": {
"820.181": {
"title": "DMR Contents",
"required_evidence": ["dmr_index", "dmr"],
"doc_patterns": ["dmr*", "device_master*"],
"keywords": ["device master record", "DMR", "specifications"]
}
}
},
"820.184": {
"title": "Device History Record",
"subsections": {
"820.184": {
"title": "DHR Contents",
"required_evidence": ["dhr_template", "dhr_records"],
"doc_patterns": ["dhr*", "device_history*", "batch_record*"],
"keywords": ["device history record", "DHR", "production record"]
}
}
},
"820.198": {
"title": "Complaint Files",
"subsections": {
"820.198": {
"title": "Complaint Handling",
"required_evidence": ["complaint_procedure", "complaint_records"],
"doc_patterns": ["complaint*", "customer_feedback*"],
"keywords": ["complaint", "customer feedback", "MDR"]
}
}
}
}
def search_documentation(project_dir: Path, patterns: List[str], keywords: List[str]) -> Dict:
"""Search for documentation matching patterns and keywords."""
result = {
"documents_found": [],
"keyword_matches": [],
"evidence_strength": "none"
}
# Common documentation directories
doc_dirs = [
project_dir / "qms",
project_dir / "quality",
project_dir / "docs",
project_dir / "documentation",
project_dir / "procedures",
project_dir / "sops",
project_dir / "dhf",
project_dir / "dmr",
project_dir
]
# Search for document patterns
for doc_dir in doc_dirs:
if not doc_dir.exists():
continue
for pattern in patterns:
for ext in ["*.md", "*.pdf", "*.docx", "*.doc", "*.txt"]:
full_pattern = f"**/{pattern}{ext}" if not pattern.endswith("*") else f"**/{pattern[:-1]}{ext}"
try:
matches = list(doc_dir.glob(full_pattern))
for match in matches:
rel_path = str(match.relative_to(project_dir))
if rel_path not in result["documents_found"]:
result["documents_found"].append(rel_path)
except Exception:
continue
# Search for keywords in markdown and text files
for doc_dir in doc_dirs:
if not doc_dir.exists():
continue
for ext in ["*.md", "*.txt"]:
try:
for file_path in doc_dir.glob(f"**/{ext}"):
try:
content = file_path.read_text(encoding='utf-8', errors='ignore').lower()
for keyword in keywords:
if keyword.lower() in content:
rel_path = str(file_path.relative_to(project_dir))
if rel_path not in result["keyword_matches"]:
result["keyword_matches"].append(rel_path)
except Exception:
continue
except Exception:
continue
# Determine evidence strength
if result["documents_found"] and result["keyword_matches"]:
result["evidence_strength"] = "strong"
elif result["documents_found"] or result["keyword_matches"]:
result["evidence_strength"] = "partial"
else:
result["evidence_strength"] = "none"
return result
def assess_section(project_dir: Path, section_id: str, section_data: Dict) -> Dict:
"""Assess compliance for a QSR section."""
result = {
"section": section_id,
"title": section_data["title"],
"subsections": [],
"compliance_score": 0,
"total_subsections": len(section_data["subsections"]),
"compliant_subsections": 0
}
for subsection_id, subsection_data in section_data["subsections"].items():
evidence = search_documentation(
project_dir,
subsection_data["doc_patterns"],
subsection_data["keywords"]
)
subsection_result = {
"subsection": subsection_id,
"title": subsection_data["title"],
"required_evidence": subsection_data["required_evidence"],
"evidence_found": evidence,
"status": "gap" if evidence["evidence_strength"] == "none" else (
"partial" if evidence["evidence_strength"] == "partial" else "compliant"
)
}
if subsection_result["status"] == "compliant":
result["compliant_subsections"] += 1
elif subsection_result["status"] == "partial":
result["compliant_subsections"] += 0.5
result["subsections"].append(subsection_result)
if result["total_subsections"] > 0:
result["compliance_score"] = round(
(result["compliant_subsections"] / result["total_subsections"]) * 100, 1
)
return result
def generate_gap_report(assessment_results: List[Dict]) -> Dict:
"""Generate gap analysis report."""
gaps = []
recommendations = []
for section in assessment_results:
for subsection in section["subsections"]:
if subsection["status"] != "compliant":
gap = {
"section": subsection["subsection"],
"title": subsection["title"],
"status": subsection["status"],
"missing_evidence": subsection["required_evidence"]
}
gaps.append(gap)
if subsection["status"] == "gap":
recommendations.append(
f"{subsection['subsection']}: Create documentation for {subsection['title']}"
)
else:
recommendations.append(
f"{subsection['subsection']}: Enhance documentation for {subsection['title']}"
)
return {
"total_gaps": len([g for g in gaps if g["status"] == "gap"]),
"total_partial": len([g for g in gaps if g["status"] == "partial"]),
"gaps": gaps,
"priority_recommendations": recommendations[:10] # Top 10
}
def calculate_overall_compliance(assessment_results: List[Dict]) -> Dict:
"""Calculate overall QSR compliance score."""
total_subsections = 0
compliant_subsections = 0
section_scores = {}
for section in assessment_results:
total_subsections += section["total_subsections"]
compliant_subsections += section["compliant_subsections"]
section_scores[section["section"]] = section["compliance_score"]
overall_score = round((compliant_subsections / total_subsections) * 100, 1) if total_subsections > 0 else 0
# Determine compliance level
if overall_score >= 90:
level = "HIGH"
color = "green"
elif overall_score >= 70:
level = "MEDIUM"
color = "yellow"
elif overall_score >= 50:
level = "LOW"
color = "orange"
else:
level = "CRITICAL"
color = "red"
return {
"overall_score": overall_score,
"compliance_level": level,
"total_subsections": total_subsections,
"compliant_subsections": compliant_subsections,
"section_scores": section_scores
}
def print_text_report(result: Dict) -> None:
"""Print human-readable compliance report."""
print("=" * 70)
print("21 CFR PART 820 (QSR) COMPLIANCE ASSESSMENT")
print("=" * 70)
# Overall compliance
overall = result["overall_compliance"]
print(f"\nOVERALL COMPLIANCE: {overall['overall_score']}% ({overall['compliance_level']})")
print(f"Subsections Assessed: {overall['total_subsections']}")
print(f"Compliant/Partial: {overall['compliant_subsections']}")
# Section summary
print("\n--- SECTION SCORES ---")
for section in result["assessment"]:
status = "OK" if section["compliance_score"] >= 70 else "GAP"
print(f" {section['section']} {section['title']}: {section['compliance_score']}% [{status}]")
# Gap analysis
gap_report = result["gap_report"]
print(f"\n--- GAP ANALYSIS ---")
print(f"Critical Gaps: {gap_report['total_gaps']}")
print(f"Partial Compliance: {gap_report['total_partial']}")
if gap_report["gaps"]:
print("\n Gaps Identified:")
for gap in gap_report["gaps"][:15]: # Show top 15
status = "GAP" if gap["status"] == "gap" else "PARTIAL"
print(f" [{status}] {gap['section']}: {gap['title']}")
# Recommendations
if gap_report["priority_recommendations"]:
print("\n--- PRIORITY RECOMMENDATIONS ---")
for i, rec in enumerate(gap_report["priority_recommendations"], 1):
print(f" {i}. {rec}")
print("\n" + "=" * 70)
print(f"Assessment Date: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 70)
def main():
parser = argparse.ArgumentParser(
description="QSR Compliance Checker - Assess 21 CFR 820 compliance"
)
parser.add_argument(
"project_dir",
nargs="?",
default=".",
help="Project directory to analyze (default: current directory)"
)
parser.add_argument(
"--section",
help="Analyze specific QSR section only (e.g., 820.30)"
)
parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format"
)
parser.add_argument(
"--detailed",
action="store_true",
help="Include detailed evidence in output"
)
args = parser.parse_args()
project_dir = Path(args.project_dir).resolve()
if not project_dir.exists():
print(f"Error: Directory not found: {project_dir}", file=sys.stderr)
sys.exit(1)
# Filter sections if specific one requested
sections_to_assess = QSR_REQUIREMENTS
if args.section:
if args.section in QSR_REQUIREMENTS:
sections_to_assess = {args.section: QSR_REQUIREMENTS[args.section]}
else:
print(f"Error: Unknown section: {args.section}", file=sys.stderr)
print(f"Available sections: {', '.join(QSR_REQUIREMENTS.keys())}")
sys.exit(1)
# Perform assessment
assessment_results = []
for section_id, section_data in sections_to_assess.items():
section_result = assess_section(project_dir, section_id, section_data)
assessment_results.append(section_result)
# Generate reports
overall_compliance = calculate_overall_compliance(assessment_results)
gap_report = generate_gap_report(assessment_results)
result = {
"project_dir": str(project_dir),
"assessment_date": datetime.now().isoformat(),
"overall_compliance": overall_compliance,
"assessment": assessment_results if args.detailed else [
{
"section": s["section"],
"title": s["title"],
"compliance_score": s["compliance_score"],
"status": "compliant" if s["compliance_score"] >= 70 else "gap"
}
for s in assessment_results
],
"gap_report": gap_report
}
if args.json:
print(json.dumps(result, indent=2))
else:
print_text_report(result)
if __name__ == "__main__":
main()
Sửa các test Playwright bị lỗi hoặc chập chờn, gỡ lỗi test hỏng và lỗi xuất hiện ngẫu nhiên.
---
name: "fix"
description: >-
Fix failing or flaky Playwright tests. Use when user says "fix test",
"flaky test", "test failing", "debug test", "test broken", "test passes
sometimes", or "intermittent failure".
---
# Fix Failing or Flaky Tests
Diagnose and fix a Playwright test that fails or passes intermittently using a systematic taxonomy.
## Input
`$ARGUMENTS` contains:
- A test file path: `e2e/login.spec.ts`
- A test name: ""should redirect after login"`
- A description: `"the checkout test fails in CI but passes locally"`
## Steps
### 1. Reproduce the Failure
Run the test to capture the error:
```bash
npx playwright test <file> --reporter=list
```
If the test passes, it's likely flaky. Run burn-in:
```bash
npx playwright test <file> --repeat-each=10 --reporter=list
```
If it still passes, try with parallel workers:
```bash
npx playwright test --fully-parallel --workers=4 --repeat-each=5
```
### 2. Capture Trace
Run with full tracing:
```bash
npx playwright test <file> --trace=on --retries=0
```
Read the trace output. Use `/debug` to analyze trace files if available.
### 3. Categorize the Failure
Load `flaky-taxonomy.md` from this skill directory.
Every failing test falls into one of four categories:
| Category | Symptom | Diagnosis |
|---|---|---|
| **Timing/Async** | Fails intermittently everywhere | `--repeat-each=20` reproduces locally |
| **Test Isolation** | Fails in suite, passes alone | `--workers=1 --grep "test name"` passes |
| **Environment** | Fails in CI, passes locally | Compare CI vs local screenshots/traces |
| **Infrastructure** | Random, no pattern | Error references browser internals |
### 4. Apply Targeted Fix
**Timing/Async:**
- Replace `waitForTimeout()` with web-first assertions
- Add `await` to missing Playwright calls
- Wait for specific network responses before asserting
- Use `toBeVisible()` before interacting with elements
**Test Isolation:**
- Remove shared mutable state between tests
- Create test data per-test via API or fixtures
- Use unique identifiers (timestamps, random strings) for test data
- Check for database state leaks
**Environment:**
- Match viewport sizes between local and CI
- Account for font rendering differences in screenshots
- Use `docker` locally to match CI environment
- Check for timezone-dependent assertions
**Infrastructure:**
- Increase timeout for slow CI runners
- Add retries in CI config (`retries: 2`)
- Check for browser OOM (reduce parallel workers)
- Ensure browser dependencies are installed
### 5. Verify the Fix
Run the test 10 times to confirm stability:
```bash
npx playwright test <file> --repeat-each=10 --reporter=list
```
All 10 must pass. If any fail, go back to step 3.
### 6. Prevent Recurrence
Suggest:
- Add to CI with `retries: 2` if not already
- Enable `trace: 'on-first-retry'` in config
- Add the fix pattern to project's test conventions doc
## Output
- Root cause category and specific issue
- The fix applied (with diff)
- Verification result (10/10 passes)
- Prevention recommendation
FILE:flaky-taxonomy.md
# Flaky Test Taxonomy
## Decision Tree
```
Test is flaky
│
├── Fails locally with --repeat-each=20?
│ ├── YES → TIMING / ASYNC
│ │ ├── Missing await? → Add await
│ │ ├── waitForTimeout? → Replace with assertion
│ │ ├── Race condition? → Wait for specific event
│ │ └── Animation? → Wait for animation end or disable
│ │
│ └── NO → Continue...
│
├── Passes alone, fails in suite?
│ ├── YES → TEST ISOLATION
│ │ ├── Shared variable? → Make per-test
│ │ ├── Database state? → Reset per-test
│ │ ├── localStorage? → Clear in beforeEach
│ │ └── Cookie leak? → Use isolated contexts
│ │
│ └── NO → Continue...
│
├── Fails in CI, passes locally?
│ ├── YES → ENVIRONMENT
│ │ ├── Viewport? → Set explicit size
│ │ ├── Fonts? → Use Docker locally
│ │ ├── Timezone? → Use UTC everywhere
│ │ └── Network? → Mock external services
│ │
│ └── NO → INFRASTRUCTURE
│ ├── Browser crash? → Reduce workers
│ ├── OOM? → Limit parallel tests
│ ├── DNS? → Add retry config
│ └── File system? → Use unique temp dirs
```
## Common Fixes by Category
### Timing / Async
**Missing await:**
```typescript
// BAD — race condition
page.goto('/dashboard');
expect(page.getByText('Welcome')).toBeVisible();
// GOOD
await page.goto('/dashboard');
await expect(page.getByText('Welcome')).toBeVisible();
```
**Clicking before visible:**
```typescript
// BAD — element may not be ready
await page.getByRole('button', { name: 'Submit' }).click();
// GOOD — ensure visible first
const submitBtn = page.getByRole('button', { name: 'Submit' });
await expect(submitBtn).toBeVisible();
await submitBtn.click();
```
**Race with network:**
```typescript
// BAD — data might not be loaded
await page.goto('/users');
await expect(page.getByRole('table')).toBeVisible();
// GOOD — wait for API response
const responsePromise = page.waitForResponse('**/api/users');
await page.goto('/users');
await responsePromise;
await expect(page.getByRole('table')).toBeVisible();
```
### Test Isolation
**Shared state fix:**
```typescript
// BAD — tests share userId
let userId: string;
test('create', async () => { userId = '123'; });
test('read', async () => { /* uses userId */ });
// GOOD — each test is independent
test('read user', async ({ request }) => {
const response = await request.post('/api/users', { data: { name: 'Test' } });
const { id } = await response.json();
// Use id within this test
});
```
**localStorage cleanup:**
```typescript
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.evaluate(() => localStorage.clear());
});
```
### Environment
**Explicit viewport:**
```typescript
test.use({ viewport: { width: 1280, height: 720 } });
```
**Timezone-safe dates:**
```typescript
// BAD
expect(dateText).toBe('March 5, 2026');
// GOOD — timezone independent
expect(dateText).toMatch(/\d{1,2}\/\d{1,2}\/\d{4}/);
```
### Infrastructure
**Retry config:**
```typescript
// playwright.config.ts
export default defineConfig({
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
});
```
**Increase timeout for CI:**
```typescript
test.setTimeout(60_000); // 60s for slow CI
```
Sửa và gỡ lỗi có hệ thống một tính năng hoặc module từ đầu đến cuối trên mọi tệp và phụ thuộc, không dùng cho lỗi đơn lẻ.
---
name: "focused-fix"
description: "Use when the user asks to fix, debug, or make a specific feature/module/area work end-to-end. Triggers: 'make X work', 'fix the Y feature', 'the Z module is broken', 'focus on [area]'. Not for quick single-bug fixes — this is for systematic deep-dive repair across all files and dependencies."
---
# Focused Fix — Deep-Dive Feature Repair
## When to Use
Activate when the user asks to fix, debug, or make a specific feature/module/area work. Key triggers:
- "make X work"
- "fix the Y feature"
- "the Z module is broken"
- "focus on [area]"
- "this feature needs to work properly"
This is NOT for quick single-bug fixes (use systematic-debugging for that). This is for when an entire feature or module needs systematic repair — tracing every dependency, reading logs, checking tests, mapping the full dependency graph.
```dot
digraph when_to_use {
"User reports feature broken" [shape=diamond];
"Single bug or symptom?" [shape=diamond];
"Use systematic-debugging" [shape=box];
"Entire feature/module needs repair?" [shape=diamond];
"Use focused-fix" [shape=box];
"Something else" [shape=box];
"User reports feature broken" -> "Single bug or symptom?";
"Single bug or symptom?" -> "Use systematic-debugging" [label="yes"];
"Single bug or symptom?" -> "Entire feature/module needs repair?" [label="no"];
"Entire feature/module needs repair?" -> "Use focused-fix" [label="yes"];
"Entire feature/module needs repair?" -> "Something else" [label="no"];
}
```
## The Iron Law
```
NO FIXES WITHOUT COMPLETING SCOPE → TRACE → DIAGNOSE FIRST
```
If you haven't finished Phase 3, you cannot propose fixes. Period.
**Violating the letter of these phases is violating the spirit of focused repair.**
## Protocol — STRICTLY follow these 5 phases IN ORDER
```dot
digraph phases {
rankdir=LR;
SCOPE [shape=box, label="Phase 1\nSCOPE"];
TRACE [shape=box, label="Phase 2\nTRACE"];
DIAGNOSE [shape=box, label="Phase 3\nDIAGNOSE"];
FIX [shape=box, label="Phase 4\nFIX"];
VERIFY [shape=box, label="Phase 5\nVERIFY"];
SCOPE -> TRACE -> DIAGNOSE -> FIX -> VERIFY;
FIX -> DIAGNOSE [label="fix broke\nsomething else"];
FIX -> ESCALATE [label="3+ fixes\ncreate new issues"];
ESCALATE [shape=doubleoctagon, label="STOP\nQuestion Architecture\nDiscuss with User"];
}
```
### Phase 1: SCOPE — Map the Feature Boundary
Before touching any code, understand the full scope of the feature.
1. Ask the user: "Which feature/folder should I focus on?" if not already clear
2. Identify the PRIMARY folder/files for this feature
3. Map EVERY file in that folder — read each one, understand its purpose
4. Create a feature manifest:
```
FEATURE SCOPE:
Primary path: src/features/auth/
Entry points: [files that are imported by other parts of the app]
Internal files: [files only used within this feature]
Total files: N
Total lines: N
```
### Phase 2: TRACE — Map All Dependencies (Inside AND Outside)
Trace every connection this feature has to the rest of the codebase.
**INBOUND (what this feature imports):**
1. For every import statement in every file in the feature folder:
- Trace it to its source
- Verify the source file exists
- Verify the imported entity (function, type, component) exists and is exported
- Check if the types/signatures match what the feature expects
2. Check for:
- Environment variables used (grep for process.env, import.meta.env, os.environ, etc.)
- Config files referenced
- Database models/schemas used
- API endpoints called
- Third-party packages imported
**OUTBOUND (what imports this feature):**
1. Search the entire codebase for imports from this feature folder
2. For each consumer:
- Verify they're importing entities that actually exist
- Check if they're using the correct API/interface
- Note if any consumers are using deprecated patterns
Output format:
```
DEPENDENCY MAP:
Inbound (this feature depends on):
src/lib/db.ts → used in auth/repository.ts (getUserById, createUser)
src/lib/jwt.ts → used in auth/service.ts (signToken, verifyToken)
@prisma/client → used in auth/repository.ts
process.env.JWT_SECRET → used in auth/service.ts
process.env.DATABASE_URL → used via prisma
Outbound (depends on this feature):
src/app/api/login/route.ts → imports { login } from auth/service
src/app/api/register/route.ts → imports { register } from auth/service
src/middleware.ts → imports { verifyToken } from auth/service
Env vars required: JWT_SECRET, DATABASE_URL
Config files: prisma/schema.prisma (User model)
```
### Phase 3: DIAGNOSE — Find Every Issue
Systematically check for problems. Run ALL of these checks:
**CODE QUALITY:**
- [ ] Every import resolves to a real file/export
- [ ] No circular dependencies within the feature
- [ ] Types are consistent across boundaries (no `any` at interfaces)
- [ ] Error handling exists for all async operations
- [ ] No TODO/FIXME/HACK comments indicating known issues
**RUNTIME:**
- [ ] All required environment variables are set (check .env)
- [ ] Database migrations are up to date (if applicable)
- [ ] API endpoints return expected shapes
- [ ] No hardcoded values that should be configurable
**TESTS:**
- [ ] Run ALL tests related to this feature: find them by searching for imports from the feature folder
- [ ] Record every failure with full error output
- [ ] Check test coverage — are there untested code paths?
**LOGS & ERRORS:**
- [ ] Search for any log files, error reports, or Sentry-style error tracking
- [ ] Check git log for recent changes to this feature: `git log --oneline -20 -- <feature-path>`
- [ ] Check if any recent commits might have broken something: `git log --oneline -5 --all -- <files that this feature depends on>`
**CONFIGURATION:**
- [ ] Verify all config files this feature depends on are valid
- [ ] Check for mismatches between development and production configs
- [ ] Verify third-party service credentials are valid (if testable)
**ROOT-CAUSE CONFIRMATION:**
For each CRITICAL issue found, confirm root cause before adding it to the fix list:
- State clearly: "I think X is the root cause because Y"
- Trace the data/control flow backward to verify — don't trust surface-level symptoms
- If the issue spans multiple components, add diagnostic logging at each boundary to identify which layer fails
- **REQUIRED SUB-SKILL:** For complex bugs found during diagnosis, apply `superpowers:systematic-debugging` Phase 1 (Root Cause Investigation) to confirm before proceeding
**RISK LABELING:**
Assign each issue a risk label:
| Risk | Criteria |
|---|---|
| HIGH | Public API surface / breaking interface contract / DB schema / auth or security logic / widely imported module (>3 callers) / git hotspot |
| MED | Internal module with tests / shared utility / config with runtime impact / internal callers of changed functions |
| LOW | Leaf module / isolated file / test-only change / single-purpose helper with no callers |
Output format:
```
DIAGNOSIS REPORT:
Issues found: N
CRITICAL:
1. [HIGH] [file:line] — description of issue. Root cause: [confirmed explanation]
2. [HIGH] [file:line] — description of issue. Root cause: [confirmed explanation]
WARNINGS:
1. [MED] [file:line] — description of issue
2. [LOW] [file:line] — description of issue
TESTS:
Ran: N tests
Passed: N
Failed: N
[list each failure with one-line summary]
```
### Phase 4: FIX — Repair Everything Systematically
Fix issues in this EXACT order:
1. **DEPENDENCIES FIRST** — fix broken imports, missing packages, wrong versions
2. **TYPES SECOND** — fix type mismatches at feature boundaries
3. **LOGIC THIRD** — fix actual business logic bugs
4. **TESTS FOURTH** — fix or create tests for each fix
5. **INTEGRATION LAST** — verify the feature works end-to-end with its consumers
Rules:
- Fix ONE issue at a time
- After each fix, run the related test to confirm it works
- If a fix breaks something else, STOP and re-evaluate (go back to DIAGNOSE)
- Keep a running log of every change made
- Never change code outside the feature folder without explicitly stating why
- Fix HIGH-risk issues before MED, MED before LOW
**ESCALATION RULE — 3-Strike Architecture Check:**
If 3+ fixes in this phase create NEW issues (not pre-existing ones), STOP immediately.
This pattern indicates an architectural problem, not a bug collection:
- Each fix reveals new shared state / coupling / problem in a different place
- Fixes require "massive refactoring" to implement
- Each fix creates new symptoms elsewhere
**Action:** Stop fixing. Tell the user: "3+ fixes have cascaded into new issues. This suggests the feature's architecture may need rethinking, not patching. Here's what I've found: [summary]. Should we continue fixing symptoms or discuss restructuring?"
Do NOT attempt fix #4 without this discussion.
Output after each fix:
```
FIX #1:
File: auth/service.ts:45
Issue: signToken called with wrong argument order
Change: swapped (expiresIn, payload) to (payload, expiresIn)
Test: auth.test.ts → PASSES
```
### Phase 5: VERIFY — Confirm Everything Works
After all fixes are applied:
1. Run ALL tests in the feature folder — every single one must pass
2. Run ALL tests in files that IMPORT from this feature — must pass
3. Run the full test suite if available — check for regressions
4. If the feature has a UI, describe how to manually verify it
5. Summarize all changes made
Final output:
```
FOCUSED FIX COMPLETE:
Feature: auth
Files changed: 4
Total fixes: 7
Tests: 23/23 passing
Regressions: 0
Changes:
1. auth/service.ts — fixed token signing argument order
2. auth/repository.ts — added null check for user lookup
3. auth/middleware.ts — fixed async error handling
4. auth/types.ts — aligned UserResponse type with actual DB schema
Consumers verified:
- src/app/api/login/route.ts ✅
- src/app/api/register/route.ts ✅
- src/middleware.ts ✅
```
## Red Flags — STOP and Return to Current Phase
If you catch yourself thinking any of these, you are skipping phases:
- "I can see the bug, let me just fix it" → STOP. You haven't traced dependencies yet.
- "Scoping is overkill, it's obviously just this file" → STOP. That's always wrong for feature-level fixes.
- "I'll map dependencies after I fix the obvious stuff" → STOP. You'll miss root causes.
- "The user said fix X, so I only need to look at X" → STOP. Features have dependencies.
- "Tests are passing so I'm done" → STOP. Did you run consumer tests too?
- "I don't need to check env vars for this" → STOP. Config issues masquerade as code bugs.
- "One more fix should do it" (after 2+ cascading failures) → STOP. Escalate.
- "I'll skip the diagnosis report, the fixes are obvious" → STOP. Write it down.
**ALL of these mean: Return to the phase you're supposed to be in.**
## Common Rationalizations
| Excuse | Reality |
|---|---|
| "The feature is small, I don't need all 5 phases" | Small features have dependencies too. Phases 1-2 take minutes for small features — do them. |
| "I already know this codebase" | Knowledge decays. Trace the actual imports, don't rely on memory. |
| "The user wants speed, not process" | Skipping phases causes rework. Systematic is faster than thrashing. |
| "Only one file is broken" | If only one file were broken, the user would say "fix this bug", not "make the feature work." |
| "I fixed the tests, so it works" | Tests can pass while consumers are broken. Verify Phase 5 fully. |
| "The dependency map is too big to trace" | Then the feature is too big to fix without tracing. That's exactly why you need it. |
| "Root cause is obvious, I don't need to confirm" | "Obvious" root causes are wrong 40% of the time. Confirm with evidence. |
| "3 cascading failures is normal for a big fix" | 3 cascading failures means you're patching symptoms of an architectural problem. |
## Anti-Patterns — NEVER do these
| Anti-Pattern | Why It's Wrong |
|---|---|
| Starting to fix code before mapping all dependencies | You'll miss root causes and create whack-a-mole fixes |
| Fixing only the file the user mentioned | Related files likely have issues too |
| Ignoring environment variables and configuration | Many "code bugs" are actually config issues |
| Skipping the test run phase | You can't verify fixes without running tests |
| Making changes outside the feature folder without explaining why | Unexpected side effects confuse the user |
| Fixing symptoms in consumer files instead of root cause in feature | Band-aids that break when the next consumer appears |
| Declaring "done" without running verification tests | Untested fixes are unverified fixes |
| Changing the public API without updating all consumers | Breaks everything that depends on the feature |
## Related Skills
- **`superpowers:systematic-debugging`** — Use within Phase 3 for root-cause tracing of individual complex bugs
- **`superpowers:verification-before-completion`** — Use within Phase 5 before claiming the feature is fixed
- **`scope`** — If you need to understand blast radius before starting, run scope first then focused-fix
## Quick Reference
| Phase | Key Action | Output |
|---|---|---|
| SCOPE | Read every file, map entry points | Feature manifest |
| TRACE | Map inbound + outbound dependencies | Dependency map |
| DIAGNOSE | Check code, runtime, tests, logs, config | Diagnosis report |
| FIX | Fix in order: deps → types → logic → tests → integration | Fix log per issue |
| VERIFY | Run all tests, check consumers, summarize | Completion report |
Tối ưu biểu mẫu không phải đăng ký: lead, liên hệ, yêu cầu demo, ứng tuyển, khảo sát, thanh toán; giảm ma sát và trường thừa.
---
name: "form-cro"
description: When the user wants to optimize any form that is NOT signup/registration — including lead capture forms, contact forms, demo request forms, application forms, survey forms, or checkout forms. Also use when the user mentions "form optimization," "lead form conversions," "form friction," "form fields," "form completion rate," or "contact form." For signup/registration forms, see signup-flow-cro. For popups containing forms, see popup-cro.
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: marketing
updated: 2026-03-06
---
# Form CRO
You are an expert in form optimization. Your goal is to maximize form completion rates while capturing the data that matters.
## Initial Assessment
**Check for product marketing context first:**
If `.claude/product-marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Before providing recommendations, identify:
1. **Form Type**
- Lead capture (gated content, newsletter)
- Contact form
- Demo/sales request
- Application form
- Survey/feedback
- Checkout form
- Quote request
2. **Current State**
- How many fields?
- What's the current completion rate?
- Mobile vs. desktop split?
- Where do users abandon?
3. **Business Context**
- What happens with form submissions?
- Which fields are actually used in follow-up?
- Are there compliance/legal requirements?
---
## Core Principles
→ See references/form-cro-playbook.md for details
## Output Format
### Form Audit
For each issue:
- **Issue**: What's wrong
- **Impact**: Estimated effect on conversions
- **Fix**: Specific recommendation
- **Priority**: High/Medium/Low
### Recommended Form Design
- **Required fields**: Justified list
- **Optional fields**: With rationale
- **Field order**: Recommended sequence
- **Copy**: Labels, placeholders, button
- **Error messages**: For each field
- **Layout**: Visual guidance
### Test Hypotheses
Ideas to A/B test with expected outcomes
---
## Experiment Ideas
### Form Structure Experiments
**Layout & Flow**
- Single-step form vs. multi-step with progress bar
- 1-column vs. 2-column field layout
- Form embedded on page vs. separate page
- Vertical vs. horizontal field alignment
- Form above fold vs. after content
**Field Optimization**
- Reduce to minimum viable fields
- Add or remove phone number field
- Add or remove company/organization field
- Test required vs. optional field balance
- Use field enrichment to auto-fill known data
- Hide fields for returning/known visitors
**Smart Forms**
- Add real-time validation for emails and phone numbers
- Progressive profiling (ask more over time)
- Conditional fields based on earlier answers
- Auto-suggest for company names
---
### Copy & Design Experiments
**Labels & Microcopy**
- Test field label clarity and length
- Placeholder text optimization
- Help text: show vs. hide vs. on-hover
- Error message tone (friendly vs. direct)
**CTAs & Buttons**
- Button text variations ("Submit" vs. "Get My Quote" vs. specific action)
- Button color and size testing
- Button placement relative to fields
**Trust Elements**
- Add privacy assurance near form
- Show trust badges next to submit
- Add testimonial near form
- Display expected response time
---
### Form Type-Specific Experiments
**Demo Request Forms**
- Test with/without phone number requirement
- Add "preferred contact method" choice
- Include "What's your biggest challenge?" question
- Test calendar embed vs. form submission
**Lead Capture Forms**
- Email-only vs. email + name
- Test value proposition messaging above form
- Gated vs. ungated content strategies
- Post-submission enrichment questions
**Contact Forms**
- Add department/topic routing dropdown
- Test with/without message field requirement
- Show alternative contact methods (chat, phone)
- Expected response time messaging
---
### Mobile & UX Experiments
- Larger touch targets for mobile
- Test appropriate keyboard types by field
- Sticky submit button on mobile
- Auto-focus first field on page load
- Test form container styling (card vs. minimal)
---
## Task-Specific Questions
1. What's your current form completion rate?
2. Do you have field-level analytics?
3. What happens with the data after submission?
4. Which fields are actually used in follow-up?
5. Are there compliance/legal requirements?
6. What's the mobile vs. desktop split?
---
## Related Skills
- **signup-flow-cro** — WHEN: the form being optimized is an account creation or trial registration form specifically. WHEN NOT: don't use signup-flow-cro for lead capture, contact, or demo request forms; form-cro is the right tool.
- **popup-cro** — WHEN: the form lives inside a modal, exit-intent popup, or slide-in widget rather than embedded on a page. WHEN NOT: don't use popup-cro for standalone page-embedded forms.
- **page-cro** — WHEN: the page containing the form is itself underperforming — poor value prop, weak headline, or mismatched traffic source. Fix the page context before or alongside the form. WHEN NOT: don't invoke page-cro if the form is the only conversion element on a dedicated landing page and the page itself is fine.
- **ab-test-setup** — WHEN: specific form hypotheses are ready to test (field count, button copy, multi-step vs. single-step). WHEN NOT: don't use ab-test-setup before the audit identifies the most impactful change to test.
- **analytics-tracking** — WHEN: field-level drop-off data doesn't exist yet and the team needs to instrument form analytics before any optimization can happen. WHEN NOT: skip if analytics are already in place.
- **marketing-context** — WHEN: check `.claude/product-marketing-context.md` for ICP and qualification criteria, which directly informs which fields are truly necessary. WHEN NOT: skip if user has explicitly listed the fields and their business rationale.
---
## Communication
All form CRO output follows this quality standard:
- Every field recommendation is justified — never just "remove fields" without explaining which and why
- Audit output uses the **Issue / Impact / Fix / Priority** structure consistently
- Multi-step vs. single-step recommendation always includes the qualifying criteria for the choice
- Mobile optimization is addressed separately from desktop — never conflate the two
- Submit button copy alternatives are always provided (minimum 3 options with reasoning)
- Error message rewrites are included when error handling is flagged as an issue
---
## Proactive Triggers
Automatically surface form-cro when:
1. **"Our lead form isn't converting"** — Any complaint about form completion rates immediately triggers the field audit and core principles review.
2. **Demo request or contact page being built** — When frontend-design or copywriting skills are active and a form is part of the page, proactively offer form-cro review.
3. **"We're getting leads but bad quality"** — Poor lead quality often signals wrong fields or missing qualification questions; proactively recommend field audit.
4. **Mobile conversion gap detected** — If page-cro or analytics review shows a desktop vs. mobile completion gap on a form, surface form-cro mobile optimization checklist.
5. **Long form identified** — When user describes or shares a form with 7+ fields, immediately flag the field-cost framework and multi-step recommendation.
---
## Output Artifacts
| Artifact | Format | Description |
|----------|--------|-------------|
| Form Audit | Issue/Impact/Fix/Priority table | Per-field and per-pattern analysis with actionable fixes |
| Recommended Field Set | Justified list | Required vs. optional fields with rationale for each |
| Field Order & Layout Spec | Annotated outline | Recommended sequence, grouping, column layout, and mobile considerations |
| Submit Button Copy Options | 3-option table | Action-oriented button copy variants with reasoning |
| A/B Test Hypotheses | Table | Hypothesis × variant × success metric × priority for top 3-5 test ideas |
FILE:references/form-cro-playbook.md
# form-cro reference
## Core Principles
### 1. Every Field Has a Cost
Each field reduces completion rate. Rule of thumb:
- 3 fields: Baseline
- 4-6 fields: 10-25% reduction
- 7+ fields: 25-50%+ reduction
For each field, ask:
- Is this absolutely necessary before we can help them?
- Can we get this information another way?
- Can we ask this later?
### 2. Value Must Exceed Effort
- Clear value proposition above form
- Make what they get obvious
- Reduce perceived effort (field count, labels)
### 3. Reduce Cognitive Load
- One question per field
- Clear, conversational labels
- Logical grouping and order
- Smart defaults where possible
---
## Field-by-Field Optimization
### Email Field
- Single field, no confirmation
- Inline validation
- Typo detection (did you mean gmail.com?)
- Proper mobile keyboard
### Name Fields
- Single "Name" vs. First/Last — test this
- Single field reduces friction
- Split needed only if personalization requires it
### Phone Number
- Make optional if possible
- If required, explain why
- Auto-format as they type
- Country code handling
### Company/Organization
- Auto-suggest for faster entry
- Enrichment after submission (Clearbit, etc.)
- Consider inferring from email domain
### Job Title/Role
- Dropdown if categories matter
- Free text if wide variation
- Consider making optional
### Message/Comments (Free Text)
- Make optional
- Reasonable character guidance
- Expand on focus
### Dropdown Selects
- "Select one..." placeholder
- Searchable if many options
- Consider radio buttons if < 5 options
- "Other" option with text field
### Checkboxes (Multi-select)
- Clear, parallel labels
- Reasonable number of options
- Consider "Select all that apply" instruction
---
## Form Layout Optimization
### Field Order
1. Start with easiest fields (name, email)
2. Build commitment before asking more
3. Sensitive fields last (phone, company size)
4. Logical grouping if many fields
### Labels and Placeholders
- Labels: Always visible (not just placeholder)
- Placeholders: Examples, not labels
- Help text: Only when genuinely helpful
**Good:**
```
Email
[name@company.com]
```
**Bad:**
```
[Enter your email address] ← Disappears on focus
```
### Visual Design
- Sufficient spacing between fields
- Clear visual hierarchy
- CTA button stands out
- Mobile-friendly tap targets (44px+)
### Single Column vs. Multi-Column
- Single column: Higher completion, mobile-friendly
- Multi-column: Only for short related fields (First/Last name)
- When in doubt, single column
---
## Multi-Step Forms
### When to Use Multi-Step
- More than 5-6 fields
- Logically distinct sections
- Conditional paths based on answers
- Complex forms (applications, quotes)
### Multi-Step Best Practices
- Progress indicator (step X of Y)
- Start with easy, end with sensitive
- One topic per step
- Allow back navigation
- Save progress (don't lose data on refresh)
- Clear indication of required vs. optional
### Progressive Commitment Pattern
1. Low-friction start (just email)
2. More detail (name, company)
3. Qualifying questions
4. Contact preferences
---
## Error Handling
### Inline Validation
- Validate as they move to next field
- Don't validate too aggressively while typing
- Clear visual indicators (green check, red border)
### Error Messages
- Specific to the problem
- Suggest how to fix
- Positioned near the field
- Don't clear their input
**Good:** "Please enter a valid email address (e.g., name@company.com)"
**Bad:** "Invalid input"
### On Submit
- Focus on first error field
- Summarize errors if multiple
- Preserve all entered data
- Don't clear form on error
---
## Submit Button Optimization
### Button Copy
Weak: "Submit" | "Send"
Strong: "[Action] + [What they get]"
Examples:
- "Get My Free Quote"
- "Download the Guide"
- "Request Demo"
- "Send Message"
- "Start Free Trial"
### Button Placement
- Immediately after last field
- Left-aligned with fields
- Sufficient size and contrast
- Mobile: Sticky or clearly visible
### Post-Submit States
- Loading state (disable button, show spinner)
- Success confirmation (clear next steps)
- Error handling (clear message, focus on issue)
---
## Trust and Friction Reduction
### Near the Form
- Privacy statement: "We'll never share your info"
- Security badges if collecting sensitive data
- Testimonial or social proof
- Expected response time
### Reducing Perceived Effort
- "Takes 30 seconds"
- Field count indicator
- Remove visual clutter
- Generous white space
### Addressing Objections
- "No spam, unsubscribe anytime"
- "We won't share your number"
- "No credit card required"
---
## Form Types: Specific Guidance
### Lead Capture (Gated Content)
- Minimum viable fields (often just email)
- Clear value proposition for what they get
- Consider asking enrichment questions post-download
- Test email-only vs. email + name
### Contact Form
- Essential: Email/Name + Message
- Phone optional
- Set response time expectations
- Offer alternatives (chat, phone)
### Demo Request
- Name, Email, Company required
- Phone: Optional with "preferred contact" choice
- Use case/goal question helps personalize
- Calendar embed can increase show rate
### Quote/Estimate Request
- Multi-step often works well
- Start with easy questions
- Technical details later
- Save progress for complex forms
### Survey Forms
- Progress bar essential
- One question per screen for engagement
- Skip logic for relevance
- Consider incentive for completion
---
## Mobile Optimization
- Larger touch targets (44px minimum height)
- Appropriate keyboard types (email, tel, number)
- Autofill support
- Single column only
- Sticky submit button
- Minimal typing (dropdowns, buttons)
---
## Measurement
### Key Metrics
- **Form start rate**: Page views → Started form
- **Completion rate**: Started → Submitted
- **Field drop-off**: Which fields lose people
- **Error rate**: By field
- **Time to complete**: Total and by field
- **Mobile vs. desktop**: Completion by device
### What to Track
- Form views
- First field focus
- Each field completion
- Errors by field
- Submit attempts
- Successful submissions
---
FILE:scripts/form_field_analyzer.py
#!/usr/bin/env python3
"""
Form Field Analyzer for CRO
Analyzes HTML forms for conversion optimization opportunities.
Checks field count, types, labels, friction signals, and mobile readiness.
Usage:
python3 form_field_analyzer.py # Demo mode
python3 form_field_analyzer.py form.html # Analyze HTML file
python3 form_field_analyzer.py form.html --json # JSON output
"""
import json
import sys
import os
import re
from html.parser import HTMLParser
class FormAnalyzer(HTMLParser):
def __init__(self):
super().__init__()
self.forms = []
self.current_form = None
self.in_label = False
self.current_label = ""
self.in_button = False
self.current_button = ""
def handle_starttag(self, tag, attrs):
attrs_dict = dict(attrs)
if tag == "form":
self.current_form = {
"action": attrs_dict.get("action", ""),
"method": attrs_dict.get("method", "GET").upper(),
"fields": [],
"buttons": [],
"has_autocomplete": "autocomplete" in attrs_dict
}
elif tag == "input" and self.current_form is not None:
input_type = attrs_dict.get("type", "text").lower()
if input_type not in ("hidden", "submit"):
self.current_form["fields"].append({
"type": input_type,
"name": attrs_dict.get("name", ""),
"placeholder": attrs_dict.get("placeholder", ""),
"required": "required" in attrs_dict,
"autocomplete": attrs_dict.get("autocomplete", ""),
"has_label": False
})
elif input_type == "submit":
self.current_form["buttons"].append(attrs_dict.get("value", "Submit"))
elif tag == "textarea" and self.current_form is not None:
self.current_form["fields"].append({
"type": "textarea",
"name": attrs_dict.get("name", ""),
"placeholder": attrs_dict.get("placeholder", ""),
"required": "required" in attrs_dict,
"autocomplete": "",
"has_label": False
})
elif tag == "select" and self.current_form is not None:
self.current_form["fields"].append({
"type": "select",
"name": attrs_dict.get("name", ""),
"placeholder": "",
"required": "required" in attrs_dict,
"autocomplete": "",
"has_label": False
})
elif tag == "label":
self.in_label = True
self.current_label = ""
for_attr = attrs_dict.get("for", "")
if for_attr and self.current_form:
for field in self.current_form["fields"]:
if field["name"] == for_attr:
field["has_label"] = True
elif tag == "button":
self.in_button = True
self.current_button = ""
def handle_data(self, data):
if self.in_label:
self.current_label += data.strip()
if self.in_button:
self.current_button += data.strip()
def handle_endtag(self, tag):
if tag == "form" and self.current_form:
self.forms.append(self.current_form)
self.current_form = None
elif tag == "label":
self.in_label = False
elif tag == "button":
self.in_button = False
if self.current_button and self.current_form:
self.current_form["buttons"].append(self.current_button)
def analyze_form(form):
"""Analyze a single form for CRO issues."""
fields = form["fields"]
issues = []
warnings = []
positives = []
field_count = len(fields)
# Field count analysis
if field_count > 7:
issues.append(f"Too many fields ({field_count}). Each field above 3 reduces conversion by ~5-10%. Consider progressive disclosure.")
elif field_count > 4:
warnings.append(f"{field_count} fields — acceptable but test reducing to 3-4 core fields.")
elif field_count <= 3:
positives.append(f"Low friction — only {field_count} fields.")
# Phone number field
phone_fields = [f for f in fields if "phone" in f["name"].lower() or f["type"] == "tel"]
if phone_fields:
required_phones = [f for f in phone_fields if f["required"]]
if required_phones:
issues.append("Phone number is REQUIRED — this is the #1 form abandonment trigger. Make optional or remove.")
else:
warnings.append("Phone field present (optional) — still causes friction. Consider removing unless sales-critical.")
# Labels
unlabeled = [f for f in fields if not f["has_label"] and not f["placeholder"]]
if unlabeled:
issues.append(f"{len(unlabeled)} fields have no label AND no placeholder. Users won't know what to enter.")
placeholder_only = [f for f in fields if not f["has_label"] and f["placeholder"]]
if placeholder_only:
warnings.append(f"{len(placeholder_only)} fields use placeholder-only labels. Placeholders disappear on focus — use visible labels.")
# Button text
weak_ctas = ["submit", "send", "go", "ok"]
for btn in form["buttons"]:
if btn.lower() in weak_ctas:
warnings.append(f'CTA button says "{btn}" — use action-specific text like "Get My Free Report" or "Start Free Trial".')
if not form["buttons"]:
issues.append("No submit button found. Form may be broken or use JavaScript submission only.")
# Autocomplete
fields_with_autocomplete = [f for f in fields if f["autocomplete"]]
if not fields_with_autocomplete and field_count > 0:
warnings.append("No autocomplete attributes. Adding autocomplete reduces mobile friction significantly.")
# Required fields
required_count = sum(1 for f in fields if f["required"])
if required_count == field_count and field_count > 2:
warnings.append("ALL fields are required. Consider making some optional to reduce perceived commitment.")
# Score
score = 100
score -= len(issues) * 15
score -= len(warnings) * 5
score += len(positives) * 5
score = max(0, min(100, score))
return {
"field_count": field_count,
"required_count": required_count,
"has_phone": len(phone_fields) > 0,
"cta_text": form["buttons"],
"issues": issues,
"warnings": warnings,
"positives": positives,
"score": score,
"fields": [{"name": f["name"], "type": f["type"], "required": f["required"]} for f in fields]
}
def format_report(analyses):
"""Format human-readable report."""
lines = []
lines.append("")
lines.append("=" * 60)
lines.append(" FORM CRO — FIELD ANALYSIS REPORT")
lines.append("=" * 60)
for i, analysis in enumerate(analyses):
lines.append("")
lines.append(f" FORM {i + 1}")
lines.append(f" Fields: {analysis['field_count']} | Required: {analysis['required_count']} | CTA: {', '.join(analysis['cta_text']) or 'none'}")
lines.append("")
score = analysis["score"]
bar = "█" * (score // 5) + "░" * (20 - score // 5)
lines.append(f" FORM SCORE: {score}/100")
lines.append(f" [{bar}]")
lines.append("")
lines.append(" Fields:")
for f in analysis["fields"]:
req = " *" if f["required"] else ""
lines.append(f" [{f['type']}] {f['name']}{req}")
lines.append("")
if analysis["positives"]:
lines.append(" 🟢 STRENGTHS:")
for p in analysis["positives"]:
lines.append(f" ✓ {p}")
lines.append("")
if analysis["issues"]:
lines.append(" 🔴 ISSUES:")
for issue in analysis["issues"]:
lines.append(f" • {issue}")
lines.append("")
if analysis["warnings"]:
lines.append(" 🟡 WARNINGS:")
for warn in analysis["warnings"]:
lines.append(f" • {warn}")
lines.append("")
return "\n".join(lines)
SAMPLE_HTML = """
<form action="/submit" method="POST">
<label for="name">Full Name</label>
<input type="text" name="name" id="name" required placeholder="John Smith">
<label for="email">Work Email</label>
<input type="email" name="email" id="email" required placeholder="you@company.com">
<label for="company">Company</label>
<input type="text" name="company" id="company" required>
<label for="phone">Phone Number</label>
<input type="tel" name="phone" id="phone" required>
<label for="role">Job Title</label>
<input type="text" name="role" id="role" required>
<label for="employees">Company Size</label>
<select name="employees" id="employees" required>
<option value="">Select...</option>
<option value="1-10">1-10</option>
<option value="11-50">11-50</option>
<option value="51-200">51-200</option>
<option value="200+">200+</option>
</select>
<label for="message">How can we help?</label>
<textarea name="message" id="message" placeholder="Tell us about your needs..."></textarea>
<button type="submit">Submit</button>
</form>
"""
def main():
use_json = "--json" in sys.argv
args = [a for a in sys.argv[1:] if a != "--json"]
if args and os.path.isfile(args[0]):
with open(args[0]) as f:
html = f.read()
else:
if not args:
print("[Demo mode — analyzing sample lead capture form]")
html = SAMPLE_HTML
parser = FormAnalyzer()
parser.feed(html)
if not parser.forms:
print("No <form> elements found in the HTML.")
sys.exit(1)
analyses = [analyze_form(form) for form in parser.forms]
if use_json:
print(json.dumps(analyses, indent=2))
else:
print(format_report(analyses))
if __name__ == "__main__":
main()
Tự động chuyển câu hỏi của nhà sáng lập đến cố vấn C-level phù hợp hoặc phiên họp hội đồng cho chủ đề đa vai trò.
---
name: "founder-mode"
description: "/cs:founder-mode <question> — Auto-routes any founder question to the right C-role advisor or to /cs:boardroom for multi-role topics. The single-command entry point."
---
# /cs:founder-mode — The Auto-Router
**Command:** `/cs:founder-mode <question>`
The single command a founder needs to remember. Routes the question to the right C-role automatically, or triggers `/cs:boardroom` if multi-role.
This is the **killer command** — the answer to "I don't know which slash command to use." Type the question; the system figures out the room.
## Routing Logic
The router (via `cs-chief-of-staff`) does keyword + intent matching:
| Signal in question | Route |
|---|---|
| burn, runway, fundraise, dilution, model, LTV, CAC | `cs-cfo-advisor` |
| pipeline, win rate, forecast, NRR, churn, ramp | `cs-cro-advisor` |
| positioning, ICP, message, brand, channel, campaign | `cs-cmo-advisor` |
| roadmap, PMF, JTBD, North Star, RICE, kill | `cs-cpo-advisor` |
| cadence, OKR, scorecard, DRI, operating system, rhythm | `cs-coo-advisor` |
| hiring, comp, ladder, level, attrition, eNPS, equity | `cs-chro-advisor` |
| security, threat, breach, compliance, audit, SOC 2 | `cs-ciso-advisor` |
| architecture, scaling, tech debt, SLO, latency | `cs-cto-advisor` |
| contract, IP, term sheet, regulator, license | `/cs:gc-review` |
| strategy, vision, board, M&A, raise, exit | `cs-ceo-advisor` |
| **2+ signals from different roles** | `/cs:boardroom` |
| **ambiguous** | `/cs:office-hours` first, then route |
## Workflow
1. Parse the question for role signals
2. If exactly one role: invoke that cs-* agent directly
3. If 2+ roles: build a brief via `/cs:brief` and trigger `/cs:boardroom`
4. If ambiguous / no signal match: trigger `/cs:office-hours` to force the founder to sharpen
5. Log the routing decision (raw layer) via `decision-logger`
## Output
The router emits one of three responses:
### Single-role route
```
**Routing:** cs-cfo-advisor
**Why:** Question hits burn rate and unit economics.
**Next:** Invoking cs-cfo-advisor with company-context loaded.
[Advisor's response follows]
```
### Multi-role route
```
**Routing:** /cs:boardroom
**Why:** Question touches CFO + CMO + CPO (pricing change has finance, positioning, and product implications).
**Next:** Building brief via /cs:brief, then running boardroom.
Brief saved: ~/.claude/briefs/2026-05-12-pricing-v3.md
Run: /cs:boardroom ~/.claude/briefs/2026-05-12-pricing-v3.md
```
### Ambiguous → office hours
```
**Routing:** /cs:office-hours
**Why:** Question is too broad ("should we grow faster?"). Need framing before any advisor can help.
**Next:** Six-question intake.
[Office hours questions follow]
```
## Why This Is the Killer Command
gstack requires the founder to know all 23 slash commands and pick the right one. That's a cognitive tax. `/cs:founder-mode` collapses that to one — the system picks. This is also where persistent memory pays off: with company-context.md + decision-logger, the router knows what's already been decided and won't re-litigate.
## Examples
```
/cs:founder-mode "should we raise a Series B now or wait 6 months?"
→ boardroom (CFO + CEO + CRO touched)
/cs:founder-mode "the win rate dropped 20% this month"
→ cs-cro-advisor
/cs:founder-mode "let's hire a VP Marketing"
→ boardroom (CHRO + CMO + CFO touched)
/cs:founder-mode "should we be growing faster?"
→ /cs:office-hours (too ambiguous)
```
## Related
- Agent: [`cs-chief-of-staff`](../../agents/cs-chief-of-staff.md) — does the routing
- Skill: [`chief-of-staff`](../../../skills/chief-of-staff/SKILL.md) — routing logic
- Skill: [`context-engine`](../../../skills/context-engine/SKILL.md) — loads context
---
**Version:** 1.0.0
Xây công cụ miễn phí (máy tính, bộ tạo, trình kiểm tra) để tạo lead, tăng giá trị SEO và nhận diện thương hiệu.
---
name: "free-tool-strategy"
description: "When the user wants to build a free tool for marketing — lead generation, SEO value, or brand awareness. Use when they mention 'engineering as marketing,' 'free tool,' 'calculator,' 'generator,' 'checker,' 'grader,' 'marketing tool,' 'lead gen tool,' 'build something for traffic,' 'interactive tool,' or 'free resource.' Covers idea evaluation, tool design, and launch strategy. For pure SEO content strategy (no tool), use seo-audit or content-strategy instead."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: marketing
updated: 2026-03-06
---
# Free Tool Strategy
You are a growth engineer who has built and launched free tools that generated hundreds of thousands of visitors, thousands of leads, and hundreds of backlinks without a single paid ad. You know which ideas have legs and which waste engineering time. Your goal is to help decide what to build, how to design it for maximum value and lead capture, and how to launch it so people actually find it.
## Before Starting
**Check for context first:**
If `marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered.
Gather this context (ask if not provided):
### 1. Product & Audience
- What's your core product and who buys it?
- What problem does your ideal customer have that a free tool could solve adjacently?
- What does your audience search for that isn't your product?
### 2. Resources
- How much engineering time can you dedicate? (Hours, days, weeks)
- Do you have design resources, or is this no-code/template?
- Who maintains the tool after launch?
### 3. Goals
- Primary goal: SEO traffic, lead generation, backlinks, or brand awareness?
- What does a "win" look like? (X leads/month, Y backlinks, Z organic visitors)
---
## How This Skill Works
### Mode 1: Evaluate Tool Ideas
You have one or more ideas and you're not sure which to build — or whether to build any of them.
**Workflow:**
1. Score each idea against the 6-factor evaluation framework
2. Identify the highest-potential idea based on your specific goals and resources
3. Validate with keyword data before committing engineering time
### Mode 2: Design the Tool
You've decided what to build. Now design it to maximize value, lead capture, and shareability.
**Workflow:**
1. Define the core value exchange (what the user inputs → what they get back)
2. Design the UX for minimum friction
3. Plan lead capture: where, what to ask, progressive profiling
4. Design shareable output (results page, generated report, embeddable badge)
5. Plan the SEO landing page structure
### Mode 3: Launch and Measure
You've built it. Now distribute it and track whether it's working.
**Workflow:**
1. Pre-launch: SEO landing page, schema markup, submit to directories
2. Launch channels: Product Hunt, Hacker News, industry newsletters, social
3. Outreach: who links to similar tools? → build a link acquisition list
4. Measurement: set up tracking for usage, leads, organic traffic, backlinks
5. Iterate: usage data tells you what to improve
---
## Tool Types and When to Use Each
| Tool Type | What It Does | Build Complexity | Best For |
|-----------|-------------|-----------------|---------|
| **Calculator** | Takes inputs, outputs a number or range | Low–Medium | LTV, ROI, pricing, salary, savings |
| **Generator** | Creates text, ideas, or structured content | Low (template) – High (AI) | Headlines, bios, copy, names, reports |
| **Checker** | Analyzes a URL, text, or file and scores/audits it | Medium–High | SEO audit, readability, compliance, spelling |
| **Grader** | Scores something against a rubric | Medium | Website grade, email grade, sales page score |
| **Converter** | Transforms input from one format to another | Low–Medium | Units, formats, currencies, time zones |
| **Template** | Pre-built fillable documents | Very Low | Contracts, briefs, decks, roadmaps |
| **Interactive Visualization** | Shows data or concepts visually | High | Market maps, comparison charts, trend data |
See [references/tool-types-guide.md](references/tool-types-guide.md) for detailed examples, build guides, and complexity breakdowns per type.
---
## The 6-Factor Evaluation Framework
Score each idea 1–5 on each factor. Highest total = build first.
| Factor | What to Check | 1 (weak) | 5 (strong) |
|--------|--------------|----------|-----------|
| **Search Volume** | Monthly searches for "free [tool]" | <100/mo | >5k/mo |
| **Competition** | Quality of existing free tools | Excellent tools exist | No good free alternatives |
| **Build Effort** | Engineering time required | Months | Days |
| **Lead Capture Potential** | Can you naturally gate or capture email? | Forced gate, kills UX | Natural fit (results emailed, report downloaded) |
| **SEO Value** | Can you build topical authority + backlinks? | Thin, one-page utility | Deep use case, link magnet |
| **Viral Potential** | Will users share results or embed the tool? | Nobody shares | Results are shareable by design |
**Scoring guide:**
- 25–30: Build it, now
- 18–24: Strong candidate, validate keyword volume first
- 12–17: Maybe, if resources are low or it fits a strategic gap
- <12: Pass, or rethink the concept
---
## Design Principles
### Value Before Gate
Give the core value first. Gate the upgrade — the deeper report, the saved results, the email delivery. If the tool is only valuable after they give you their email, you've designed a lead form, not a tool.
**Good:** Show the score immediately → offer to email the full report
**Bad:** "Enter your email to see your results"
### Minimal Friction
- Max 3 inputs to get initial results
- No account required for the core value
- Progressive disclosure: simple first, detailed on request
- Mobile-optimized — 50%+ of tool traffic is mobile
### Shareable Results
Design results so users want to share them:
- Unique results URL that others can visit
- "Tweet your score" / "Copy your results" buttons
- Embed code for badges or widgets
- Downloadable report (PDF or CSV)
- Social-ready image generation (score card, certificate)
### Mobile-First
- Inputs work on touch screens
- Results render cleanly on mobile
- Share buttons trigger native share sheet
- No hover-dependent UI
---
## Lead Capture — When, What, How
### When to Gate
**Gate with email when:**
- Results are complex enough to warrant a "report" framing
- Tool produces ongoing value (track over time, re-run monthly)
- Results are personalized and users would naturally want to save them
**Don't gate when:**
- Core result is a single number or short answer
- Competition offers the same thing without a gate
- Your primary goal is SEO/backlinks (gates hurt time-on-page and links)
### What to Ask
Ask the minimum. Every field drops completion by ~10%.
**First gate:** Email only
**Second gate (on re-use or report download):** Name + Company size + Role
### Progressive Profiling
Don't ask everything at once. Build the profile over multiple sessions:
- Session 1: Email to save results
- Session 2: Role, use case (asked contextually, not in a form)
- Session 3: Company, team size (if they request team features)
---
## SEO Strategy for Free Tools
### Landing Page Structure
```
H1: [Free Tool Name] — [What It Does] [one phrase]
Subhead: [Who it's for] + [what problem it solves]
[The Tool — above the fold]
H2: How [Tool Name] works
H2: Why [audience] use [tool name]
H2: [Related Question 1]
H2: [Related Question 2]
H2: Frequently Asked Questions
```
Target keyword in: H1, URL slug, meta title, first 100 words, at least 2 subheadings.
### Schema Markup
Add `SoftwareApplication` schema to tell Google what the page is:
```json
{
"@type": "SoftwareApplication",
"name": "Tool Name",
"applicationCategory": "BusinessApplication",
"offers": {"@type": "Offer", "price": "0"},
"description": "..."
}
```
### Link Magnet Potential
Tools attract links from:
- Resource pages ("best free tools for X")
- Blog posts ("the tools I use for X")
- Subreddits, Slack communities, Facebook groups
- Weekly newsletters in your niche
Plan your outreach list before launch. Who writes about tools in your category? Find their existing "best tools" posts and reach out post-launch.
---
## Measurement
Track these from day one:
| Metric | What It Tells You | Tool |
|--------|------------------|------|
| Tool usage (sessions, completions) | Is anyone using it? | GA4 / Plausible |
| Lead conversion rate | Is it generating leads? | CRM + GA4 events |
| Organic traffic | Is it ranking? | Google Search Console |
| Referring domains | Is it earning links? | Ahrefs / Google GSC |
| Email to paid conversion | Is it generating pipeline? | CRM attribution |
| Bounce rate / time on page | Is the tool actually used? | GA4 |
**Targets at 90 days post-launch:**
- Organic traffic: 500+ sessions/month
- Lead conversion: 5–15% of completions
- Referring domains: 10+ organic backlinks
Run `scripts/tool_roi_estimator.py` to model break-even timeline based on your traffic and conversion assumptions.
---
## Proactive Triggers
Surface these without being asked:
- **Tool requires account before use** → Flag and redesign the gate. This kills SEO, kills virality, and tells users you're harvesting data, not providing value.
- **No shareable output** → If results exist only in the session and can't be shared or saved, you've built half a tool. Flag the missed virality opportunity.
- **No keyword validation** → If the tool concept hasn't been validated against search volume before build, flag — 3 hours of research beats 3 weeks of building a tool nobody searches for.
- **Competitors with the same free tool** → If an existing tool is well-established and free, the bar is "10x better or don't build it." Flag the competitive risk.
- **Single input → single output** → Ultra-simple tools lose SEO value quickly and attract no links. Flag if the tool needs more depth to be link-worthy.
- **No maintenance plan** → Free tools die when the API they call changes or the logic gets stale. Flag the need for a maintenance owner before launch.
---
## Output Artifacts
| When you ask for... | You get... |
|---------------------|------------|
| "Evaluate my tool ideas" | Scored comparison matrix (6 factors × ideas), ranked recommendation with rationale |
| "Design this tool" | UX spec: inputs, outputs, lead capture flow, share mechanics, landing page outline |
| "Write the landing page" | Full landing page copy: H1, subhead, how it works section, FAQ, meta title + description |
| "Plan the launch" | Pre-launch checklist, launch channel list with specific actions, outreach target list |
| "Set up measurement" | GA4 event tracking plan, GSC setup checklist, KPI targets at 30/60/90 days |
| "Is this tool worth building?" | ROI model (using tool_roi_estimator.py): break-even month, required traffic, lead value threshold |
---
## Communication
All output follows the structured communication standard:
- **Bottom line first** — recommendation before reasoning
- **Numbers-grounded** — traffic targets, conversion rates, ROI projections tied to your inputs
- **Confidence tagging** — 🟢 validated / 🟡 estimated / 🔴 assumed
- **Build decisions are binary** — "build it" or "don't build it" with a clear reason, not "it depends"
---
## Related Skills
- **seo-audit**: Use for auditing existing pages and keyword strategy. NOT for building new tool-based content assets.
- **content-strategy**: Use for planning the overall content program (blogs, guides, whitepapers). NOT for tool-specific lead generation.
- **copywriting**: Use when writing the marketing copy for the tool landing page. NOT for the tool UX design or lead capture strategy.
- **launch-strategy**: Use when planning the full product or feature launch. NOT for tool-specific distribution (use free-tool-strategy for that).
- **analytics-tracking**: Use when implementing the measurement stack for the tool. NOT for deciding what to measure (use free-tool-strategy for that).
- **form-cro**: Use when optimizing the lead capture form in the tool. NOT for the tool design or launch strategy.
FILE:references/launch-playbook.md
# Launch Playbook — How to Launch a Free Tool for Maximum Impact
A free tool with no distribution is just code sitting on a server. This playbook gives you the launch sequence that turns a new tool into traffic, leads, and backlinks.
---
## The Launch Mindset
Most companies "launch" by posting it on LinkedIn and waiting. That gets you 200 visits from your existing followers and then nothing.
A real launch is a 4-week sustained distribution campaign. You're not announcing — you're seeding. Every channel you touch plants a seed that compounds over months (especially for SEO).
---
## Pre-Launch Checklist (1–2 Weeks Before)
### SEO Foundations
- [ ] Target keyword researched and confirmed (search volume + low-medium competition)
- [ ] URL slug locked: `/tools/[keyword-rich-name]`
- [ ] Meta title written: "[Free Tool Name] — [What It Does] | [Brand]"
- [ ] Meta description written: 155 chars, includes target keyword, tells user what they get
- [ ] H1 matches search intent, not just brand name
- [ ] `SoftwareApplication` schema markup added (see SKILL.md)
- [ ] Internal links from related content pointing to the tool page
- [ ] Tool page links to 2-3 related resources on your site
### Tool Quality Gate
- [ ] Core value delivered in ≤3 user inputs
- [ ] Results render on mobile
- [ ] Results are shareable (unique URL, copy button, or social share)
- [ ] Lead capture is in place (but gated after value, not before)
- [ ] Email delivery working if you're sending results via email
- [ ] Error handling — what happens with bad inputs?
- [ ] Load time <3 seconds (tools with slow loads have brutal bounce rates)
### Analytics Setup
- [ ] GA4 (or Plausible) tracking installed
- [ ] Key events tracked: tool_started, tool_completed, lead_captured, result_shared
- [ ] Google Search Console verified
- [ ] Heatmap tool installed (Hotjar or Microsoft Clarity) to watch real usage
### Outreach List Ready
- [ ] List of 20-50 sites that link to similar free tools (from Ahrefs / Google "site:domain resources")
- [ ] List of newsletters in your category that feature tools
- [ ] List of subreddits and communities where your audience hangs out
- [ ] Influencers or thought leaders who regularly share tools in your space
---
## Launch Week — The Sequence
### Day 1: SEO and Directories
- Submit tool to Google Search Console (Request Indexing)
- Submit to Bing Webmaster Tools
- Submit to relevant online directories (AlternativeTo, Product Hunt upcoming, SaaSHub, Capterra if applicable)
- Post in your company's blog (a 600-900 word post explaining the tool, linking to it)
### Day 2: Product Hunt
- Submit to Product Hunt at midnight PST (Thursday or Tuesday for best timing)
- Have your team and early fans upvote in the first 2 hours
- Respond to every comment personally — PH algorithm rewards engagement
- Ask your top customers to upvote (personalized message, not mass email)
- Product Hunt tip: the thumbnail image and tagline matter more than the description
### Day 3: Community Seeding (No Pitch)
- Post in relevant subreddits — share as a resource, not a promotion
- Frame: "I built this free [tool type] for [audience] because I couldn't find one — feedback welcome"
- No "check out our new tool" — that's spam and gets removed
- Share in Slack communities in your industry
- Share in relevant Facebook groups
- LinkedIn post — personal post from founder, not company page (personal posts get 10× the reach)
### Day 4: Email to Your List
- Dedicated email to your subscriber list introducing the tool
- Subject line: "Free [Tool Name] — [benefit in 5 words]"
- Keep it short: what it is, why you built it, one sentence result, link
- Ask them to share with one person who'd benefit
### Day 5: Hacker News
- Post to HN with a "Show HN:" prefix: `Show HN: [Tool Name] — [what it does in one line]`
- HN community responds well to honest builder posts with a unique angle
- Must be technically interesting or niche — generic marketing tools don't land
- Be available to answer technical questions in the thread all day
### Day 6-7: Social Amplification
- Twitter/X thread: "I built a free [tool] for [audience]. Here's how it works:" → walkthrough with screenshots
- Short-form video (LinkedIn/TikTok): screen recording of yourself using the tool
- Reach out to 5 people who you know will love it — personal message, not mass email
---
## Post-Launch: Weeks 2-4
### Backlink Outreach
This is where the long-term SEO value comes from.
**Identify targets:**
1. Search Google: `"best free tools for [your category]"` — email everyone on that list
2. Use Ahrefs: find pages linking to similar tools → those same pages may link to yours
3. Search: `"[competitor tool name]" site:[niche blog]` — those bloggers are interested in tools like yours
**Outreach template:**
```
Subject: Free [Tool Name] that might fit your "[Resource List Title]" post
Hi [Name],
I noticed your post on the best free tools for [category]. I recently built [Tool Name]
— it helps [audience] [specific outcome] without [common pain point].
[Direct link to tool]
Would it fit your list? Happy to give you early access or a custom embed if that's useful.
[Your name]
```
**Volume:** 50-100 personalized outreach emails in the first 30 days. Expect 5-15% positive response. One good resource page link is worth 50 generic directory submissions.
### Content That Multiplies
- Write a guide that uses the tool as a central reference: "How to [goal] — with a free calculator to check your numbers"
- Create a results-based case study: "We analyzed 500 [things] with our [tool] — here's what we found"
- Partner with a newsletter: offer to write a guest post that features the tool as the main resource
---
## Measurement — First 90 Days
### Weekly Check-ins (GA4 + GSC)
| Week | What to Look For |
|------|----------------|
| 1 | Direct traffic from launch channels. Tool completion rate (anything under 40% means fix UX) |
| 2-4 | Product Hunt/HN traffic tailing off. Backlinks starting to trickle in. |
| 5-8 | First organic impressions in GSC. Check what queries are sending traffic. |
| 9-12 | Organic traffic should be visible. Lead capture rate should be stable. |
### The "Is It Working?" Test at 90 Days
| Metric | Needs Work | Good | Great |
|--------|-----------|------|-------|
| Organic sessions/month | <200 | 500–2,000 | >5,000 |
| Tool completion rate | <30% | 40–60% | >70% |
| Lead conversion rate (completions → email) | <3% | 5–15% | >20% |
| Referring domains (backlinks) | <5 | 10–30 | >50 |
---
## When a Launch Flops
A tool can fail to gain traction for 4 reasons:
1. **Wrong keyword** — nobody searches for this. Check GSC; if you have zero impressions after 60 days, the keyword target is wrong. Pivot the page copy to a related term with volume.
2. **Wrong problem** — the tool exists, but it's not solving an acute enough problem. Talk to 5 people who used it and didn't return. What were they hoping for?
3. **Gated too early** — traffic is high but completion is low. You're asking for email before delivering value. Remove or move the gate.
4. **Distribution failure** — the tool is fine, but you only posted it once. Run the backlink outreach again with a fresh list. Submit to 10 new directories. Write the guide post.
Most "failed" tools aren't actually failures — they just didn't get the 90-day distribution campaign they needed.
---
## Tools That Keep Working (Maintenance)
A free tool is a 3-year investment, not a 3-week campaign.
**Monthly:**
- Check tool is still functioning (APIs, URLs, formulas)
- Review top search queries in GSC → update H2s and content to match
- Add one new feature based on user requests (check support inbox)
**Quarterly:**
- Update any data the tool uses (benchmarks, averages, rates)
- Refresh the landing page copy — Google rewards freshness
- Identify 20 new backlink targets and run outreach
**Annually:**
- Full UX review — does it still work on the latest mobile browsers?
- Competitive audit — are better free alternatives emerging?
- Decide: invest more, maintain as-is, or retire and redirect
FILE:references/tool-types-guide.md
# Tool Types Guide — Comprehensive Reference for Free Marketing Tools
Each tool type explained with examples, build complexity, typical outcomes, and design guidance.
---
## The 7 Tool Types
### 1. Calculators
**What they do:** Take numerical or categorical inputs → output a calculated result (a number, range, or score).
**Examples:**
- SaaS Pricing Calculator ("What should you charge?")
- ROI Calculator ("How much would you save?")
- LTV Calculator ("What's your customer worth?")
- Churn Impact Calculator ("What does 1% more churn cost you?")
- Salary Calculator by role/location/experience
**Build complexity:** Low–Medium
- Simple formula: 1-2 days of dev
- Multi-variable model: 1-2 weeks
**Lead potential:** High — people want to save or email complex results.
**SEO value:** Medium-High — calculators earn links from resource pages and ranking for "[topic] calculator" queries.
**Viral potential:** Medium — people share results when they're surprising or validating.
**Design tips:**
- Sliders are more satisfying than input fields for numerical ranges
- Show results dynamically (real-time as they adjust inputs)
- Include a "how this was calculated" section for credibility
- Email results: "Send this to myself" captures the lead naturally
**What makes a calculator link-worthy:**
The underlying model must be credible. If you're calculating LTV, show your formula and cite your assumptions. A calculator with methodology is shareable content, not just a widget.
---
### 2. Generators
**What they do:** Take inputs (topic, style, parameters) → output structured text or content.
**Examples:**
- Headline Generator (input: product + audience → 10 headline options)
- LinkedIn Bio Generator
- Job Description Generator
- Email Subject Line Generator
- Product Description Generator
- Business Name Generator
**Build complexity:** Low (template-based) to High (LLM-powered)
**Template-based (madlibs):**
- 1-3 days
- Take inputs, fill template slots, combine with variations
- Deterministic output
**LLM-powered:**
- 1-2 weeks (API integration + prompt engineering)
- Generative output
- Requires API key costs to be modeled into business case
**Lead potential:** Medium — output varies, so gating with email is natural if you offer "save and regenerate."
**SEO value:** High for "[topic] generator free" — some of the highest-traffic tools are generators.
**Viral potential:** High — people share clever or surprisingly good generated outputs.
**Design tips:**
- Show an example output before the user enters anything (reduces bounce)
- Generate 3-5 variations, not just 1
- "Copy to clipboard" button is a must
- "Generate again" encourages engagement (more pageviews, better SEO signal)
---
### 3. Checkers
**What they do:** Analyze a URL, email, text, file, or domain → return an audit or pass/fail assessment.
**Examples:**
- SEO Checker ("Analyze your page's SEO")
- Email Spam Checker ("Will your email hit spam?")
- Website Speed Checker
- LinkedIn Profile Checker
- Ad Copy Compliance Checker
- Password Strength Checker
- Domain Authority Checker
**Build complexity:** Medium–High
- Text analysis (readability, keyword density): 2-5 days
- URL crawling (page analysis): 1-2 weeks
- Email delivery testing: 1-2 weeks + email infrastructure
**Lead potential:** High — checker results are specific to the user; saves/exports feel natural.
**SEO value:** Very High — "[type] checker" or "check my [thing]" queries are often high-volume.
**Viral potential:** High — "Your page scored 47/100 — here's what's broken" drives sharing.
**Design tips:**
- Score the output (0-100) — people anchor on scores and compare
- Categorize results: Critical / Warnings / Passed
- Prioritize issues — don't just list everything, rank by impact
- Loading state matters — show progress (feels like analysis is happening)
---
### 4. Graders
**What they do:** Score something holistically against a rubric. More opinionated than a checker — you're grading against a defined standard.
**Examples:**
- Website Grader (HubSpot's classic)
- Sales Page Grader
- Email Newsletter Grader
- LinkedIn Company Page Grader
- Onboarding Flow Grader
- Pricing Page Grader
**Build complexity:** Medium
- Define the rubric first (the criteria matter more than the tech)
- Usually 1-2 weeks
**Lead potential:** Very High — graders feel like getting a report card; people want the full results.
**SEO value:** High for niche graders ("sales page grader" etc.).
**Viral potential:** Medium-High — share your score as social proof or to invite critique.
**Design tips:**
- The grade (A-F or 0-100) is the hook — show it prominently
- Break down the grade into components (e.g., "Design: A, Copy: C, CTA: D")
- Each component should explain why and how to improve it
- The improvement advice is where the lead capture is earned
---
### 5. Converters
**What they do:** Transform input from one format to another. Pure utility.
**Examples:**
- Markdown to HTML Converter
- Timestamp Converter
- CSV to JSON Converter
- Video Frame Rate Converter
- UTC to Local Time Converter
- File Format Converter
- Currency Converter
**Build complexity:** Very Low – Low
- Most conversions are 1-2 days
- Pure client-side (no server needed)
**Lead potential:** Low — pure utility, low friction reason to capture email.
**SEO value:** Medium — "convert X to Y" queries exist but are dominated by large tool sites.
**Viral potential:** Low — people bookmark and return, don't share.
**When to build:** Only if the conversion is specific to your audience (e.g., a SaaS for designers building a "Figma token to CSS converter"). Generic converters are dominated by free sites with years of SEO authority.
---
### 6. Templates
**What they do:** Pre-built, fillable documents that users download, copy, or use.
**Examples:**
- Job Description Templates
- Product Roadmap Template
- SaaS Metrics Dashboard Template (Google Sheets)
- Email Sequence Template
- SEO Content Brief Template
- Brand Voice Guide Template
- Engineering RFP Template
**Build complexity:** Very Low
- Template creation: hours to 1 day
- Hosting: Google Docs/Sheets share, Notion public page, or downloadable PDF
**Lead potential:** Very High — download = natural lead capture (email to send the file).
**SEO value:** High — "[role] template" queries are competitive but high-intent.
**Viral potential:** Medium — people share templates that save them real time.
**Design tips:**
- The template itself is the product — make it excellent
- Include instructions inside the template
- Offer a "filled example" so users understand what it should look like
- Update templates seasonally to keep them ranking
---
### 7. Interactive Visualizations
**What they do:** Show data, concepts, or comparisons in a visual, explorable way.
**Examples:**
- SaaS Market Map (interactive, filterable)
- Marketing Funnel Visualizer
- Company Comparison Tool (filter by size, location, tech stack)
- Real-Time Industry Benchmark Dashboard
- Interactive Pricing Comparison
**Build complexity:** High
- 2-6 weeks typically
- Requires data (your own research, public datasets, or API)
- May require ongoing data maintenance
**Lead potential:** Medium — users engage deeply but email capture isn't always natural.
**SEO value:** Very High if data-driven — journalists and bloggers link to unique datasets.
**Viral potential:** Very High if the data is surprising or highly visual — these are your link magnets.
**Design tips:**
- The data is the moat — if you have unique data, this is the highest-leverage tool type
- Interactive beats static for time-on-page
- Make it embeddable (embed code button) for backlink acquisition
- Update the data regularly — stale data kills backlinks when someone discovers it
---
## Build vs. No-Code Decision Guide
| Tool Type | No-Code Options | When to Go Custom Dev |
|-----------|---------------|----------------------|
| Calculator | Outgrow, Calconic, Typeform | When logic is complex, or brand/speed matters |
| Generator | Typeform + Zapier, GPT wrappers | When you need custom LLM behavior |
| Checker | Limited — usually needs dev | Always (URL crawling, text analysis) |
| Grader | Outgrow, Involve.me | When the rubric is fixed and simple |
| Converter | Findable no-code tools | Rarely — utility tools are trivially buildable |
| Template | Google Docs, Notion, Canva | When document quality matters |
| Visualization | Flourish, Observable | When data is complex or interactive |
---
## What Makes a Tool "10x Better Than the Existing Free Option"
If there's already a free tool for the job, you need a compelling reason to build yours. One of:
1. **Niche specificity** — existing tool is generic, yours is specific to your audience's workflow
2. **Better UX** — existing tools are ugly, clunky, or require too many steps
3. **Integrated action** — after results, existing tools drop the user; yours offers next steps or a trial
4. **Unique data or model** — your checker uses proprietary data that others don't have
5. **Shareable output** — existing tools give results in a table; yours generates a shareable card or PDF
Don't build "the same tool, but ours." That's a traffic fight you won't win. Build "the tool that does what the others don't."
FILE:scripts/tool_roi_estimator.py
#!/usr/bin/env python3
"""
tool_roi_estimator.py — Estimates ROI of building a free marketing tool.
Models the return from a free tool given build cost, maintenance, expected traffic,
conversion rate, and lead value. Outputs ROI timeline, break-even month, and
minimum traffic needed to justify the investment.
Usage:
python3 tool_roi_estimator.py # runs embedded sample
python3 tool_roi_estimator.py params.json # uses your params
echo '{"build_cost": 5000, "lead_value": 200}' | python3 tool_roi_estimator.py
JSON input format:
{
"build_cost": 5000, # One-time engineering cost ($) — dev time × rate
"monthly_maintenance": 150, # Ongoing server, API, ops cost per month ($)
"traffic_month_1": 500, # Expected organic sessions in month 1
"traffic_growth_rate": 0.15, # Monthly organic traffic growth rate (0.15 = 15%)
"tool_completion_rate": 0.55, # % of visitors who complete the tool (0.55 = 55%)
"lead_capture_rate": 0.10, # % of completions who give email (0.10 = 10%)
"lead_to_trial_rate": 0.08, # % of leads who start a trial
"trial_to_paid_rate": 0.25, # % of trials who become paid customers
"ltv": 1200, # Customer LTV ($)
"months_to_model": 24, # How many months to project
"seo_ramp_months": 3, # Months before organic traffic kicks in (0 if PH/HN spike)
"backlink_value_monthly": 200, # Estimated value of earned backlinks (DA × niche rate)
"tool_name": "ROI Calculator" # For display only
}
"""
import json
import math
import sys
# ---------------------------------------------------------------------------
# Core calculations
# ---------------------------------------------------------------------------
def traffic_at_month(params, month):
"""
Traffic grows from near-zero during SEO ramp, then compounds.
Month 1 = launch spike (Product Hunt / HN etc.) if ramp=0, or baseline.
"""
ramp = params.get("seo_ramp_months", 3)
base = params["traffic_month_1"]
growth = params["traffic_growth_rate"]
if month <= ramp:
# Linear ramp to base traffic during SEO warmup
return round(base * (month / ramp), 0) if ramp > 0 else base
else:
# Compound growth after ramp
months_since_ramp = month - ramp
return round(base * ((1 + growth) ** months_since_ramp), 0)
def leads_at_month(params, sessions):
completion_rate = params["tool_completion_rate"]
lead_capture_rate = params["lead_capture_rate"]
completions = sessions * completion_rate
leads = completions * lead_capture_rate
return round(leads, 1)
def customers_at_month(params, leads):
trial_rate = params["lead_to_trial_rate"]
paid_rate = params["trial_to_paid_rate"]
customers = leads * trial_rate * paid_rate
return round(customers, 2)
def revenue_at_month(params, customers):
return round(customers * params["ltv"], 2)
def cost_at_month(params, month):
"""
Month 1: build cost + maintenance.
Subsequent months: maintenance only.
"""
maintenance = params["monthly_maintenance"]
backlink_value = params.get("backlink_value_monthly", 0)
if month == 1:
return params["build_cost"] + maintenance
return maintenance # backlink value is additive, not a cost
def backlink_value_at_month(params, month):
"""Backlinks grow slowly — assume linear ramp over 6 months."""
max_val = params.get("backlink_value_monthly", 0)
ramp = 6
if month >= ramp:
return max_val
return round(max_val * (month / ramp), 2)
def build_projection(params):
months = params["months_to_model"]
rows = []
cumulative_cost = 0
cumulative_revenue = 0
cumulative_backlink_value = 0
for m in range(1, months + 1):
sessions = traffic_at_month(params, m)
leads = leads_at_month(params, sessions)
customers = customers_at_month(params, leads)
revenue = revenue_at_month(params, customers)
cost = cost_at_month(params, m)
bl_value = backlink_value_at_month(params, m)
cumulative_cost += cost
cumulative_revenue += revenue
cumulative_backlink_value += bl_value
total_value = cumulative_revenue + cumulative_backlink_value
cumulative_net = total_value - cumulative_cost
rows.append({
"month": m,
"sessions": int(sessions),
"leads": leads,
"customers": customers,
"revenue": revenue,
"cost": round(cost, 2),
"backlink_value": bl_value,
"cumulative_cost": round(cumulative_cost, 2),
"cumulative_revenue": round(cumulative_revenue, 2),
"cumulative_backlink_value": round(cumulative_backlink_value, 2),
"cumulative_net": round(cumulative_net, 2),
})
return rows
def find_break_even_month(projection):
for row in projection:
if row["cumulative_net"] >= 0:
return row["month"]
return None
def calculate_minimum_traffic(params):
"""
What monthly traffic volume is needed to break even within 12 months?
Solve for traffic where 12-month cumulative net >= 0.
Uses binary search.
"""
target_months = 12
total_cost_12mo = params["build_cost"] + params["monthly_maintenance"] * target_months
# Revenue per session (steady state, month 12)
completion = params["tool_completion_rate"]
lead_cap = params["lead_capture_rate"]
trial = params["lead_to_trial_rate"]
paid = params["trial_to_paid_rate"]
ltv = params["ltv"]
bl_monthly = params.get("backlink_value_monthly", 0)
revenue_per_session = completion * lead_cap * trial * paid * ltv
# Total sessions needed over 12 months (ignoring ramp for simplification)
if revenue_per_session <= 0:
return None
# With backlink value: total_value = sessions_total × revenue_per_session + 12 × bl_monthly
# sessions_total = total needed
total_bl_value = bl_monthly * 12 * 0.5 # ramp factor
needed_from_sessions = max(0, total_cost_12mo - total_bl_value)
min_monthly_sessions = needed_from_sessions / (target_months * 0.6 * revenue_per_session)
# 0.6 factor: first 3 months lower traffic during ramp
return round(min_monthly_sessions, 0)
def calculate_roi_summary(projection, params):
if not projection:
return {}
last = projection[-1]
total_cost = last["cumulative_cost"]
total_revenue = last["cumulative_revenue"]
total_value = total_revenue + last["cumulative_backlink_value"]
net = last["cumulative_net"]
roi = (net / total_cost * 100) if total_cost > 0 else 0
total_leads = sum(r["leads"] for r in projection)
total_customers = sum(r["customers"] for r in projection)
cost_per_lead = total_cost / total_leads if total_leads > 0 else 0
return {
"total_cost": round(total_cost, 2),
"total_revenue": round(total_revenue, 2),
"total_value_with_backlinks": round(total_value, 2),
"net_benefit": round(net, 2),
"roi_pct": round(roi, 1),
"total_leads": round(total_leads, 0),
"total_customers": round(total_customers, 1),
"cost_per_lead": round(cost_per_lead, 2),
}
# ---------------------------------------------------------------------------
# Formatting
# ---------------------------------------------------------------------------
def fc(value):
return f",.2f"
def fp(value):
return f"{value:.1f}%"
def fi(value):
return f"{int(value):,}"
def print_report(params, projection, summary, break_even, min_traffic):
tool_name = params.get("tool_name", "Free Tool")
months = params["months_to_model"]
print("\n" + "=" * 65)
print(f"FREE TOOL ROI ESTIMATOR — {tool_name.upper()}")
print("=" * 65)
print("\n📊 INPUT PARAMETERS")
print(f" Build cost (one-time): {fc(params['build_cost'])}")
print(f" Monthly maintenance: {fc(params['monthly_maintenance'])}")
print(f" Starting monthly traffic: {fi(params['traffic_month_1'])} sessions")
print(f" Monthly traffic growth: {fp(params['traffic_growth_rate'] * 100)}")
print(f" SEO ramp period: {params.get('seo_ramp_months', 3)} months")
print(f" Tool completion rate: {fp(params['tool_completion_rate'] * 100)}")
print(f" Lead capture rate: {fp(params['lead_capture_rate'] * 100)} (of completions)")
print(f" Lead → trial rate: {fp(params['lead_to_trial_rate'] * 100)}")
print(f" Trial → paid rate: {fp(params['trial_to_paid_rate'] * 100)}")
print(f" LTV: {fc(params['ltv'])}")
print(f" Backlink value (monthly): {fc(params.get('backlink_value_monthly', 0))}")
print(f"\n📈 {months}-MONTH SUMMARY")
print(f" Total investment: {fc(summary['total_cost'])}")
print(f" Revenue from leads: {fc(summary['total_revenue'])}")
print(f" Backlink value: {fc(summary.get('total_value_with_backlinks', 0) - summary['total_revenue'])}")
print(f" Total value generated: {fc(summary.get('total_value_with_backlinks', summary['total_revenue']))}")
print(f" Net benefit: {fc(summary['net_benefit'])}")
print(f" ROI: {fp(summary['roi_pct'])}")
print(f"\n🎯 LEAD & CUSTOMER METRICS")
print(f" Total leads generated: {fi(summary['total_leads'])}")
print(f" Total customers acquired: {round(summary['total_customers'], 1)}")
print(f" Cost per lead: {fc(summary['cost_per_lead'])}")
print(f" CAC via tool: {fc(summary['total_cost'] / max(summary['total_customers'], 0.01))}")
print(f"\n⏱ BREAK-EVEN ANALYSIS")
if break_even:
print(f" Break-even month: Month {break_even}")
assessment = "🟢 Fast payback" if break_even <= 6 else "🟡 Moderate" if break_even <= 12 else "🔴 Long payback"
print(f" Assessment: {assessment}")
else:
print(f" Break-even month: Not reached in {months} months ⚠️")
print(f" Action needed: Increase traffic, improve completion/capture rate, or reduce build cost")
if min_traffic:
print(f" Min traffic for 12-mo break-even: {fi(min_traffic)} sessions/month")
current = params["traffic_month_1"]
if current >= min_traffic:
print(f" Your projected traffic ({fi(current)}/mo) exceeds minimum ✅")
else:
gap = min_traffic - current
print(f" Traffic gap: need {fi(gap)} more sessions/month than projected ⚠️")
print(f"\n📅 MONTHLY PROJECTION")
print(f" {'Mo':>3} {'Sessions':>9} {'Leads':>6} {'Custs':>6} {'Revenue':>9} {'Cum Net':>10}")
print(f" {'-'*3} {'-'*9} {'-'*6} {'-'*6} {'-'*9} {'-'*10}")
for row in projection:
net = row["cumulative_net"]
net_str = fc(net) if net >= 0 else f"({fc(abs(net))})"
be_marker = " ← break-even" if row["month"] == break_even else ""
print(f" {row['month']:>3} {fi(row['sessions']):>9} {row['leads']:>6.1f} {row['customers']:>6.2f}"
f" {fc(row['revenue']):>9} {net_str:>10}{be_marker}")
print("\n" + "=" * 65)
# Recommendations
print("\n💡 RECOMMENDATIONS")
roi = summary["roi_pct"]
if roi > 200:
print(" ✅ Strong ROI case — build it, invest in distribution")
elif roi > 50:
print(" 🟡 Positive ROI but slim — validate keyword volume before committing full build cost")
print(" Consider: MVP version (no-code) to test demand before full dev investment")
else:
print(" 🔴 ROI case is weak — investigate:")
print(" 1. Is the target keyword validated? (check search volume)")
print(" 2. Can you reduce build cost? (no-code MVP first)")
print(" 3. Is the lead-to-customer conversion realistic?")
print(" 4. Is the LTV accurate?")
completion = params["tool_completion_rate"]
if completion < 0.40:
print(" ⚠️ Low completion rate — reconsider UX or number of required inputs")
if params["lead_capture_rate"] < 0.05:
print(" ⚠️ Low lead capture — check gate placement (should be after value is delivered)")
if break_even and break_even > 18:
print(" ⚠️ Long break-even — prioritize launch distribution to accelerate traffic ramp")
# ---------------------------------------------------------------------------
# Default sample
# ---------------------------------------------------------------------------
DEFAULT_PARAMS = {
"tool_name": "SaaS ROI Calculator",
"build_cost": 4000,
"monthly_maintenance": 100,
"traffic_month_1": 600,
"traffic_growth_rate": 0.12,
"seo_ramp_months": 3,
"tool_completion_rate": 0.55,
"lead_capture_rate": 0.12,
"lead_to_trial_rate": 0.08,
"trial_to_paid_rate": 0.25,
"ltv": 1400,
"months_to_model": 18,
"backlink_value_monthly": 150,
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
import argparse
parser = argparse.ArgumentParser(
description="Estimates ROI of building a free marketing tool. "
"Models return given build cost, maintenance, traffic, "
"conversion rate, and lead value."
)
parser.add_argument(
"file", nargs="?", default=None,
help="Path to a JSON file with tool parameters. "
"If omitted, reads from stdin or runs embedded sample."
)
args = parser.parse_args()
params = None
if args.file:
try:
with open(args.file) as f:
params = json.load(f)
except Exception as e:
print(f"Error reading file: {e}", file=sys.stderr)
sys.exit(1)
elif not sys.stdin.isatty():
raw = sys.stdin.read().strip()
if raw:
try:
params = json.loads(raw)
except Exception as e:
print(f"Error reading stdin: {e}", file=sys.stderr)
sys.exit(1)
else:
print("No input provided — running with sample parameters.\n")
params = DEFAULT_PARAMS
else:
print("No input provided — running with sample parameters.\n")
params = DEFAULT_PARAMS
# Fill defaults for any missing keys
for k, v in DEFAULT_PARAMS.items():
params.setdefault(k, v)
projection = build_projection(params)
summary = calculate_roi_summary(projection, params)
break_even = find_break_even_month(projection)
min_traffic = calculate_minimum_traffic(params)
print_report(params, projection, summary, break_even, min_traffic)
# JSON output
json_output = {
"inputs": params,
"results": {
"roi_pct": summary["roi_pct"],
"break_even_month": break_even,
"total_leads": summary["total_leads"],
"total_customers": summary["total_customers"],
"cost_per_lead": summary["cost_per_lead"],
"net_benefit": summary["net_benefit"],
"min_monthly_traffic_for_12mo_breakeven": min_traffic,
}
}
print("\n--- JSON Output ---")
print(json.dumps(json_output, indent=2))
if __name__ == "__main__":
main()
Thiết kế kiến trúc GCP: triển khai GKE, Cloud Run, pipeline BigQuery, tối ưu chi phí và di chuyển lên Google Cloud.
---
name: "gcp-cloud-architect"
description: "Design GCP architectures for startups and enterprises. Use when asked to design Google Cloud infrastructure, deploy to GKE or Cloud Run, configure BigQuery pipelines, optimize GCP costs, or migrate to GCP. Covers Cloud Run, GKE, Cloud Functions, Cloud SQL, BigQuery, and cost optimization."
---
# GCP Cloud Architect
Design scalable, cost-effective Google Cloud architectures for startups and enterprises with infrastructure-as-code templates.
---
## Workflow
### Step 1: Gather Requirements
Collect application specifications:
```
- Application type (web app, mobile backend, data pipeline, SaaS)
- Expected users and requests per second
- Budget constraints (monthly spend limit)
- Team size and GCP experience level
- Compliance requirements (GDPR, HIPAA, SOC 2)
- Availability requirements (SLA, RPO/RTO)
```
### Step 2: Design Architecture
Run the architecture designer to get pattern recommendations:
```bash
python scripts/architecture_designer.py --input requirements.json
```
**Example output:**
```json
{
"recommended_pattern": "serverless_web",
"service_stack": ["Cloud Storage", "Cloud CDN", "Cloud Run", "Firestore", "Identity Platform"],
"estimated_monthly_cost_usd": 30,
"pros": ["Low ops overhead", "Pay-per-use", "Auto-scaling", "No cold starts on Cloud Run min instances"],
"cons": ["Vendor lock-in", "Regional limitations", "Eventual consistency with Firestore"]
}
```
Select from recommended patterns:
- **Serverless Web**: Cloud Storage + Cloud CDN + Cloud Run + Firestore
- **Microservices on GKE**: GKE Autopilot + Cloud SQL + Memorystore + Cloud Pub/Sub
- **Serverless Data Pipeline**: Pub/Sub + Dataflow + BigQuery + Looker
- **ML Platform**: Vertex AI + Cloud Storage + BigQuery + Cloud Functions
See `references/architecture_patterns.md` for detailed pattern specifications.
**Validation checkpoint:** Confirm the recommended pattern matches the team's operational maturity and compliance requirements before proceeding to Step 3.
### Step 3: Estimate Cost
Analyze estimated costs and optimization opportunities:
```bash
python scripts/cost_optimizer.py --resources current_setup.json --monthly-spend 2000
```
**Example output:**
```json
{
"current_monthly_usd": 2000,
"recommendations": [
{ "action": "Right-size Cloud SQL db-custom-4-16384 to db-custom-2-8192", "savings_usd": 380, "priority": "high" },
{ "action": "Purchase 1-yr committed use discount for GKE nodes", "savings_usd": 290, "priority": "high" },
{ "action": "Move Cloud Storage objects >90 days to Nearline", "savings_usd": 75, "priority": "medium" }
],
"total_potential_savings_usd": 745
}
```
Output includes:
- Monthly cost breakdown by service
- Right-sizing recommendations
- Committed use discount opportunities
- Sustained use discount analysis
- Potential monthly savings
Use the [GCP Pricing Calculator](https://cloud.google.com/products/calculator) for detailed estimates.
### Step 4: Generate IaC
Create infrastructure-as-code for the selected pattern:
```bash
python scripts/deployment_manager.py --app-name my-app --pattern serverless_web --region us-central1
```
**Example Terraform HCL output (Cloud Run + Firestore):**
```hcl
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
}
provider "google" {
project = var.project_id
region = var.region
}
variable "project_id" {
description = "GCP project ID"
type = string
}
variable "region" {
description = "GCP region"
type = string
default = "us-central1"
}
resource "google_cloud_run_v2_service" "api" {
name = "var.environment-var.app_name-api"
location = var.region
template {
containers {
image = "gcr.io/var.project_id/var.app_name:latest"
resources {
limits = {
cpu = "1000m"
memory = "512Mi"
}
}
env {
name = "FIRESTORE_PROJECT"
value = var.project_id
}
}
scaling {
min_instance_count = 0
max_instance_count = 10
}
}
}
resource "google_firestore_database" "default" {
project = var.project_id
name = "(default)"
location_id = var.region
type = "FIRESTORE_NATIVE"
}
```
**Example gcloud CLI deployment:**
```bash
# Deploy Cloud Run service
gcloud run deploy my-app-api \
--image gcr.io/$PROJECT_ID/my-app:latest \
--region us-central1 \
--platform managed \
--allow-unauthenticated \
--memory 512Mi \
--cpu 1 \
--min-instances 0 \
--max-instances 10
# Create Firestore database
gcloud firestore databases create --location=us-central1
```
> Full templates including Cloud CDN, Identity Platform, IAM, and Cloud Monitoring are generated by `deployment_manager.py` and also available in `references/architecture_patterns.md`.
### Step 5: Configure CI/CD
Set up automated deployment with Cloud Build or GitHub Actions:
```yaml
# cloudbuild.yaml
steps:
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', 'gcr.io/$PROJECT_ID/my-app:$COMMIT_SHA', '.']
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/$PROJECT_ID/my-app:$COMMIT_SHA']
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: gcloud
args:
- 'run'
- 'deploy'
- 'my-app-api'
- '--image=gcr.io/$PROJECT_ID/my-app:$COMMIT_SHA'
- '--region=us-central1'
- '--platform=managed'
images:
- 'gcr.io/$PROJECT_ID/my-app:$COMMIT_SHA'
```
```bash
# Connect repo and create trigger
gcloud builds triggers create github \
--repo-name=my-app \
--repo-owner=my-org \
--branch-pattern="^main$" \
--build-config=cloudbuild.yaml
```
### Step 6: Security Review
Verify security configuration:
```bash
# Review IAM bindings
gcloud projects get-iam-policy $PROJECT_ID --format=json
# Check service account permissions
gcloud iam service-accounts list --project=$PROJECT_ID
# Verify VPC Service Controls (if applicable)
gcloud access-context-manager perimeters list --policy=$POLICY_ID
```
**Security checklist:**
- IAM roles follow least privilege (prefer predefined roles over basic roles)
- Service accounts use Workload Identity for GKE
- VPC Service Controls configured for sensitive APIs
- Cloud KMS encryption keys for customer-managed encryption
- Cloud Audit Logs enabled for all admin activity
- Organization policies restrict public access
- Secret Manager used for all credentials
**If deployment fails:**
1. Check the failure reason:
```bash
gcloud run services describe my-app-api --region us-central1
gcloud logging read "resource.type=cloud_run_revision" --limit=20
```
2. Review Cloud Logging for application errors.
3. Fix the configuration or container image.
4. Redeploy:
```bash
gcloud run deploy my-app-api --image gcr.io/$PROJECT_ID/my-app:latest --region us-central1
```
**Common failure causes:**
- IAM permission errors -- verify service account roles and `--allow-unauthenticated` flag
- Quota exceeded -- request quota increase via IAM & Admin > Quotas
- Container startup failure -- check container logs and health check configuration
- Region not enabled -- enable the required APIs with `gcloud services enable`
---
## Tools
### architecture_designer.py
Recommends GCP services based on workload requirements.
```bash
python scripts/architecture_designer.py --input requirements.json --output design.json
```
**Input:** JSON with app type, scale, budget, compliance needs
**Output:** Recommended pattern, service stack, cost estimate, pros/cons
### cost_optimizer.py
Analyzes GCP resources for cost savings.
```bash
python scripts/cost_optimizer.py --resources inventory.json --monthly-spend 5000
```
**Output:** Recommendations for:
- Idle resource removal
- Machine type right-sizing
- Committed use discounts
- Storage class transitions
- Network egress optimization
### deployment_manager.py
Generates gcloud CLI deployment scripts and Terraform configurations.
```bash
python scripts/deployment_manager.py --app-name my-app --pattern serverless_web --region us-central1
```
**Output:** Production-ready deployment scripts with:
- Cloud Run or GKE deployment
- Firestore or Cloud SQL setup
- Identity Platform configuration
- IAM roles with least privilege
- Cloud Monitoring and Logging
---
## Quick Start
### Web App on Cloud Run (< $100/month)
```
Ask: "Design a serverless web backend for a mobile app with 1000 users"
Result:
- Cloud Run for API (auto-scaling, no cold start with min instances)
- Firestore for data (pay-per-operation)
- Identity Platform for authentication
- Cloud Storage + Cloud CDN for static assets
- Estimated: $15-40/month
```
### Microservices on GKE ($500-2000/month)
```
Ask: "Design a scalable architecture for a SaaS platform with 50k users"
Result:
- GKE Autopilot for containerized workloads
- Cloud SQL (PostgreSQL) with read replicas
- Memorystore (Redis) for session caching
- Cloud CDN for global delivery
- Cloud Build for CI/CD
- Multi-zone deployment
```
### Serverless Data Pipeline
```
Ask: "Design a real-time analytics pipeline for event data"
Result:
- Pub/Sub for event ingestion
- Dataflow (Apache Beam) for stream processing
- BigQuery for analytics and warehousing
- Looker for dashboards
- Cloud Functions for lightweight transforms
```
### ML Platform
```
Ask: "Design a machine learning platform for model training and serving"
Result:
- Vertex AI for training and prediction
- Cloud Storage for datasets and model artifacts
- BigQuery for feature store
- Cloud Functions for preprocessing triggers
- Cloud Monitoring for model drift detection
```
---
## Input Requirements
Provide these details for architecture design:
| Requirement | Description | Example |
|-------------|-------------|---------|
| Application type | What you're building | SaaS platform, mobile backend |
| Expected scale | Users, requests/sec | 10k users, 100 RPS |
| Budget | Monthly GCP limit | $500/month max |
| Team context | Size, GCP experience | 3 devs, intermediate |
| Compliance | Regulatory needs | HIPAA, GDPR, SOC 2 |
| Availability | Uptime requirements | 99.9% SLA, 1hr RPO |
**JSON Format:**
```json
{
"application_type": "saas_platform",
"expected_users": 10000,
"requests_per_second": 100,
"budget_monthly_usd": 500,
"team_size": 3,
"gcp_experience": "intermediate",
"compliance": ["SOC2"],
"availability_sla": "99.9%"
}
```
---
## Output Formats
### Architecture Design
- Pattern recommendation with rationale
- Service stack diagram (ASCII)
- Monthly cost estimate and trade-offs
### IaC Templates
- **Terraform HCL**: Production-ready Google provider configs
- **gcloud CLI**: Scripted deployment commands
- **Cloud Build YAML**: CI/CD pipeline definitions
### Cost Analysis
- Current spend breakdown with optimization recommendations
- Priority action list (high/medium/low) and implementation checklist
---
## Anti-Patterns
| Anti-Pattern | Why It Fails | Better Approach |
|---|---|---|
| Using default VPC for production | No isolation, shared firewall rules | Create custom VPC with private subnets |
| Over-provisioning GKE node pools | Wasted cost on idle capacity | Use GKE Autopilot or cluster autoscaler |
| Storing secrets in environment variables | Visible in Cloud Console, logs | Use Secret Manager with Workload Identity |
| Ignoring sustained use discounts | Missing 20-30% automatic savings | Right-size VMs for consistent baseline usage |
| Single-region deployment for SaaS | One region outage = full downtime | Multi-region with Cloud Load Balancing |
| BigQuery on-demand for heavy workloads | Unpredictable costs at scale | Use BigQuery slots (flat-rate) for consistent workloads |
| Running Cloud Functions for long tasks | 9-minute timeout, cold starts | Use Cloud Run for tasks > 60 seconds |
---
## Cross-References
| Skill | Relationship |
|-------|-------------|
| `engineering-team/aws-solution-architect` | AWS equivalent — same 6-step workflow, different services |
| `engineering-team/azure-cloud-architect` | Azure equivalent — completes the cloud trifecta |
| `engineering-team/senior-devops` | Broader DevOps scope — pipelines, monitoring, containerization |
| `engineering/terraform-patterns` | IaC implementation — use for Terraform modules targeting GCP |
| `engineering/ci-cd-pipeline-builder` | Pipeline construction — automates Cloud Build and deployment |
---
## Reference Documentation
| Document | Contents |
|----------|----------|
| `references/architecture_patterns.md` | 6 patterns: serverless, GKE microservices, three-tier, data pipeline, ML platform, multi-region |
| `references/service_selection.md` | Decision matrices for compute, database, storage, messaging |
| `references/best_practices.md` | Naming, labels, IAM, networking, monitoring, disaster recovery |
FILE:references/architecture_patterns.md
# GCP Architecture Patterns
Reference guide for selecting the right GCP architecture pattern based on application requirements.
---
## Table of Contents
- [Pattern Selection Matrix](#pattern-selection-matrix)
- [Pattern 1: Serverless Web Application](#pattern-1-serverless-web-application)
- [Pattern 2: Microservices on GKE](#pattern-2-microservices-on-gke)
- [Pattern 3: Three-Tier Application](#pattern-3-three-tier-application)
- [Pattern 4: Serverless Data Pipeline](#pattern-4-serverless-data-pipeline)
- [Pattern 5: ML Platform](#pattern-5-ml-platform)
- [Pattern 6: Multi-Region High Availability](#pattern-6-multi-region-high-availability)
---
## Pattern Selection Matrix
| Pattern | Best For | Users | Monthly Cost | Complexity |
|---------|----------|-------|--------------|------------|
| Serverless Web | MVP, SaaS, mobile backend | <50K | $30-400 | Low |
| Microservices on GKE | Complex services, enterprise | 10K-500K | $400-2500 | Medium |
| Three-Tier | Traditional web, e-commerce | 10K-200K | $300-1500 | Medium |
| Data Pipeline | Analytics, ETL, streaming | Any | $100-2000 | Medium-High |
| ML Platform | Training, serving, MLOps | Any | $200-5000 | High |
| Multi-Region HA | Global apps, DR | >100K | 2x single | High |
---
## Pattern 1: Serverless Web Application
### Use Case
SaaS platforms, mobile backends, low-traffic websites, MVPs
### Architecture Diagram
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Cloud CDN │────▶│ Cloud │ │ Identity │
│ (CDN) │ │ Storage │ │ Platform │
└─────────────┘ │ (Static) │ │ (Auth) │
└─────────────┘ └──────┬──────┘
│
┌─────────────┐ ┌─────────────┐ ┌──────▼──────┐
│ Cloud DNS │────▶│ Cloud │────▶│ Cloud Run │
│ (DNS) │ │ Load Bal. │ │ (API) │
└─────────────┘ └─────────────┘ └──────┬──────┘
│
┌──────▼──────┐
│ Firestore │
│ (Database) │
└─────────────┘
```
### Service Stack
| Layer | Service | Configuration |
|-------|---------|---------------|
| Frontend | Cloud Storage + Cloud CDN | Static hosting with HTTPS |
| API | Cloud Run | Containerized API with auto-scaling |
| Database | Firestore | Native mode, pay-per-operation |
| Auth | Identity Platform | Multi-provider authentication |
| CI/CD | Cloud Build | Automated container deployments |
### Terraform Example
```hcl
resource "google_cloud_run_v2_service" "api" {
name = "my-app-api"
location = "us-central1"
template {
containers {
image = "gcr.io/my-project/my-app:latest"
resources {
limits = {
cpu = "1000m"
memory = "512Mi"
}
}
}
scaling {
min_instance_count = 0
max_instance_count = 10
}
}
}
```
### Cost Breakdown (10K users)
| Service | Monthly Cost |
|---------|-------------|
| Cloud Run | $5-25 |
| Firestore | $5-30 |
| Cloud CDN | $5-15 |
| Cloud Storage | $1-5 |
| Identity Platform | $0-10 |
| **Total** | **$16-85** |
### Pros and Cons
**Pros:**
- Scale-to-zero (pay nothing when idle)
- Container-based (no runtime restrictions)
- Built-in HTTPS and custom domains
- Auto-scaling with no configuration
**Cons:**
- Cold starts if min instances = 0
- Firestore query limitations vs SQL
- Vendor lock-in to GCP
---
## Pattern 2: Microservices on GKE
### Use Case
Complex business systems, enterprise applications, platform engineering
### Architecture Diagram
```
┌─────────────┐ ┌─────────────┐
│ Cloud CDN │────▶│ Global │
│ (CDN) │ │ Load Bal. │
└─────────────┘ └──────┬──────┘
│
┌──────▼──────┐
│ GKE │
│ Autopilot │
└──────┬──────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Cloud SQL │ │ Memorystore │ │ Pub/Sub │
│ (Postgres) │ │ (Redis) │ │ (Messaging) │
└─────────────┘ └─────────────┘ └─────────────┘
```
### Service Stack
| Layer | Service | Configuration |
|-------|---------|---------------|
| CDN | Cloud CDN | Edge caching, HTTPS |
| Load Balancer | Global Application LB | Backend services, health checks |
| Compute | GKE Autopilot | Managed node provisioning |
| Database | Cloud SQL PostgreSQL | Regional HA, read replicas |
| Cache | Memorystore Redis | Session, query caching |
| Messaging | Pub/Sub | Async service communication |
### GKE Autopilot Configuration
```yaml
# Deployment manifest
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service
spec:
replicas: 2
selector:
matchLabels:
app: api-service
template:
metadata:
labels:
app: api-service
spec:
serviceAccountName: api-workload-sa
containers:
- name: api
image: us-central1-docker.pkg.dev/my-project/my-app/api:latest
ports:
- containerPort: 8080
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1Gi"
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: db-credentials
key: host
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-service
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
```
### Cost Breakdown (50K users)
| Service | Monthly Cost |
|---------|-------------|
| GKE Autopilot | $150-400 |
| Cloud Load Balancing | $25-50 |
| Cloud SQL | $100-300 |
| Memorystore | $40-80 |
| Pub/Sub | $5-20 |
| **Total** | **$320-850** |
---
## Pattern 3: Three-Tier Application
### Use Case
Traditional web apps, e-commerce, CMS, applications with complex queries
### Architecture Diagram
```
┌─────────────┐ ┌─────────────┐
│ Cloud CDN │────▶│ Global │
│ (CDN) │ │ Load Bal. │
└─────────────┘ └──────┬──────┘
│
┌──────▼──────┐
│ Cloud Run │
│ (or MIG) │
└──────┬──────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Cloud SQL │ │ Memorystore │ │ Cloud │
│ (Database) │ │ (Redis) │ │ Storage │
└─────────────┘ └─────────────┘ └─────────────┘
```
### Service Stack
| Layer | Service | Configuration |
|-------|---------|---------------|
| CDN | Cloud CDN | Edge caching, compression |
| Load Balancer | External Application LB | SSL termination, health checks |
| Compute | Cloud Run or Managed Instance Group | Auto-scaling containers or VMs |
| Database | Cloud SQL (MySQL/PostgreSQL) | Regional HA, automated backups |
| Cache | Memorystore Redis | Session store, query cache |
| Storage | Cloud Storage | Uploads, static assets, backups |
### Cost Breakdown (50K users)
| Service | Monthly Cost |
|---------|-------------|
| Cloud Run / MIG | $80-200 |
| Cloud Load Balancing | $25-50 |
| Cloud SQL | $100-250 |
| Memorystore | $30-60 |
| Cloud Storage | $10-30 |
| **Total** | **$245-590** |
---
## Pattern 4: Serverless Data Pipeline
### Use Case
Analytics, IoT data ingestion, log processing, real-time streaming, ETL
### Architecture Diagram
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Sources │────▶│ Pub/Sub │────▶│ Dataflow │
│ (Apps/IoT) │ │ (Ingest) │ │ (Process) │
└─────────────┘ └─────────────┘ └──────┬──────┘
│
┌─────────────┐ ┌─────────────┐ ┌──────▼──────┐
│ Looker │◀────│ BigQuery │◀────│ Cloud │
│ (Dashbd) │ │(Warehouse) │ │ Storage │
└─────────────┘ └─────────────┘ │ (Data Lake) │
└─────────────┘
```
### Service Stack
| Layer | Service | Purpose |
|-------|---------|---------|
| Ingestion | Pub/Sub | Real-time event capture |
| Processing | Dataflow (Apache Beam) | Stream/batch transforms |
| Warehouse | BigQuery | SQL analytics at scale |
| Storage | Cloud Storage | Raw data lake |
| Visualization | Looker / Looker Studio | Dashboards and reports |
| Orchestration | Cloud Composer (Airflow) | Pipeline scheduling |
### Dataflow Pipeline Example
```python
import apache_beam as beam
from apache_beam.options.pipeline_options import PipelineOptions
options = PipelineOptions([
'--runner=DataflowRunner',
'--project=my-project',
'--region=us-central1',
'--temp_location=gs://my-bucket/temp',
'--streaming'
])
with beam.Pipeline(options=options) as p:
(p
| 'ReadPubSub' >> beam.io.ReadFromPubSub(topic='projects/my-project/topics/events')
| 'ParseJSON' >> beam.Map(lambda x: json.loads(x))
| 'WindowInto' >> beam.WindowInto(beam.window.FixedWindows(60))
| 'WriteBQ' >> beam.io.WriteToBigQuery(
'my-project:analytics.events',
schema='event_id:STRING,event_type:STRING,timestamp:TIMESTAMP',
write_disposition=beam.io.BigQueryDisposition.WRITE_APPEND
))
```
### Cost Breakdown
| Service | Monthly Cost |
|---------|-------------|
| Pub/Sub | $5-30 |
| Dataflow | $30-200 |
| BigQuery (on-demand) | $10-100 |
| Cloud Storage | $5-30 |
| Looker Studio | $0 (free) |
| **Total** | **$50-360** |
---
## Pattern 5: ML Platform
### Use Case
Model training, serving, MLOps, feature engineering
### Architecture Diagram
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ BigQuery │────▶│ Vertex AI │────▶│ Vertex AI │
│ (Features) │ │ (Training) │ │ (Endpoints) │
└─────────────┘ └──────┬──────┘ └─────────────┘
│
┌─────────────┐ ┌──────▼──────┐ ┌─────────────┐
│ Cloud │◀────│ Cloud │────▶│ Vertex AI │
│ Functions │ │ Storage │ │ Pipelines │
│ (Triggers) │ │ (Artifacts) │ │ (MLOps) │
└─────────────┘ └─────────────┘ └─────────────┘
```
### Service Stack
| Layer | Service | Purpose |
|-------|---------|---------|
| Data | BigQuery | Feature engineering, exploration |
| Training | Vertex AI Training | Custom or AutoML training |
| Serving | Vertex AI Endpoints | Online/batch prediction |
| Storage | Cloud Storage | Datasets, model artifacts |
| Orchestration | Vertex AI Pipelines | ML workflow automation |
| Monitoring | Vertex AI Model Monitoring | Drift and skew detection |
### Vertex AI Training Example
```python
from google.cloud import aiplatform
aiplatform.init(project='my-project', location='us-central1')
job = aiplatform.CustomTrainingJob(
display_name='my-model-training',
script_path='train.py',
container_uri='us-docker.pkg.dev/vertex-ai/training/tf-gpu.2-12:latest',
requirements=['pandas', 'scikit-learn'],
)
model = job.run(
replica_count=1,
machine_type='n1-standard-8',
accelerator_type='NVIDIA_TESLA_T4',
accelerator_count=1,
)
endpoint = model.deploy(
deployed_model_display_name='my-model-v1',
machine_type='n1-standard-4',
min_replica_count=1,
max_replica_count=5,
)
```
### Cost Breakdown
| Service | Monthly Cost |
|---------|-------------|
| Vertex AI Training (T4 GPU) | $50-500 |
| Vertex AI Prediction | $30-200 |
| BigQuery | $10-50 |
| Cloud Storage | $5-30 |
| **Total** | **$95-780** |
---
## Pattern 6: Multi-Region High Availability
### Use Case
Global applications, disaster recovery, data sovereignty compliance
### Architecture Diagram
```
┌─────────────┐
│ Cloud DNS │
│(Geo routing)│
└──────┬──────┘
│
┌────────────────┼────────────────┐
│ │
┌──────▼──────┐ ┌──────▼──────┐
│us-central1 │ │europe-west1 │
│ Cloud Run │ │ Cloud Run │
└──────┬──────┘ └──────┬──────┘
│ │
┌──────▼──────┐ ┌──────▼──────┐
│Cloud Spanner│◀── Replication ──▶│Cloud Spanner│
│ (Region) │ │ (Region) │
└─────────────┘ └─────────────┘
```
### Service Stack
| Component | Service | Configuration |
|-----------|---------|---------------|
| DNS | Cloud DNS | Geolocation or latency routing |
| CDN | Cloud CDN | Multiple regional origins |
| Compute | Cloud Run (multi-region) | Deployed in each region |
| Database | Cloud Spanner (multi-region) | Strong global consistency |
| Storage | Cloud Storage (multi-region) | Automatic geo-redundancy |
### Cloud DNS Geolocation Policy
```bash
# Create geolocation routing policy
gcloud dns record-sets create api.example.com \
--zone=my-zone \
--type=A \
--routing-policy-type=GEO \
--routing-policy-data="us-central1=projects/my-project/regions/us-central1/addresses/api-us;europe-west1=projects/my-project/regions/europe-west1/addresses/api-eu"
```
### Cost Considerations
| Factor | Impact |
|--------|--------|
| Compute | 2x (each region) |
| Cloud Spanner | Multi-region 3x regional price |
| Data Transfer | Cross-region replication costs |
| Cloud DNS | Geolocation queries premium |
| **Total** | **2-3x single region** |
---
## Pattern Comparison Summary
### Latency
| Pattern | Typical Latency |
|---------|-----------------|
| Serverless Web | 30-150ms (Cloud Run) |
| GKE Microservices | 15-80ms |
| Three-Tier | 20-100ms |
| Multi-Region | <50ms (regional) |
### Scaling Characteristics
| Pattern | Scale Limit | Scale Speed |
|---------|-------------|-------------|
| Serverless Web | 1000 instances/service | Seconds |
| GKE Microservices | Cluster node limits | Minutes |
| Data Pipeline | Unlimited (Dataflow) | Seconds |
| Multi-Region | Regional limits | Seconds |
### Operational Complexity
| Pattern | Setup | Maintenance | Debugging |
|---------|-------|-------------|-----------|
| Serverless Web | Low | Low | Medium |
| GKE Microservices | Medium | Medium | Medium |
| Three-Tier | Medium | Medium | Low |
| Data Pipeline | High | Medium | High |
| ML Platform | High | High | High |
| Multi-Region | High | High | High |
FILE:references/best_practices.md
# GCP Best Practices
Production-ready practices for naming, labels, IAM, networking, monitoring, and disaster recovery.
---
## Table of Contents
- [Naming Conventions](#naming-conventions)
- [Labels and Organization](#labels-and-organization)
- [IAM and Security](#iam-and-security)
- [Networking](#networking)
- [Monitoring and Logging](#monitoring-and-logging)
- [Cost Optimization](#cost-optimization)
- [Disaster Recovery](#disaster-recovery)
- [Common Pitfalls](#common-pitfalls)
---
## Naming Conventions
### Resource Naming Pattern
```
{environment}-{project}-{resource-type}-{purpose}
Examples:
prod-myapp-gke-cluster
dev-myapp-sql-primary
staging-myapp-run-api
prod-myapp-gcs-uploads
```
### Project Naming
```
{org}-{team}-{environment}
Examples:
acme-platform-prod
acme-platform-dev
acme-data-prod
```
### Naming Rules
| Resource | Format | Max Length | Example |
|----------|--------|-----------|---------|
| Project ID | lowercase, hyphens | 30 chars | acme-platform-prod |
| GKE Cluster | lowercase, hyphens | 40 chars | prod-api-cluster |
| Cloud Run | lowercase, hyphens | 49 chars | prod-myapp-api |
| Cloud SQL | lowercase, hyphens | 84 chars | prod-myapp-sql-primary |
| GCS Bucket | lowercase, hyphens, dots | 63 chars | acme-prod-myapp-uploads |
| Service Account | lowercase, hyphens | 30 chars | myapp-run-sa |
---
## Labels and Organization
### Required Labels
Apply these labels to all resources:
```
labels:
environment: "prod" # dev, staging, prod
team: "platform" # team owning the resource
app: "myapp" # application name
cost-center: "eng-001" # billing allocation
managed-by: "terraform" # terraform, gcloud, console
```
### Label-Based Cost Reporting
```bash
# Export billing data to BigQuery with labels
# Then query by label:
SELECT
labels.value AS environment,
SUM(cost) AS total_cost
FROM `billing_export.gcp_billing_export_v1_*`
CROSS JOIN UNNEST(labels) AS labels
WHERE labels.key = 'environment'
GROUP BY environment
ORDER BY total_cost DESC
```
### Organization Hierarchy
```
Organization
├── Folder: Production
│ ├── Project: platform-prod
│ ├── Project: data-prod
│ └── Project: ml-prod
├── Folder: Non-Production
│ ├── Project: platform-dev
│ ├── Project: platform-staging
│ └── Project: data-dev
└── Folder: Shared Services
├── Project: shared-networking
├── Project: shared-security
└── Project: shared-monitoring
```
---
## IAM and Security
### Principle of Least Privilege
```bash
# BAD: Basic roles are too broad
gcloud projects add-iam-policy-binding my-project \
--member="user:dev@example.com" \
--role="roles/editor"
# GOOD: Use predefined roles
gcloud projects add-iam-policy-binding my-project \
--member="user:dev@example.com" \
--role="roles/run.developer"
```
### Service Account Best Practices
```bash
# 1. Create dedicated SA per workload
gcloud iam service-accounts create myapp-api-sa \
--display-name="MyApp API Service Account"
# 2. Grant only required roles
gcloud projects add-iam-policy-binding my-project \
--member="serviceAccount:myapp-api-sa@my-project.iam.gserviceaccount.com" \
--role="roles/datastore.user"
# 3. Use Workload Identity for GKE (no key files)
gcloud iam service-accounts add-iam-policy-binding \
myapp-api-sa@my-project.iam.gserviceaccount.com \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:my-project.svc.id.goog[default/myapp-api-ksa]"
# 4. NEVER download SA key files in production
# Instead, use attached service accounts or impersonation
```
### VPC Service Controls
```bash
# Create a service perimeter to restrict data exfiltration
gcloud access-context-manager perimeters create my-perimeter \
--title="Production Data Perimeter" \
--resources="projects/123456" \
--restricted-services="bigquery.googleapis.com,storage.googleapis.com" \
--policy=$POLICY_ID
```
### Organization Policies
```bash
# Restrict external IPs on VMs
gcloud resource-manager org-policies set-policy \
--project=my-project policy.yaml
# policy.yaml
constraint: compute.vmExternalIpAccess
listPolicy:
allValues: DENY
# Restrict public Cloud Storage
constraint: storage.publicAccessPrevention
booleanPolicy:
enforced: true
```
### Encryption
| Layer | Service | Default |
|-------|---------|---------|
| At rest | Google-managed keys | Always enabled |
| At rest | CMEK (Cloud KMS) | Optional, recommended |
| In transit | TLS 1.3 | Always enabled |
| Application | Cloud KMS | Encrypt sensitive fields |
```bash
# Create CMEK key for Cloud SQL
gcloud kms keys create myapp-sql-key \
--keyring=myapp-keyring \
--location=us-central1 \
--purpose=encryption
# Use CMEK with Cloud SQL
gcloud sql instances create myapp-db \
--disk-encryption-key=projects/my-project/locations/us-central1/keyRings/myapp-keyring/cryptoKeys/myapp-sql-key
```
---
## Networking
### VPC Design
```bash
# Create custom VPC (avoid default network)
gcloud compute networks create myapp-vpc \
--subnet-mode=custom
# Create subnets with secondary ranges for GKE
gcloud compute networks subnets create myapp-subnet \
--network=myapp-vpc \
--region=us-central1 \
--range=10.0.0.0/20 \
--secondary-range pods=10.4.0.0/14,services=10.8.0.0/20 \
--enable-private-google-access
```
### Shared VPC
Use Shared VPC for multi-project environments:
```
Host Project (shared-networking)
├── VPC: shared-vpc
│ ├── Subnet: prod-us-central1 → Service Project: platform-prod
│ ├── Subnet: prod-europe-west1 → Service Project: platform-prod
│ └── Subnet: dev-us-central1 → Service Project: platform-dev
```
### Firewall Rules
```bash
# Allow internal traffic
gcloud compute firewall-rules create allow-internal \
--network=myapp-vpc \
--allow=tcp,udp,icmp \
--source-ranges=10.0.0.0/8
# Allow health checks from Google load balancers
gcloud compute firewall-rules create allow-health-checks \
--network=myapp-vpc \
--allow=tcp:8080 \
--source-ranges=35.191.0.0/16,130.211.0.0/22 \
--target-tags=allow-health-check
# Deny all other ingress (implicit, but be explicit)
gcloud compute firewall-rules create deny-all-ingress \
--network=myapp-vpc \
--action=DENY \
--rules=all \
--direction=INGRESS \
--priority=65534
```
### Private Google Access
Always enable Private Google Access to reach GCP APIs without public IPs:
```bash
gcloud compute networks subnets update myapp-subnet \
--region=us-central1 \
--enable-private-google-access
```
---
## Monitoring and Logging
### Cloud Monitoring Setup
```bash
# Create uptime check
gcloud monitoring uptime create \
--display-name="API Health Check" \
--resource-type=cloud-run-revision \
--resource-labels="service_name=myapp-api,location=us-central1" \
--check-request-path="/health" \
--period=60s
# Create alerting policy
gcloud alpha monitoring policies create \
--display-name="High Error Rate" \
--condition-display-name="Cloud Run 5xx > 1%" \
--condition-filter='resource.type="cloud_run_revision" AND metric.type="run.googleapis.com/request_count" AND metric.labels.response_code_class="5xx"' \
--condition-threshold-value=1 \
--notification-channels="projects/my-project/notificationChannels/12345"
```
### Key Metrics to Monitor
| Service | Metric | Alert Threshold |
|---------|--------|-----------------|
| Cloud Run | request_latencies (p99) | >2s |
| Cloud Run | request_count (5xx) | >1% of total |
| Cloud SQL | cpu/utilization | >80% |
| Cloud SQL | disk/utilization | >85% |
| GKE | container/cpu/utilization | >80% |
| GKE | node/cpu/allocatable_utilization | >85% |
| Pub/Sub | subscription/oldest_unacked_message_age | >300s |
| BigQuery | query/execution_time | >60s |
### Log-Based Metrics
```bash
# Create a metric for application errors
gcloud logging metrics create app-errors \
--description="Application error count" \
--log-filter='resource.type="cloud_run_revision" AND severity>=ERROR'
# Create log sink to BigQuery for analysis
gcloud logging sinks create audit-logs-bq \
bigquery.googleapis.com/projects/my-project/datasets/audit_logs \
--log-filter='logName="projects/my-project/logs/cloudaudit.googleapis.com%2Factivity"'
```
### Log Exclusion (Cost Reduction)
```bash
# Exclude verbose debug logs to save on Cloud Logging costs
gcloud logging sinks create _Default \
--log-filter='NOT (severity="DEBUG" OR severity="DEFAULT")' \
--description="Exclude debug-level logs"
# Or create exclusion filters
gcloud logging exclusions create exclude-debug \
--log-filter='severity="DEBUG"' \
--description="Exclude debug logs to reduce costs"
```
---
## Cost Optimization
### Committed Use Discounts
| Term | Compute Discount | Memory Discount |
|------|-----------------|-----------------|
| 1 year | 37% | 37% |
| 3 years | 55% | 55% |
```bash
# Check recommendations
gcloud recommender recommendations list \
--project=my-project \
--location=us-central1 \
--recommender=google.compute.commitment.UsageCommitmentRecommender
```
### Sustained Use Discounts
Automatic discounts for resources running >25% of the month:
| Usage | Discount |
|-------|----------|
| 25-50% | 20% |
| 50-75% | 40% |
| 75-100% | 60% |
### BigQuery Cost Control
```sql
-- Use partitioning to limit data scanned
CREATE TABLE my_dataset.events
PARTITION BY DATE(timestamp)
CLUSTER BY event_type
AS SELECT * FROM raw_events;
-- Estimate query cost before running
-- Use --dry_run flag
bq query --dry_run --use_legacy_sql=false \
'SELECT * FROM my_dataset.events WHERE DATE(timestamp) = "2026-01-01"'
```
### Cloud Storage Optimization
```bash
# Enable Autoclass for automatic class management
gsutil mb -l us-central1 --autoclass gs://my-bucket/
# Set lifecycle policy
gsutil lifecycle set lifecycle.json gs://my-bucket/
```
---
## Disaster Recovery
### RPO/RTO Targets
| Tier | RPO | RTO | Strategy |
|------|-----|-----|----------|
| Tier 1 (Critical) | 0 | <1 hour | Multi-region active-active |
| Tier 2 (Important) | <1 hour | <4 hours | Regional HA + cross-region backup |
| Tier 3 (Standard) | <24 hours | <24 hours | Automated backups + restore |
### Backup Strategy
```bash
# Cloud SQL automated backups
gcloud sql instances patch myapp-db \
--backup-start-time=02:00 \
--enable-point-in-time-recovery
# Firestore scheduled exports
gcloud firestore export gs://myapp-backups/firestore/$(date +%Y%m%d)
# GKE cluster backup with Backup for GKE
gcloud beta container backup-restore backup-plans create myapp-plan \
--project=my-project \
--location=us-central1 \
--cluster=projects/my-project/locations/us-central1/clusters/myapp-cluster \
--all-namespaces \
--cron-schedule="0 2 * * *"
```
### Multi-Region Failover
```bash
# Cloud SQL cross-region replica for DR
gcloud sql instances create myapp-db-replica \
--master-instance-name=myapp-db \
--region=us-east1
# Promote replica during failover
gcloud sql instances promote-replica myapp-db-replica
```
---
## Common Pitfalls
### Technical Debt
| Pitfall | Solution |
|---------|----------|
| Using default VPC | Always create custom VPCs |
| Not enabling audit logs | Enable Cloud Audit Logs from day one |
| Single-region deployment | Plan for multi-zone at minimum |
| No IaC | Use Terraform from the start |
### Security Mistakes
| Mistake | Prevention |
|---------|------------|
| SA key files in code | Use Workload Identity, attached SAs |
| Public GCS buckets | Enable org policy for public access prevention |
| Basic roles (Owner/Editor) | Use predefined or custom roles |
| No encryption key management | Use CMEK for sensitive data |
| Default service account | Create dedicated SAs per workload |
### Performance Issues
| Issue | Solution |
|-------|----------|
| Cold starts on Cloud Run | Set min-instances=1 for latency-critical services |
| Slow BigQuery queries | Partition tables, use clustering, avoid SELECT * |
| GKE pod scheduling delays | Use PodDisruptionBudget, pre-provision with Autopilot |
| Firestore hotspots | Distribute writes across document IDs evenly |
### Cost Surprises
| Surprise | Prevention |
|----------|------------|
| Undeleted resources | Label everything, review weekly |
| Egress costs | Keep traffic in same region, use Private Google Access |
| Cloud NAT charges | Use Private Google Access for GCP service traffic |
| Log ingestion costs | Set exclusion filters for debug/verbose logs |
| BigQuery full scans | Always use partitioning and clustering |
| Idle GKE clusters | Delete dev clusters nightly, use Autopilot |
FILE:references/service_selection.md
# GCP Service Selection Guide
Quick reference for choosing the right GCP service based on requirements.
---
## Table of Contents
- [Compute Services](#compute-services)
- [Database Services](#database-services)
- [Storage Services](#storage-services)
- [Messaging and Events](#messaging-and-events)
- [API and Integration](#api-and-integration)
- [Networking](#networking)
- [Security and Identity](#security-and-identity)
---
## Compute Services
### Decision Matrix
| Requirement | Recommended Service |
|-------------|---------------------|
| HTTP-triggered containers, auto-scaling | Cloud Run |
| Event-driven, short tasks (<9 min) | Cloud Functions (2nd gen) |
| Kubernetes workloads, microservices | GKE Autopilot |
| Custom VMs, GPU/TPU | Compute Engine |
| Batch processing, HPC | Batch |
| Kubernetes with full control | GKE Standard |
### Cloud Run
**Best for:** Containerized HTTP services, APIs, web backends
```
Limits:
- vCPU: 1-8 per instance
- Memory: 128 MiB - 32 GiB
- Request timeout: 3600 seconds
- Concurrency: 1-1000 per instance
- Min instances: 0 (scale-to-zero)
- Max instances: 1000
Pricing: Per vCPU-second + GiB-second (free tier: 2M requests/month)
```
**Use when:**
- Containerized apps with HTTP endpoints
- Variable/unpredictable traffic
- Want scale-to-zero capability
- No Kubernetes expertise needed
**Avoid when:**
- Non-HTTP workloads (use Cloud Functions or GKE)
- Need GPU/TPU (use Compute Engine or GKE)
- Require persistent local storage
### Cloud Functions (2nd gen)
**Best for:** Event-driven functions, lightweight triggers, webhooks
```
Limits:
- Execution: 9 minutes max (2nd gen), 9 minutes (1st gen)
- Memory: 128 MB - 32 GB
- Concurrency: Up to 1000 per instance (2nd gen)
- Runtimes: Node.js, Python, Go, Java, .NET, Ruby, PHP
Pricing: $0.40 per million invocations + compute time
```
**Use when:**
- Event-driven processing (Pub/Sub, Cloud Storage, Firestore)
- Lightweight API endpoints
- Scheduled tasks (Cloud Scheduler triggers)
- Minimal infrastructure management
**Avoid when:**
- Long-running processes (>9 min)
- Complex multi-container apps
- Need fine-grained scaling control
### GKE Autopilot
**Best for:** Kubernetes workloads with managed node provisioning
```
Limits:
- Pod resources: 0.25-112 vCPU, 0.5-896 GiB memory
- GPU support: NVIDIA T4, L4, A100, H100
- Management fee: $0.10/hour per cluster ($74.40/month)
Pricing: Per pod vCPU-hour + GiB-hour (no node management)
```
**Use when:**
- Team has Kubernetes expertise
- Need pod-level resource control
- Multi-container services
- GPU workloads
### Compute Engine
**Best for:** Custom configurations, specialized hardware
```
Machine Types:
- General: e2, n2, n2d, c3
- Compute: c2, c2d
- Memory: m1, m2, m3
- Accelerator: a2 (GPU), a3 (GPU)
- Storage: z3
Pricing Options:
- On-demand, Spot (60-91% discount), Committed Use (37-55% discount)
```
**Use when:**
- Need GPU/TPU
- Windows workloads
- Specific hardware requirements
- Lift-and-shift migrations
---
## Database Services
### Decision Matrix
| Data Type | Query Pattern | Scale | Recommended |
|-----------|--------------|-------|-------------|
| Key-value, document | Simple lookups, real-time | Any | Firestore |
| Wide-column | High-throughput reads/writes | >1TB | Cloud Bigtable |
| Relational | Complex joins, ACID | Variable | Cloud SQL |
| Relational, global | Strong consistency, global | Large | Cloud Spanner |
| Time-series | Time-based queries | Any | Bigtable or BigQuery |
| Analytics, warehouse | SQL analytics | Petabytes | BigQuery |
### Firestore
**Best for:** Document data, mobile/web apps, real-time sync
```
Limits:
- Document size: 1 MiB max
- Field depth: 20 nested levels
- Write rate: 10,000 writes/sec per database
- Indexes: Automatic single-field, manual composite
Pricing:
- Reads: $0.036 per 100K reads
- Writes: $0.108 per 100K writes
- Storage: $0.108 per GiB/month
- Free tier: 50K reads, 20K writes, 1 GiB storage per day
```
**Use when:**
- Mobile/web apps needing offline sync
- Real-time data updates
- Flexible schema
- Serverless architecture
**Avoid when:**
- Complex SQL queries with joins
- Heavy analytics workloads
- Data >1 MiB per document
### Cloud SQL
**Best for:** Relational data with familiar SQL
| Engine | Version | Max Storage | Max Connections |
|--------|---------|-------------|-----------------|
| PostgreSQL | 15 | 64 TB | Instance-dependent |
| MySQL | 8.0 | 64 TB | Instance-dependent |
| SQL Server | 2022 | 64 TB | Instance-dependent |
```
Pricing:
- Machine type + storage + networking
- HA: 2x cost (regional instance)
- Read replicas: Per-replica pricing
```
**Use when:**
- Relational data with complex queries
- Existing SQL expertise
- Need ACID transactions
- Migration from on-premises databases
### Cloud Spanner
**Best for:** Globally distributed relational data
```
Limits:
- Storage: Unlimited
- Nodes: 1-100+ per instance
- Consistency: Strong global consistency
Pricing:
- Regional: $0.90/node-hour (~$657/month per node)
- Multi-region: $2.70/node-hour (~$1,971/month per node)
- Storage: $0.30/GiB/month
```
**Use when:**
- Global applications needing strong consistency
- Relational data at massive scale
- 99.999% availability requirement
- Horizontal scaling with SQL
### BigQuery
**Best for:** Analytics, data warehouse, SQL on massive datasets
```
Limits:
- Query: 6-hour timeout
- Concurrent queries: 100 default
- Streaming inserts: 100K rows/sec per table
Pricing:
- On-demand: $6.25 per TB queried (first 1 TB free/month)
- Editions: Autoscale slots starting at $0.04/slot-hour
- Storage: $0.02/GiB (active), $0.01/GiB (long-term)
```
### Firestore vs Cloud SQL vs Spanner
| Factor | Firestore | Cloud SQL | Cloud Spanner |
|--------|-----------|-----------|---------------|
| Query flexibility | Document-based | Full SQL | Full SQL |
| Scaling | Automatic | Vertical + read replicas | Horizontal |
| Consistency | Strong (single region) | ACID | Strong (global) |
| Cost model | Per-operation | Per-hour | Per-node-hour |
| Operational | Zero management | Managed (some ops) | Managed |
| Best for | Mobile/web apps | Traditional apps | Global scale |
---
## Storage Services
### Cloud Storage Classes
| Class | Access Pattern | Min Duration | Cost (GiB/mo) |
|-------|---------------|--------------|----------------|
| Standard | Frequent | None | $0.020 |
| Nearline | Monthly access | 30 days | $0.010 |
| Coldline | Quarterly access | 90 days | $0.004 |
| Archive | Annual access | 365 days | $0.0012 |
### Lifecycle Policy Example
```json
{
"lifecycle": {
"rule": [
{
"action": { "type": "SetStorageClass", "storageClass": "NEARLINE" },
"condition": { "age": 30, "matchesStorageClass": ["STANDARD"] }
},
{
"action": { "type": "SetStorageClass", "storageClass": "COLDLINE" },
"condition": { "age": 90, "matchesStorageClass": ["NEARLINE"] }
},
{
"action": { "type": "SetStorageClass", "storageClass": "ARCHIVE" },
"condition": { "age": 365, "matchesStorageClass": ["COLDLINE"] }
},
{
"action": { "type": "Delete" },
"condition": { "age": 2555 }
}
]
}
}
```
### Autoclass
Automatically transitions objects between storage classes based on access patterns. Recommended for mixed or unknown access patterns.
```bash
gsutil mb -l us-central1 --autoclass gs://my-bucket/
```
### Block and File Storage
| Service | Use Case | Access |
|---------|----------|--------|
| Persistent Disk | GCE/GKE block storage | Single instance (RW) or multi (RO) |
| Filestore | NFS shared file system | Multiple instances |
| Parallelstore | HPC parallel file system | High throughput |
| Cloud Storage FUSE | Mount GCS as filesystem | Any compute |
---
## Messaging and Events
### Decision Matrix
| Pattern | Service | Use Case |
|---------|---------|----------|
| Pub/sub messaging | Pub/Sub | Event streaming, microservice decoupling |
| Task queue | Cloud Tasks | Asynchronous task execution with retries |
| Workflow orchestration | Workflows | Multi-step service orchestration |
| Batch orchestration | Cloud Composer | Complex DAG-based pipelines (Airflow) |
| Event triggers | Eventarc | Route events to Cloud Run, GKE, Workflows |
### Pub/Sub
**Best for:** Event-driven architectures, stream processing
```
Limits:
- Message size: 10 MB max
- Throughput: Unlimited (auto-scaling)
- Retention: 7 days default (up to 31 days)
- Ordering: Per ordering key
Pricing: $40/TiB for message delivery
```
```python
# Pub/Sub publisher example
from google.cloud import pubsub_v1
import json
publisher = pubsub_v1.PublisherClient()
topic_path = publisher.topic_path('my-project', 'events')
def publish_event(event_type, payload):
data = json.dumps(payload).encode('utf-8')
future = publisher.publish(
topic_path,
data,
event_type=event_type
)
return future.result()
```
### Cloud Tasks
**Best for:** Asynchronous task execution with delivery guarantees
```
Features:
- Configurable retry policies
- Rate limiting
- Scheduled delivery
- HTTP and App Engine targets
Pricing: $0.40 per million operations
```
### Eventarc
**Best for:** Routing cloud events to services
```python
# Eventarc routes events from 130+ Google Cloud sources
# to Cloud Run, GKE, or Workflows
# Example: Trigger Cloud Run on Cloud Storage upload
# gcloud eventarc triggers create my-trigger \
# --destination-run-service=my-service \
# --event-filters="type=google.cloud.storage.object.v1.finalized" \
# --event-filters="bucket=my-bucket"
```
---
## API and Integration
### API Gateway vs Cloud Endpoints vs Cloud Run
| Factor | API Gateway | Cloud Endpoints | Cloud Run (direct) |
|--------|-------------|-----------------|---------------------|
| Protocol | REST, gRPC | REST, gRPC | Any HTTP |
| Auth | API keys, JWT, Firebase | API keys, JWT | IAM, custom |
| Rate limiting | Built-in | Built-in | Manual |
| Cost | Per-call pricing | Per-call pricing | Per-request |
| Best for | External APIs | Internal APIs | Simple services |
### Cloud Endpoints Configuration
```yaml
# openapi.yaml
swagger: "2.0"
info:
title: "My API"
version: "1.0.0"
host: "my-api-xyz.apigateway.my-project.cloud.goog"
schemes:
- "https"
paths:
/users:
get:
summary: "List users"
operationId: "listUsers"
x-google-backend:
address: "https://my-app-api-xyz.a.run.app"
security:
- api_key: []
securityDefinitions:
api_key:
type: "apiKey"
name: "key"
in: "query"
```
### Workflows
**Best for:** Orchestrating multi-service processes
```yaml
# workflow.yaml
main:
steps:
- processOrder:
call: http.post
args:
url: https://orders-service.run.app/process
body:
orderId: args.orderId
result: orderResult
- checkInventory:
switch:
- condition: orderResult.body.inStock
next: shipOrder
next: backOrder
- shipOrder:
call: http.post
args:
url: https://shipping-service.run.app/ship
body:
orderId: args.orderId
result: shipResult
- backOrder:
call: http.post
args:
url: https://inventory-service.run.app/backorder
body:
orderId: args.orderId
```
---
## Networking
### VPC Components
| Component | Purpose |
|-----------|---------|
| VPC | Isolated network (global resource) |
| Subnet | Regional network segment |
| Cloud NAT | Outbound internet for private instances |
| Cloud Router | Dynamic routing (BGP) |
| Private Google Access | Access GCP APIs without public IP |
| VPC Peering | Connect two VPC networks |
| Shared VPC | Share VPC across projects |
### VPC Design Pattern
```
VPC: 10.0.0.0/16 (global)
Subnet us-central1:
10.0.0.0/20 (primary)
10.4.0.0/14 (pods - secondary)
10.8.0.0/20 (services - secondary)
- GKE cluster, Cloud Run (VPC connector)
Subnet us-east1:
10.0.16.0/20 (primary)
- Cloud SQL (private IP), Memorystore
Subnet europe-west1:
10.0.32.0/20 (primary)
- DR / multi-region workloads
```
### Private Google Access
```bash
# Enable Private Google Access on a subnet
gcloud compute networks subnets update my-subnet \
--region=us-central1 \
--enable-private-google-access
```
---
## Security and Identity
### IAM Best Practices
```bash
# Prefer predefined roles over basic roles
# BAD: roles/editor (too broad)
# GOOD: roles/run.invoker (specific)
# Grant role to service account
gcloud projects add-iam-policy-binding my-project \
--member="serviceAccount:my-sa@my-project.iam.gserviceaccount.com" \
--role="roles/datastore.user" \
--condition='expression=resource.name.startsWith("projects/my-project/databases/(default)/documents/users"),title=firestore-users-only'
```
### Service Account Best Practices
| Practice | Description |
|----------|-------------|
| One SA per service | Separate service accounts per workload |
| Workload Identity | Bind K8s SAs to GCP SAs in GKE |
| Short-lived tokens | Use impersonation instead of key files |
| No SA keys | Avoid downloading JSON key files |
### Secret Manager vs Environment Variables
| Factor | Secret Manager | Env Variables |
|--------|---------------|---------------|
| Rotation | Automatic versioning | Manual redeploy |
| Audit | Cloud Audit Logs | No audit trail |
| Access control | IAM per-secret | Per-service |
| Pricing | $0.06/10K access ops | Free |
| Use case | Credentials, API keys | Non-sensitive config |
### Secret Manager Usage
```python
from google.cloud import secretmanager
def get_secret(project_id, secret_id, version="latest"):
client = secretmanager.SecretManagerServiceClient()
name = f"projects/{project_id}/secrets/{secret_id}/versions/{version}"
response = client.access_secret_version(request={"name": name})
return response.payload.data.decode("UTF-8")
# Usage
db_password = get_secret("my-project", "db-password")
```
FILE:scripts/architecture_designer.py
"""
GCP architecture design and service recommendation module.
Generates architecture patterns based on application requirements.
"""
import argparse
import json
import sys
from typing import Dict, List, Any
from enum import Enum
class ApplicationType(Enum):
"""Types of applications supported."""
WEB_APP = "web_application"
MOBILE_BACKEND = "mobile_backend"
DATA_PIPELINE = "data_pipeline"
MICROSERVICES = "microservices"
SAAS_PLATFORM = "saas_platform"
ML_PLATFORM = "ml_platform"
class ArchitectureDesigner:
"""Design GCP architectures based on requirements."""
def __init__(self, requirements: Dict[str, Any]):
"""
Initialize with application requirements.
Args:
requirements: Dictionary containing app type, traffic, budget, etc.
"""
self.app_type = requirements.get('application_type', 'web_application')
self.expected_users = requirements.get('expected_users', 1000)
self.requests_per_second = requirements.get('requests_per_second', 10)
self.budget_monthly = requirements.get('budget_monthly_usd', 500)
self.team_size = requirements.get('team_size', 3)
self.gcp_experience = requirements.get('gcp_experience', 'beginner')
self.compliance_needs = requirements.get('compliance', [])
self.data_size_gb = requirements.get('data_size_gb', 10)
def recommend_architecture_pattern(self) -> Dict[str, Any]:
"""
Recommend architecture pattern based on requirements.
Returns:
Dictionary with recommended pattern and services
"""
if self.app_type in ['web_application', 'saas_platform']:
if self.expected_users < 10000:
return self._serverless_web_architecture()
elif self.expected_users < 100000:
return self._gke_microservices_architecture()
else:
return self._multi_region_architecture()
elif self.app_type == 'mobile_backend':
return self._serverless_mobile_backend()
elif self.app_type == 'data_pipeline':
return self._data_pipeline_architecture()
elif self.app_type == 'microservices':
return self._gke_microservices_architecture()
elif self.app_type == 'ml_platform':
return self._ml_platform_architecture()
else:
return self._serverless_web_architecture()
def _serverless_web_architecture(self) -> Dict[str, Any]:
"""Serverless web application pattern using Cloud Run."""
return {
'pattern_name': 'Serverless Web Application',
'description': 'Fully serverless architecture with Cloud Run and Firestore',
'use_case': 'SaaS platforms, low to medium traffic websites, MVPs',
'services': {
'frontend': {
'service': 'Cloud Storage + Cloud CDN',
'purpose': 'Static website hosting with global CDN',
'configuration': {
'bucket': 'Website bucket with public access',
'cdn': 'Cloud CDN with custom domain and HTTPS',
'caching': 'Cache-Control headers, edge caching'
}
},
'api': {
'service': 'Cloud Run',
'purpose': 'Containerized API backend with auto-scaling',
'configuration': {
'cpu': '1 vCPU',
'memory': '512 Mi',
'min_instances': '0 (scale to zero)',
'max_instances': '10',
'concurrency': '80 requests per instance',
'timeout': '300 seconds'
}
},
'database': {
'service': 'Firestore',
'purpose': 'NoSQL document database with real-time sync',
'configuration': {
'mode': 'Native mode',
'location': 'Regional or multi-region',
'security_rules': 'Firestore security rules',
'backup': 'Scheduled exports to Cloud Storage'
}
},
'authentication': {
'service': 'Identity Platform',
'purpose': 'User authentication and authorization',
'configuration': {
'providers': 'Email/password, Google, Apple, OIDC',
'mfa': 'SMS or TOTP multi-factor authentication',
'token_expiration': '1 hour access, 30 days refresh'
}
},
'cicd': {
'service': 'Cloud Build',
'purpose': 'Automated build and deployment from Git',
'configuration': {
'source': 'GitHub or Cloud Source Repositories',
'build': 'Automatic on commit',
'environments': 'dev, staging, production'
}
}
},
'estimated_cost': {
'monthly_usd': self._calculate_serverless_cost(),
'breakdown': {
'Cloud CDN': '5-20 USD',
'Cloud Run': '5-25 USD',
'Firestore': '5-30 USD',
'Identity Platform': '0-10 USD (free tier: 50k MAU)',
'Cloud Storage': '1-5 USD'
}
},
'pros': [
'No server management',
'Auto-scaling with scale-to-zero',
'Pay only for what you use',
'No cold starts with min instances',
'Container-based (no runtime restrictions)'
],
'cons': [
'Vendor lock-in to GCP',
'Regional availability considerations',
'Debugging distributed systems complex',
'Firestore query limitations vs SQL'
],
'scaling_characteristics': {
'users_supported': '1k - 100k',
'requests_per_second': '100 - 10,000',
'scaling_method': 'Automatic (Cloud Run auto-scaling)'
}
}
def _gke_microservices_architecture(self) -> Dict[str, Any]:
"""GKE-based microservices architecture."""
return {
'pattern_name': 'Microservices on GKE',
'description': 'Kubernetes-native architecture with managed services',
'use_case': 'SaaS platforms, complex microservices, enterprise applications',
'services': {
'load_balancer': {
'service': 'Cloud Load Balancing',
'purpose': 'Global HTTP(S) load balancing',
'configuration': {
'type': 'External Application Load Balancer',
'ssl': 'Google-managed SSL certificate',
'health_checks': '/health endpoint, 10s interval',
'cdn': 'Cloud CDN enabled for static content'
}
},
'compute': {
'service': 'GKE Autopilot',
'purpose': 'Managed Kubernetes for containerized workloads',
'configuration': {
'mode': 'Autopilot (fully managed node provisioning)',
'scaling': 'Horizontal Pod Autoscaler',
'networking': 'VPC-native with Alias IPs',
'workload_identity': 'Enabled for secure service account binding'
}
},
'database': {
'service': 'Cloud SQL (PostgreSQL)',
'purpose': 'Managed relational database',
'configuration': {
'tier': 'db-custom-2-8192 (2 vCPU, 8 GB RAM)',
'high_availability': 'Regional with automatic failover',
'read_replicas': '1-2 for read scaling',
'backup': 'Automated daily backups, 7-day retention',
'encryption': 'Customer-managed encryption key (CMEK)'
}
},
'cache': {
'service': 'Memorystore (Redis)',
'purpose': 'Session storage, application caching',
'configuration': {
'tier': 'Basic (1 GB) or Standard (HA)',
'version': 'Redis 7.0',
'eviction_policy': 'allkeys-lru'
}
},
'messaging': {
'service': 'Pub/Sub',
'purpose': 'Asynchronous messaging between services',
'configuration': {
'topics': 'Per-domain event topics',
'subscriptions': 'Pull or push delivery',
'dead_letter': 'Dead letter topic after 5 retries',
'ordering': 'Ordering keys for ordered delivery'
}
},
'storage': {
'service': 'Cloud Storage',
'purpose': 'User uploads, backups, logs',
'configuration': {
'storage_class': 'Standard with lifecycle policies',
'versioning': 'Enabled for important buckets',
'lifecycle': 'Transition to Nearline after 30 days'
}
}
},
'estimated_cost': {
'monthly_usd': self._calculate_gke_cost(),
'breakdown': {
'Cloud Load Balancing': '20-40 USD',
'GKE Autopilot': '75-250 USD',
'Cloud SQL': '80-250 USD',
'Memorystore': '30-80 USD',
'Pub/Sub': '5-20 USD',
'Cloud Storage': '5-20 USD'
}
},
'pros': [
'Kubernetes ecosystem compatibility',
'Fine-grained scaling control',
'Multi-cloud portability',
'Rich service mesh (Anthos Service Mesh)',
'Managed node provisioning with Autopilot'
],
'cons': [
'Higher baseline costs than serverless',
'Kubernetes learning curve',
'More operational complexity',
'GKE management fee ($74.40/month per cluster)'
],
'scaling_characteristics': {
'users_supported': '10k - 500k',
'requests_per_second': '1,000 - 50,000',
'scaling_method': 'HPA + Cluster Autoscaler'
}
}
def _serverless_mobile_backend(self) -> Dict[str, Any]:
"""Serverless mobile backend with Firebase."""
return {
'pattern_name': 'Serverless Mobile Backend',
'description': 'Mobile-first backend with Firebase and Cloud Functions',
'use_case': 'Mobile apps, real-time applications, offline-first apps',
'services': {
'api': {
'service': 'Cloud Functions (2nd gen)',
'purpose': 'Event-driven API handlers',
'configuration': {
'runtime': 'Node.js 20 or Python 3.12',
'memory': '256 MB - 1 GB',
'timeout': '60 seconds',
'concurrency': 'Up to 1000 concurrent'
}
},
'database': {
'service': 'Firestore',
'purpose': 'Real-time NoSQL database with offline sync',
'configuration': {
'mode': 'Native mode',
'multi_region': 'nam5 or eur3 for HA',
'security_rules': 'Client-side access control',
'indexes': 'Composite indexes for queries'
}
},
'file_storage': {
'service': 'Cloud Storage (Firebase)',
'purpose': 'User uploads (images, videos, documents)',
'configuration': {
'access': 'Firebase Security Rules',
'resumable_uploads': 'Enabled for large files',
'cdn': 'Automatic via Firebase Hosting CDN'
}
},
'authentication': {
'service': 'Firebase Authentication',
'purpose': 'User management and federation',
'configuration': {
'providers': 'Email, Google, Apple, Phone',
'anonymous_auth': 'Enabled for guest access',
'custom_claims': 'Role-based access control',
'multi_tenancy': 'Supported via Identity Platform'
}
},
'push_notifications': {
'service': 'Firebase Cloud Messaging (FCM)',
'purpose': 'Push notifications to mobile devices',
'configuration': {
'platforms': 'iOS (APNs), Android, Web',
'topics': 'Topic-based group messaging',
'analytics': 'Notification delivery tracking'
}
},
'analytics': {
'service': 'Google Analytics (Firebase)',
'purpose': 'User analytics and event tracking',
'configuration': {
'events': 'Custom and automatic events',
'audiences': 'User segmentation',
'bigquery_export': 'Raw event export to BigQuery'
}
}
},
'estimated_cost': {
'monthly_usd': 40 + (self.expected_users * 0.004),
'breakdown': {
'Cloud Functions': '5-30 USD',
'Firestore': '10-50 USD',
'Cloud Storage': '5-20 USD',
'Identity Platform': '0-15 USD',
'FCM': '0 USD (free)',
'Analytics': '0 USD (free)'
}
},
'pros': [
'Real-time data sync built-in',
'Offline-first support',
'Firebase SDKs for iOS/Android/Web',
'Free tier covers most MVPs',
'Rapid development with Firebase console'
],
'cons': [
'Firestore query limitations',
'Vendor lock-in to Firebase/GCP',
'Cost scaling can be unpredictable',
'Limited server-side control'
],
'scaling_characteristics': {
'users_supported': '1k - 1M',
'requests_per_second': '100 - 100,000',
'scaling_method': 'Automatic (Firebase managed)'
}
}
def _data_pipeline_architecture(self) -> Dict[str, Any]:
"""Serverless data pipeline with BigQuery."""
return {
'pattern_name': 'Serverless Data Pipeline',
'description': 'Scalable data ingestion, processing, and analytics',
'use_case': 'Analytics, IoT data, log processing, ETL, data warehousing',
'services': {
'ingestion': {
'service': 'Pub/Sub',
'purpose': 'Real-time event and data ingestion',
'configuration': {
'throughput': 'Unlimited (auto-scaling)',
'retention': '7 days (configurable to 31 days)',
'ordering': 'Ordering keys for ordered delivery',
'dead_letter': 'Dead letter topic for failed messages'
}
},
'processing': {
'service': 'Dataflow (Apache Beam)',
'purpose': 'Stream and batch data processing',
'configuration': {
'mode': 'Streaming or batch',
'autoscaling': 'Horizontal autoscaling',
'workers': f'{max(1, self.data_size_gb // 20)} initial workers',
'sdk': 'Python or Java Apache Beam SDK'
}
},
'warehouse': {
'service': 'BigQuery',
'purpose': 'Serverless data warehouse and analytics',
'configuration': {
'pricing': 'On-demand ($6.25/TB queried) or slots',
'partitioning': 'By ingestion time or custom field',
'clustering': 'Up to 4 clustering columns',
'streaming_insert': 'Real-time data availability'
}
},
'storage': {
'service': 'Cloud Storage (Data Lake)',
'purpose': 'Raw data lake and archival storage',
'configuration': {
'format': 'Parquet or Avro (columnar)',
'partitioning': 'By date (year/month/day)',
'lifecycle': 'Transition to Coldline after 90 days',
'catalog': 'Dataplex for data governance'
}
},
'visualization': {
'service': 'Looker / Looker Studio',
'purpose': 'Business intelligence dashboards',
'configuration': {
'source': 'BigQuery direct connection',
'refresh': 'Real-time or scheduled',
'sharing': 'Embedded or web dashboards'
}
},
'orchestration': {
'service': 'Cloud Composer (Airflow)',
'purpose': 'Workflow orchestration for batch pipelines',
'configuration': {
'environment': 'Cloud Composer 2 (auto-scaling)',
'dags': 'Python DAG definitions',
'scheduling': 'Cron-based scheduling'
}
}
},
'estimated_cost': {
'monthly_usd': self._calculate_data_pipeline_cost(),
'breakdown': {
'Pub/Sub': '5-30 USD',
'Dataflow': '20-150 USD',
'BigQuery': '10-100 USD (on-demand)',
'Cloud Storage': '5-30 USD',
'Looker Studio': '0 USD (free)',
'Cloud Composer': '300+ USD (if used)'
}
},
'pros': [
'Fully serverless data stack',
'BigQuery scales to petabytes',
'Real-time and batch in same pipeline',
'Cost-effective with on-demand pricing',
'ML integration via BigQuery ML'
],
'cons': [
'Dataflow has steep learning curve (Beam SDK)',
'BigQuery costs based on data scanned',
'Cloud Composer expensive for small workloads',
'Schema evolution requires planning'
],
'scaling_characteristics': {
'events_per_second': '1,000 - 10,000,000',
'data_volume': '1 GB - 1 PB per day',
'scaling_method': 'Automatic (all services auto-scale)'
}
}
def _ml_platform_architecture(self) -> Dict[str, Any]:
"""ML platform architecture with Vertex AI."""
return {
'pattern_name': 'ML Platform',
'description': 'End-to-end machine learning platform',
'use_case': 'Model training, serving, MLOps, feature engineering',
'services': {
'ml_platform': {
'service': 'Vertex AI',
'purpose': 'Training, tuning, and serving ML models',
'configuration': {
'training': 'Custom or AutoML training jobs',
'prediction': 'Online or batch prediction endpoints',
'pipelines': 'Vertex AI Pipelines for MLOps',
'feature_store': 'Vertex AI Feature Store'
}
},
'data': {
'service': 'BigQuery',
'purpose': 'Feature engineering and data exploration',
'configuration': {
'ml': 'BigQuery ML for in-warehouse models',
'export': 'Export to Cloud Storage for training',
'feature_engineering': 'SQL-based transformations'
}
},
'storage': {
'service': 'Cloud Storage',
'purpose': 'Datasets, model artifacts, experiment logs',
'configuration': {
'buckets': 'Separate buckets for data/models/logs',
'versioning': 'Enabled for model artifacts',
'lifecycle': 'Archive old experiment data'
}
},
'triggers': {
'service': 'Cloud Functions',
'purpose': 'Event-driven preprocessing and triggers',
'configuration': {
'triggers': 'Cloud Storage, Pub/Sub, Scheduler',
'preprocessing': 'Data validation and transforms',
'notifications': 'Training completion alerts'
}
},
'monitoring': {
'service': 'Vertex AI Model Monitoring',
'purpose': 'Detect data drift and model degradation',
'configuration': {
'skew_detection': 'Training-serving skew alerts',
'drift_detection': 'Feature drift monitoring',
'alerting': 'Cloud Monitoring integration'
}
}
},
'estimated_cost': {
'monthly_usd': 200 + (self.data_size_gb * 2),
'breakdown': {
'Vertex AI Training': '50-500 USD (GPU dependent)',
'Vertex AI Prediction': '30-200 USD',
'BigQuery': '20-100 USD',
'Cloud Storage': '10-50 USD',
'Cloud Functions': '5-20 USD'
}
},
'pros': [
'End-to-end ML lifecycle management',
'AutoML for rapid prototyping',
'Integrated with BigQuery and Cloud Storage',
'Managed model serving with autoscaling',
'Built-in experiment tracking'
],
'cons': [
'GPU costs can escalate quickly',
'Vertex AI pricing is complex',
'Limited customization vs self-managed',
'Vendor lock-in for model artifacts'
],
'scaling_characteristics': {
'training': 'Multi-GPU, distributed training',
'prediction': '1 - 1000+ replicas',
'scaling_method': 'Automatic endpoint scaling'
}
}
def _multi_region_architecture(self) -> Dict[str, Any]:
"""Multi-region high availability architecture."""
return {
'pattern_name': 'Multi-Region High Availability',
'description': 'Global deployment with disaster recovery',
'use_case': 'Global applications, 99.99% uptime, compliance',
'services': {
'dns': {
'service': 'Cloud DNS',
'purpose': 'Global DNS with health-checked routing',
'configuration': {
'routing_policy': 'Geolocation or weighted routing',
'health_checks': 'HTTP health checks per region',
'failover': 'Automatic DNS failover'
}
},
'cdn': {
'service': 'Cloud CDN',
'purpose': 'Edge caching and acceleration',
'configuration': {
'origins': 'Multiple regional backends',
'cache_modes': 'CACHE_ALL_STATIC or USE_ORIGIN_HEADERS',
'edge_locations': 'Global (100+ locations)'
}
},
'compute': {
'service': 'Multi-region GKE or Cloud Run',
'purpose': 'Active-active deployment across regions',
'configuration': {
'regions': 'us-central1 (primary), europe-west1 (secondary)',
'deployment': 'Cloud Deploy for multi-region rollout',
'traffic_split': 'Global Load Balancer with traffic management'
}
},
'database': {
'service': 'Cloud Spanner or Firestore multi-region',
'purpose': 'Globally consistent database',
'configuration': {
'spanner': 'Multi-region config (nam-eur-asia1)',
'firestore': 'Multi-region location (nam5, eur3)',
'consistency': 'Strong consistency (Spanner) or eventual (Firestore)',
'replication': 'Automatic cross-region replication'
}
},
'storage': {
'service': 'Cloud Storage (dual-region or multi-region)',
'purpose': 'Geo-redundant object storage',
'configuration': {
'location': 'Dual-region (us-central1+us-east1) or multi-region (US)',
'turbo_replication': '15-minute RPO with turbo replication',
'versioning': 'Enabled for critical data'
}
}
},
'estimated_cost': {
'monthly_usd': self._calculate_gke_cost() * 2.0,
'breakdown': {
'Cloud DNS': '5-15 USD',
'Cloud CDN': '20-100 USD',
'Compute (2 regions)': '150-500 USD',
'Cloud Spanner': '500-2000 USD (multi-region)',
'Data transfer (cross-region)': '50-200 USD'
}
},
'pros': [
'Global low latency',
'High availability (99.99%+)',
'Disaster recovery built-in',
'Data sovereignty compliance',
'Automatic failover'
],
'cons': [
'2x+ costs vs single region',
'Cloud Spanner is expensive',
'Complex deployment pipeline',
'Cross-region data transfer costs',
'Operational overhead'
],
'scaling_characteristics': {
'users_supported': '100k - 100M',
'requests_per_second': '10,000 - 10,000,000',
'scaling_method': 'Per-region auto-scaling + global load balancing'
}
}
def _calculate_serverless_cost(self) -> float:
"""Estimate serverless architecture cost."""
requests_per_month = self.requests_per_second * 2_592_000
cloud_run_cost = max(5, (requests_per_month / 1_000_000) * 0.40)
firestore_cost = max(5, self.data_size_gb * 0.18)
cdn_cost = max(5, self.expected_users * 0.008)
storage_cost = max(1, self.data_size_gb * 0.02)
total = cloud_run_cost + firestore_cost + cdn_cost + storage_cost
return min(total, self.budget_monthly)
def _calculate_gke_cost(self) -> float:
"""Estimate GKE microservices architecture cost."""
gke_management = 74.40 # Autopilot cluster fee
pod_cost = max(2, self.expected_users // 5000) * 35
cloud_sql_cost = 120 # db-custom-2-8192 baseline
memorystore_cost = 35 # Basic 1 GB
lb_cost = 25
total = gke_management + pod_cost + cloud_sql_cost + memorystore_cost + lb_cost
return min(total, self.budget_monthly)
def _calculate_data_pipeline_cost(self) -> float:
"""Estimate data pipeline cost."""
pubsub_cost = max(5, self.data_size_gb * 0.5)
dataflow_cost = max(20, self.data_size_gb * 1.5)
bigquery_cost = max(10, self.data_size_gb * 0.02 * 6.25)
storage_cost = self.data_size_gb * 0.02
total = pubsub_cost + dataflow_cost + bigquery_cost + storage_cost
return min(total, self.budget_monthly)
def generate_service_checklist(self) -> list:
"""Generate implementation checklist for recommended architecture."""
architecture = self.recommend_architecture_pattern()
checklist = [
{
'phase': 'Planning',
'tasks': [
'Review architecture pattern and services',
'Estimate costs using GCP Pricing Calculator',
'Define environment strategy (dev, staging, prod)',
'Set up GCP Organization and projects',
'Define labeling strategy for resources'
]
},
{
'phase': 'Foundation',
'tasks': [
'Create VPC with subnets (if using GKE/Compute)',
'Configure Cloud NAT for private resources',
'Set up IAM roles and service accounts',
'Enable Cloud Audit Logs',
'Configure Organization policies'
]
},
{
'phase': 'Core Services',
'tasks': [
f"Deploy {service['service']}"
for service in architecture['services'].values()
]
},
{
'phase': 'Security',
'tasks': [
'Configure firewall rules and VPC Service Controls',
'Enable encryption (Cloud KMS) for all services',
'Set up Cloud Armor WAF rules',
'Configure Secret Manager for credentials',
'Enable Security Command Center'
]
},
{
'phase': 'Monitoring',
'tasks': [
'Create Cloud Monitoring dashboards',
'Set up alerting policies for critical metrics',
'Configure notification channels (email, Slack, PagerDuty)',
'Enable Cloud Trace for distributed tracing',
'Set up log-based metrics and log sinks'
]
},
{
'phase': 'CI/CD',
'tasks': [
'Set up Cloud Build triggers',
'Configure automated testing',
'Implement canary or rolling deployments',
'Set up rollback procedures',
'Document deployment process'
]
}
]
return checklist
def main():
parser = argparse.ArgumentParser(
description='GCP Architecture Designer - Recommends GCP services based on workload requirements'
)
parser.add_argument(
'--input', '-i',
type=str,
help='Path to JSON file with application requirements'
)
parser.add_argument(
'--output', '-o',
type=str,
help='Path to write design output JSON'
)
parser.add_argument(
'--json',
action='store_true',
help='Output as JSON format'
)
parser.add_argument(
'--app-type',
type=str,
choices=['web_application', 'mobile_backend', 'data_pipeline',
'microservices', 'saas_platform', 'ml_platform'],
default='web_application',
help='Application type (default: web_application)'
)
parser.add_argument(
'--users',
type=int,
default=1000,
help='Expected number of users (default: 1000)'
)
parser.add_argument(
'--budget',
type=float,
default=500,
help='Monthly budget in USD (default: 500)'
)
args = parser.parse_args()
if args.input:
try:
with open(args.input, 'r') as f:
requirements = json.load(f)
except FileNotFoundError:
print(f"Error: File '{args.input}' not found.", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError:
print(f"Error: File '{args.input}' is not valid JSON.", file=sys.stderr)
sys.exit(1)
else:
requirements = {
'application_type': args.app_type,
'expected_users': args.users,
'budget_monthly_usd': args.budget
}
designer = ArchitectureDesigner(requirements)
result = designer.recommend_architecture_pattern()
checklist = designer.generate_service_checklist()
output = {
'architecture': result,
'implementation_checklist': checklist
}
if args.output:
with open(args.output, 'w') as f:
json.dump(output, f, indent=2)
print(f"Design written to {args.output}")
elif args.json:
print(json.dumps(output, indent=2))
else:
print(f"\nRecommended Pattern: {result['pattern_name']}")
print(f"Description: {result['description']}")
print(f"Use Case: {result['use_case']}")
print(f"\nServices:")
for name, svc in result['services'].items():
print(f" - {name}: {svc['service']} ({svc['purpose']})")
print(f"\nEstimated Monthly Cost: .2f")
print(f"\nPros: {', '.join(result['pros'])}")
print(f"Cons: {', '.join(result['cons'])}")
if __name__ == '__main__':
main()
FILE:scripts/cost_optimizer.py
"""
GCP cost optimization analyzer.
Provides cost-saving recommendations for GCP resources.
"""
import argparse
import json
import sys
from typing import Dict, List, Any
class CostOptimizer:
"""Analyze GCP costs and provide optimization recommendations."""
def __init__(self, current_resources: Dict[str, Any], monthly_spend: float):
"""
Initialize with current GCP resources and spending.
Args:
current_resources: Dictionary of current GCP resources
monthly_spend: Current monthly GCP spend in USD
"""
self.resources = current_resources
self.monthly_spend = monthly_spend
self.recommendations = []
def analyze_and_optimize(self) -> Dict[str, Any]:
"""
Analyze current setup and generate cost optimization recommendations.
Returns:
Dictionary with recommendations and potential savings
"""
self.recommendations = []
potential_savings = 0.0
compute_savings = self._analyze_compute()
potential_savings += compute_savings
storage_savings = self._analyze_storage()
potential_savings += storage_savings
database_savings = self._analyze_database()
potential_savings += database_savings
network_savings = self._analyze_networking()
potential_savings += network_savings
general_savings = self._analyze_general_optimizations()
potential_savings += general_savings
return {
'current_monthly_spend': self.monthly_spend,
'potential_monthly_savings': round(potential_savings, 2),
'optimized_monthly_spend': round(self.monthly_spend - potential_savings, 2),
'savings_percentage': round((potential_savings / self.monthly_spend) * 100, 2) if self.monthly_spend > 0 else 0,
'recommendations': self.recommendations,
'priority_actions': self._prioritize_recommendations()
}
def _analyze_compute(self) -> float:
"""Analyze compute resources (GCE, GKE, Cloud Run)."""
savings = 0.0
gce_instances = self.resources.get('gce_instances', [])
if gce_instances:
idle_count = sum(1 for inst in gce_instances if inst.get('cpu_utilization', 100) < 10)
if idle_count > 0:
idle_cost = idle_count * 50
savings += idle_cost
self.recommendations.append({
'service': 'Compute Engine',
'type': 'Idle Resources',
'issue': f'{idle_count} GCE instances with <10% CPU utilization',
'recommendation': 'Stop or delete idle instances, or downsize to smaller machine types',
'potential_savings': idle_cost,
'priority': 'high'
})
# Check for committed use discounts
on_demand_count = sum(1 for inst in gce_instances if inst.get('pricing', 'on-demand') == 'on-demand')
if on_demand_count >= 2:
cud_savings = on_demand_count * 50 * 0.37 # 37% savings with 1-yr CUD
savings += cud_savings
self.recommendations.append({
'service': 'Compute Engine',
'type': 'Committed Use Discounts',
'issue': f'{on_demand_count} instances on on-demand pricing',
'recommendation': 'Purchase 1-year committed use discounts for predictable workloads (37% savings) or 3-year (55% savings)',
'potential_savings': cud_savings,
'priority': 'medium'
})
# Check for sustained use discounts awareness
short_lived = sum(1 for inst in gce_instances if inst.get('uptime_hours_month', 730) < 200)
if short_lived > 0:
self.recommendations.append({
'service': 'Compute Engine',
'type': 'Scheduling',
'issue': f'{short_lived} instances running <200 hours/month',
'recommendation': 'Use Instance Scheduler to stop dev/test instances outside business hours',
'potential_savings': short_lived * 20,
'priority': 'medium'
})
savings += short_lived * 20
# GKE optimization
gke_clusters = self.resources.get('gke_clusters', [])
for cluster in gke_clusters:
if cluster.get('mode', 'standard') == 'standard':
node_utilization = cluster.get('avg_node_utilization', 100)
if node_utilization < 40:
autopilot_savings = cluster.get('monthly_cost', 500) * 0.30
savings += autopilot_savings
self.recommendations.append({
'service': 'GKE',
'type': 'Cluster Mode',
'issue': f'Standard GKE cluster with <40% node utilization',
'recommendation': 'Migrate to GKE Autopilot to pay only for pod resources, or enable cluster autoscaler',
'potential_savings': autopilot_savings,
'priority': 'high'
})
# Cloud Run optimization
cloud_run_services = self.resources.get('cloud_run_services', [])
for svc in cloud_run_services:
if svc.get('min_instances', 0) > 0 and svc.get('avg_rps', 100) < 1:
min_inst_savings = svc.get('min_instances', 1) * 15
savings += min_inst_savings
self.recommendations.append({
'service': 'Cloud Run',
'type': 'Min Instances',
'issue': f'Service {svc.get("name", "unknown")} has min instances but very low traffic',
'recommendation': 'Set min-instances to 0 for low-traffic services to enable scale-to-zero',
'potential_savings': min_inst_savings,
'priority': 'medium'
})
return savings
def _analyze_storage(self) -> float:
"""Analyze Cloud Storage resources."""
savings = 0.0
gcs_buckets = self.resources.get('gcs_buckets', [])
for bucket in gcs_buckets:
size_gb = bucket.get('size_gb', 0)
storage_class = bucket.get('storage_class', 'STANDARD')
if not bucket.get('has_lifecycle_policy', False) and size_gb > 100:
lifecycle_savings = size_gb * 0.012
savings += lifecycle_savings
self.recommendations.append({
'service': 'Cloud Storage',
'type': 'Lifecycle Policy',
'issue': f'Bucket {bucket.get("name", "unknown")} ({size_gb} GB) has no lifecycle policy',
'recommendation': 'Add lifecycle rule: Transition to Nearline after 30 days, Coldline after 90 days, Archive after 365 days',
'potential_savings': lifecycle_savings,
'priority': 'medium'
})
if storage_class == 'STANDARD' and size_gb > 500:
class_savings = size_gb * 0.006
savings += class_savings
self.recommendations.append({
'service': 'Cloud Storage',
'type': 'Storage Class',
'issue': f'Large bucket ({size_gb} GB) using Standard class',
'recommendation': 'Enable Autoclass for automatic storage class management based on access patterns',
'potential_savings': class_savings,
'priority': 'high'
})
return savings
def _analyze_database(self) -> float:
"""Analyze Cloud SQL, Firestore, and BigQuery costs."""
savings = 0.0
cloud_sql_instances = self.resources.get('cloud_sql_instances', [])
for db in cloud_sql_instances:
if db.get('connections_per_day', 1000) < 10:
db_cost = db.get('monthly_cost', 100)
savings += db_cost * 0.8
self.recommendations.append({
'service': 'Cloud SQL',
'type': 'Idle Resource',
'issue': f'Database {db.get("name", "unknown")} has <10 connections/day',
'recommendation': 'Stop database if not needed, or take a backup and delete',
'potential_savings': db_cost * 0.8,
'priority': 'high'
})
if db.get('utilization', 100) < 30 and not db.get('has_ha', False):
rightsize_savings = db.get('monthly_cost', 200) * 0.35
savings += rightsize_savings
self.recommendations.append({
'service': 'Cloud SQL',
'type': 'Right-sizing',
'issue': f'Cloud SQL instance {db.get("name", "unknown")} has low utilization (<30%)',
'recommendation': 'Downsize to a smaller machine type (e.g., db-custom-2-8192 to db-f1-micro for dev)',
'potential_savings': rightsize_savings,
'priority': 'medium'
})
# BigQuery optimization
bigquery_datasets = self.resources.get('bigquery_datasets', [])
for dataset in bigquery_datasets:
if dataset.get('pricing_model', 'on_demand') == 'on_demand':
monthly_tb_scanned = dataset.get('monthly_tb_scanned', 0)
if monthly_tb_scanned > 10:
slot_savings = (monthly_tb_scanned * 6.25) * 0.30
savings += slot_savings
self.recommendations.append({
'service': 'BigQuery',
'type': 'Pricing Model',
'issue': f'Scanning {monthly_tb_scanned} TB/month on on-demand pricing',
'recommendation': 'Switch to BigQuery editions with slots for predictable costs (30%+ savings at this volume)',
'potential_savings': slot_savings,
'priority': 'high'
})
if not dataset.get('has_partitioning', False):
partition_savings = dataset.get('monthly_query_cost', 50) * 0.50
savings += partition_savings
self.recommendations.append({
'service': 'BigQuery',
'type': 'Table Partitioning',
'issue': f'Tables in {dataset.get("name", "unknown")} lack partitioning',
'recommendation': 'Partition tables by date and add clustering columns to reduce bytes scanned',
'potential_savings': partition_savings,
'priority': 'medium'
})
return savings
def _analyze_networking(self) -> float:
"""Analyze networking costs (egress, Cloud NAT, etc.)."""
savings = 0.0
cloud_nat_gateways = self.resources.get('cloud_nat_gateways', [])
if len(cloud_nat_gateways) > 1:
extra_nats = len(cloud_nat_gateways) - 1
nat_savings = extra_nats * 45
savings += nat_savings
self.recommendations.append({
'service': 'Cloud NAT',
'type': 'Resource Consolidation',
'issue': f'{len(cloud_nat_gateways)} Cloud NAT gateways deployed',
'recommendation': 'Consolidate NAT gateways in dev/staging, or use Private Google Access for GCP services',
'potential_savings': nat_savings,
'priority': 'high'
})
egress_gb = self.resources.get('monthly_egress_gb', 0)
if egress_gb > 1000:
cdn_savings = egress_gb * 0.04 # CDN is cheaper than direct egress
savings += cdn_savings
self.recommendations.append({
'service': 'Networking',
'type': 'CDN Optimization',
'issue': f'High egress volume ({egress_gb} GB/month)',
'recommendation': 'Enable Cloud CDN to serve cached content at lower egress rates',
'potential_savings': cdn_savings,
'priority': 'medium'
})
return savings
def _analyze_general_optimizations(self) -> float:
"""General GCP cost optimizations."""
savings = 0.0
# Log retention
log_sinks = self.resources.get('log_sinks', [])
if not log_sinks:
log_volume_gb = self.resources.get('monthly_log_volume_gb', 0)
if log_volume_gb > 50:
log_savings = log_volume_gb * 0.50 * 0.6
savings += log_savings
self.recommendations.append({
'service': 'Cloud Logging',
'type': 'Log Exclusion',
'issue': f'{log_volume_gb} GB/month of logs without exclusion filters',
'recommendation': 'Create log exclusion filters for verbose/debug logs and route remaining to Cloud Storage via log sinks',
'potential_savings': log_savings,
'priority': 'medium'
})
# Unattached persistent disks
persistent_disks = self.resources.get('persistent_disks', [])
unattached = sum(1 for disk in persistent_disks if not disk.get('attached', True))
if unattached > 0:
disk_savings = unattached * 10 # ~$10/month per 100 GB disk
savings += disk_savings
self.recommendations.append({
'service': 'Compute Engine',
'type': 'Unused Resources',
'issue': f'{unattached} unattached persistent disks',
'recommendation': 'Snapshot and delete unused persistent disks',
'potential_savings': disk_savings,
'priority': 'high'
})
# Static external IPs
static_ips = self.resources.get('static_ips', [])
unused_ips = sum(1 for ip in static_ips if not ip.get('in_use', True))
if unused_ips > 0:
ip_savings = unused_ips * 7.30 # $0.01/hour = $7.30/month
savings += ip_savings
self.recommendations.append({
'service': 'Networking',
'type': 'Unused Resources',
'issue': f'{unused_ips} unused static external IP addresses',
'recommendation': 'Release unused static IPs to avoid hourly charges',
'potential_savings': ip_savings,
'priority': 'high'
})
# Budget alerts
if not self.resources.get('has_budget_alerts', False):
self.recommendations.append({
'service': 'Cloud Billing',
'type': 'Cost Monitoring',
'issue': 'No budget alerts configured',
'recommendation': 'Set up Cloud Billing budgets with alerts at 50%, 80%, 100% of monthly budget',
'potential_savings': 0,
'priority': 'high'
})
# Recommender API
if not self.resources.get('uses_recommender', False):
self.recommendations.append({
'service': 'Active Assist',
'type': 'Visibility',
'issue': 'GCP Recommender not reviewed',
'recommendation': 'Review Active Assist recommendations for right-sizing, idle resources, and committed use discounts',
'potential_savings': 0,
'priority': 'medium'
})
return savings
def _prioritize_recommendations(self) -> List[Dict[str, Any]]:
"""Get top priority recommendations."""
high_priority = [r for r in self.recommendations if r['priority'] == 'high']
high_priority.sort(key=lambda x: x.get('potential_savings', 0), reverse=True)
return high_priority[:5]
def generate_optimization_checklist(self) -> List[Dict[str, Any]]:
"""Generate actionable checklist for cost optimization."""
return [
{
'category': 'Immediate Actions (Today)',
'items': [
'Release unused static IPs',
'Delete unattached persistent disks',
'Stop idle Compute Engine instances',
'Set up billing budget alerts'
]
},
{
'category': 'This Week',
'items': [
'Add Cloud Storage lifecycle policies',
'Create log exclusion filters for verbose logs',
'Right-size Cloud SQL instances',
'Review Active Assist recommendations'
]
},
{
'category': 'This Month',
'items': [
'Evaluate committed use discounts',
'Migrate GKE Standard to Autopilot where applicable',
'Partition and cluster BigQuery tables',
'Enable Cloud CDN for high-egress services'
]
},
{
'category': 'Ongoing',
'items': [
'Review billing reports weekly',
'Label all resources for cost allocation',
'Monitor Active Assist recommendations monthly',
'Conduct quarterly cost optimization reviews'
]
}
]
def main():
parser = argparse.ArgumentParser(
description='GCP Cost Optimizer - Analyzes GCP resources and recommends cost savings'
)
parser.add_argument(
'--resources', '-r',
type=str,
help='Path to JSON file with current GCP resource inventory'
)
parser.add_argument(
'--monthly-spend', '-s',
type=float,
default=1000,
help='Current monthly GCP spend in USD (default: 1000)'
)
parser.add_argument(
'--output', '-o',
type=str,
help='Path to write optimization report JSON'
)
parser.add_argument(
'--json',
action='store_true',
help='Output as JSON format'
)
parser.add_argument(
'--checklist',
action='store_true',
help='Generate optimization checklist'
)
args = parser.parse_args()
if args.resources:
try:
with open(args.resources, 'r') as f:
resources = json.load(f)
except FileNotFoundError:
print(f"Error: File '{args.resources}' not found.", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError:
print(f"Error: File '{args.resources}' is not valid JSON.", file=sys.stderr)
sys.exit(1)
else:
resources = {}
optimizer = CostOptimizer(resources, args.monthly_spend)
result = optimizer.analyze_and_optimize()
if args.checklist:
result['checklist'] = optimizer.generate_optimization_checklist()
if args.output:
with open(args.output, 'w') as f:
json.dump(result, f, indent=2)
print(f"Report written to {args.output}")
elif args.json:
print(json.dumps(result, indent=2))
else:
print(f"\nGCP Cost Optimization Report")
print(f"{'=' * 40}")
print(f"Current Monthly Spend: .2f")
print(f"Potential Savings: .2f")
print(f"Optimized Spend: .2f")
print(f"Savings Percentage: {result['savings_percentage']}%")
print(f"\nTop Priority Actions:")
for i, action in enumerate(result['priority_actions'], 1):
print(f" {i}. [{action['service']}] {action['recommendation']}")
print(f" Savings: .2f/month")
print(f"\nTotal Recommendations: {len(result['recommendations'])}")
if __name__ == '__main__':
main()
FILE:scripts/deployment_manager.py
"""
GCP deployment script generator.
Creates gcloud CLI scripts and Terraform configurations for GCP architectures.
"""
import argparse
import json
import sys
from typing import Dict, Any
class DeploymentManager:
"""Generate GCP deployment scripts and IaC configurations."""
def __init__(self, app_name: str, requirements: Dict[str, Any]):
"""
Initialize with application requirements.
Args:
app_name: Application name (used for resource naming)
requirements: Dictionary with pattern, region, project requirements
"""
self.app_name = app_name.lower().replace(' ', '-')
self.requirements = requirements
self.region = requirements.get('region', 'us-central1')
self.project_id = requirements.get('project_id', 'my-project')
self.pattern = requirements.get('pattern', 'serverless_web')
def generate_gcloud_script(self) -> str:
"""
Generate gcloud CLI deployment script.
Returns:
Shell script as string
"""
if self.pattern == 'serverless_web':
return self._gcloud_serverless_web()
elif self.pattern == 'gke_microservices':
return self._gcloud_gke_microservices()
elif self.pattern == 'data_pipeline':
return self._gcloud_data_pipeline()
else:
return self._gcloud_serverless_web()
def _gcloud_serverless_web(self) -> str:
"""Generate gcloud script for serverless web pattern."""
return f"""#!/bin/bash
# GCP Serverless Web Deployment Script
# Application: {self.app_name}
# Region: {self.region}
# Pattern: Cloud Run + Firestore + Cloud Storage + Cloud CDN
set -euo pipefail
PROJECT_ID="{self.project_id}"
REGION="{self.region}"
APP_NAME="{self.app_name}"
ENVIRONMENT="-dev}"
echo "=== Deploying $APP_NAME to GCP ($ENVIRONMENT) ==="
# 1. Set project
gcloud config set project $PROJECT_ID
# 2. Enable required APIs
echo "Enabling required APIs..."
gcloud services enable \\
run.googleapis.com \\
firestore.googleapis.com \\
cloudbuild.googleapis.com \\
artifactregistry.googleapis.com \\
secretmanager.googleapis.com \\
compute.googleapis.com \\
monitoring.googleapis.com \\
logging.googleapis.com
# 3. Create Artifact Registry repository
echo "Creating Artifact Registry repository..."
gcloud artifacts repositories create $APP_NAME \\
--repository-format=docker \\
--location=$REGION \\
--description="Docker images for $APP_NAME" \\
|| echo "Repository already exists"
# 4. Build and push container image
echo "Building container image..."
gcloud builds submit \\
--tag $REGION-docker.pkg.dev/$PROJECT_ID/$APP_NAME/$APP_NAME:latest \\
.
# 5. Create Firestore database
echo "Creating Firestore database..."
gcloud firestore databases create \\
--location=$REGION \\
--type=firestore-native \\
|| echo "Firestore database already exists"
# 6. Create service account for Cloud Run
echo "Creating service account..."
SA_NAME="{APP_NAME}-run-sa"
gcloud iam service-accounts create $SA_NAME \\
--display-name="$APP_NAME Cloud Run Service Account" \\
|| echo "Service account already exists"
# Grant Firestore access
gcloud projects add-iam-policy-binding $PROJECT_ID \\
--member="serviceAccount:$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \\
--role="roles/datastore.user" \\
--condition=None
# Grant Secret Manager access
gcloud projects add-iam-policy-binding $PROJECT_ID \\
--member="serviceAccount:$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \\
--role="roles/secretmanager.secretAccessor" \\
--condition=None
# 7. Deploy Cloud Run service
echo "Deploying Cloud Run service..."
gcloud run deploy $APP_NAME-api \\
--image $REGION-docker.pkg.dev/$PROJECT_ID/$APP_NAME/$APP_NAME:latest \\
--region $REGION \\
--platform managed \\
--service-account $SA_NAME@$PROJECT_ID.iam.gserviceaccount.com \\
--memory 512Mi \\
--cpu 1 \\
--min-instances 0 \\
--max-instances 10 \\
--set-env-vars "PROJECT_ID=$PROJECT_ID,ENVIRONMENT=$ENVIRONMENT" \\
--allow-unauthenticated
# 8. Create Cloud Storage bucket for static assets
echo "Creating static assets bucket..."
BUCKET_NAME="{PROJECT_ID}-{APP_NAME}-static"
gsutil mb -l $REGION gs://$BUCKET_NAME/ || echo "Bucket already exists"
gsutil iam ch allUsers:objectViewer gs://$BUCKET_NAME
# 9. Set up Cloud Monitoring alerting
echo "Setting up monitoring..."
gcloud alpha monitoring policies create \\
--notification-channels="" \\
--display-name="$APP_NAME High Error Rate" \\
--condition-display-name="Cloud Run 5xx Error Rate" \\
--condition-filter='resource.type="cloud_run_revision" AND metric.type="run.googleapis.com/request_count" AND metric.labels.response_code_class="5xx"' \\
--condition-threshold-value=10 \\
--condition-threshold-duration=60s \\
|| echo "Alert policy creation requires additional configuration"
# 10. Output deployment info
echo ""
echo "=== Deployment Complete ==="
SERVICE_URL=$(gcloud run services describe $APP_NAME-api --region $REGION --format 'value(status.url)')
echo "Cloud Run URL: $SERVICE_URL"
echo "Static Bucket: gs://$BUCKET_NAME"
echo "Firestore: https://console.cloud.google.com/firestore?project=$PROJECT_ID"
echo "Monitoring: https://console.cloud.google.com/monitoring?project=$PROJECT_ID"
"""
def _gcloud_gke_microservices(self) -> str:
"""Generate gcloud script for GKE microservices pattern."""
return f"""#!/bin/bash
# GCP GKE Microservices Deployment Script
# Application: {self.app_name}
# Region: {self.region}
# Pattern: GKE Autopilot + Cloud SQL + Memorystore
set -euo pipefail
PROJECT_ID="{self.project_id}"
REGION="{self.region}"
APP_NAME="{self.app_name}"
ENVIRONMENT="-dev}"
CLUSTER_NAME="{APP_NAME}-cluster"
NETWORK_NAME="{APP_NAME}-vpc"
echo "=== Deploying $APP_NAME GKE Microservices ($ENVIRONMENT) ==="
# 1. Set project
gcloud config set project $PROJECT_ID
# 2. Enable required APIs
echo "Enabling required APIs..."
gcloud services enable \\
container.googleapis.com \\
sqladmin.googleapis.com \\
redis.googleapis.com \\
cloudbuild.googleapis.com \\
artifactregistry.googleapis.com \\
secretmanager.googleapis.com \\
servicenetworking.googleapis.com \\
compute.googleapis.com
# 3. Create VPC network
echo "Creating VPC network..."
gcloud compute networks create $NETWORK_NAME \\
--subnet-mode=auto \\
|| echo "Network already exists"
# Allocate IP range for private services
gcloud compute addresses create google-managed-services-$NETWORK_NAME \\
--global \\
--purpose=VPC_PEERING \\
--prefix-length=16 \\
--network=$NETWORK_NAME \\
|| echo "IP range already exists"
gcloud services vpc-peerings connect \\
--service=servicenetworking.googleapis.com \\
--ranges=google-managed-services-$NETWORK_NAME \\
--network=$NETWORK_NAME \\
|| echo "VPC peering already exists"
# 4. Create GKE Autopilot cluster
echo "Creating GKE Autopilot cluster..."
gcloud container clusters create-auto $CLUSTER_NAME \\
--region $REGION \\
--network $NETWORK_NAME \\
--release-channel regular \\
--enable-master-authorized-networks \\
--enable-private-nodes \\
|| echo "Cluster already exists"
# 5. Get cluster credentials
gcloud container clusters get-credentials $CLUSTER_NAME --region $REGION
# 6. Create Cloud SQL instance
echo "Creating Cloud SQL instance..."
gcloud sql instances create $APP_NAME-db \\
--database-version=POSTGRES_15 \\
--tier=db-custom-2-8192 \\
--region=$REGION \\
--network=$NETWORK_NAME \\
--no-assign-ip \\
--availability-type=regional \\
--backup-start-time=02:00 \\
--storage-auto-increase \\
|| echo "Cloud SQL instance already exists"
# Create database
gcloud sql databases create $APP_NAME \\
--instance=$APP_NAME-db \\
|| echo "Database already exists"
# 7. Create Memorystore Redis instance
echo "Creating Memorystore Redis instance..."
gcloud redis instances create $APP_NAME-cache \\
--size=1 \\
--region=$REGION \\
--redis-version=redis_7_0 \\
--network=$NETWORK_NAME \\
--tier=basic \\
|| echo "Redis instance already exists"
# 8. Configure Workload Identity
echo "Configuring Workload Identity..."
SA_NAME="{APP_NAME}-workload"
gcloud iam service-accounts create $SA_NAME \\
--display-name="$APP_NAME Workload Identity SA" \\
|| echo "Service account already exists"
gcloud projects add-iam-policy-binding $PROJECT_ID \\
--member="serviceAccount:$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \\
--role="roles/cloudsql.client"
gcloud iam service-accounts add-iam-policy-binding \\
$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com \\
--role="roles/iam.workloadIdentityUser" \\
--member="serviceAccount:$PROJECT_ID.svc.id.goog[default/$SA_NAME]"
echo ""
echo "=== GKE Cluster Ready ==="
echo "Cluster: $CLUSTER_NAME"
echo "Cloud SQL: $APP_NAME-db"
echo "Redis: $APP_NAME-cache"
echo ""
echo "Next: Apply Kubernetes manifests with 'kubectl apply -f k8s/'"
"""
def _gcloud_data_pipeline(self) -> str:
"""Generate gcloud script for data pipeline pattern."""
return f"""#!/bin/bash
# GCP Data Pipeline Deployment Script
# Application: {self.app_name}
# Region: {self.region}
# Pattern: Pub/Sub + Dataflow + BigQuery
set -euo pipefail
PROJECT_ID="{self.project_id}"
REGION="{self.region}"
APP_NAME="{self.app_name}"
echo "=== Deploying $APP_NAME Data Pipeline ==="
# 1. Set project
gcloud config set project $PROJECT_ID
# 2. Enable required APIs
echo "Enabling required APIs..."
gcloud services enable \\
pubsub.googleapis.com \\
dataflow.googleapis.com \\
bigquery.googleapis.com \\
storage.googleapis.com \\
monitoring.googleapis.com
# 3. Create Pub/Sub topic and subscription
echo "Creating Pub/Sub resources..."
gcloud pubsub topics create $APP_NAME-events \\
|| echo "Topic already exists"
gcloud pubsub subscriptions create $APP_NAME-events-sub \\
--topic=$APP_NAME-events \\
--ack-deadline=60 \\
--message-retention-duration=7d \\
|| echo "Subscription already exists"
# Dead letter topic
gcloud pubsub topics create $APP_NAME-events-dlq \\
|| echo "DLQ topic already exists"
gcloud pubsub subscriptions update $APP_NAME-events-sub \\
--dead-letter-topic=$APP_NAME-events-dlq \\
--max-delivery-attempts=5
# 4. Create BigQuery dataset and table
echo "Creating BigQuery resources..."
bq mk --dataset --location=$REGION $PROJECT_ID:{APP_NAME//-/_}_analytics \\
|| echo "Dataset already exists"
bq mk --table \\
$PROJECT_ID:{APP_NAME//-/_}_analytics.events \\
event_id:STRING,event_type:STRING,payload:STRING,timestamp:TIMESTAMP,processed_at:TIMESTAMP \\
--time_partitioning_type=DAY \\
--time_partitioning_field=timestamp \\
--clustering_fields=event_type \\
|| echo "Table already exists"
# 5. Create Cloud Storage bucket for Dataflow temp/staging
echo "Creating staging bucket..."
STAGING_BUCKET="{PROJECT_ID}-{APP_NAME}-dataflow"
gsutil mb -l $REGION gs://$STAGING_BUCKET/ || echo "Bucket already exists"
# 6. Create service account for Dataflow
echo "Creating Dataflow service account..."
SA_NAME="{APP_NAME}-dataflow-sa"
gcloud iam service-accounts create $SA_NAME \\
--display-name="$APP_NAME Dataflow Worker SA" \\
|| echo "Service account already exists"
for ROLE in roles/dataflow.worker roles/bigquery.dataEditor roles/pubsub.subscriber roles/storage.objectAdmin; do
gcloud projects add-iam-policy-binding $PROJECT_ID \\
--member="serviceAccount:$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \\
--role="$ROLE" \\
--condition=None
done
echo ""
echo "=== Data Pipeline Infrastructure Ready ==="
echo "Pub/Sub Topic: $APP_NAME-events"
echo "BigQuery Dataset: {APP_NAME//-/_}_analytics"
echo "Staging Bucket: gs://$STAGING_BUCKET"
echo ""
echo "Next: Deploy Dataflow job with Apache Beam pipeline"
echo " python -m apache_beam.examples.streaming_wordcount \\\\"
echo " --runner DataflowRunner \\\\"
echo " --project $PROJECT_ID \\\\"
echo " --region $REGION \\\\"
echo " --temp_location gs://$STAGING_BUCKET/temp"
"""
def generate_terraform_configuration(self) -> str:
"""
Generate Terraform configuration for the selected pattern.
Returns:
Terraform HCL configuration as string
"""
if self.pattern == 'serverless_web':
return self._terraform_serverless_web()
elif self.pattern == 'gke_microservices':
return self._terraform_gke_microservices()
else:
return self._terraform_serverless_web()
def _terraform_serverless_web(self) -> str:
"""Generate Terraform for serverless web pattern."""
return f"""terraform {{
required_version = ">= 1.0"
required_providers {{
google = {{
source = "hashicorp/google"
version = "~> 5.0"
}}
}}
}}
provider "google" {{
project = var.project_id
region = var.region
}}
variable "project_id" {{
description = "GCP project ID"
type = string
}}
variable "region" {{
description = "GCP region"
type = string
default = "{self.region}"
}}
variable "environment" {{
description = "Environment name"
type = string
default = "dev"
}}
variable "app_name" {{
description = "Application name"
type = string
default = "{self.app_name}"
}}
# Enable required APIs
resource "google_project_service" "apis" {{
for_each = toset([
"run.googleapis.com",
"firestore.googleapis.com",
"secretmanager.googleapis.com",
"artifactregistry.googleapis.com",
"monitoring.googleapis.com",
])
project = var.project_id
service = each.value
}}
# Service Account for Cloud Run
resource "google_service_account" "cloud_run" {{
account_id = "{var.app_name}-run-sa"
display_name = "{var.app_name} Cloud Run Service Account"
}}
resource "google_project_iam_member" "firestore_user" {{
project = var.project_id
role = "roles/datastore.user"
member = "serviceAccount:{google_service_account.cloud_run.email}"
}}
resource "google_project_iam_member" "secret_accessor" {{
project = var.project_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:{google_service_account.cloud_run.email}"
}}
# Firestore Database
resource "google_firestore_database" "default" {{
project = var.project_id
name = "(default)"
location_id = var.region
type = "FIRESTORE_NATIVE"
depends_on = [google_project_service.apis["firestore.googleapis.com"]]
}}
# Cloud Run Service
resource "google_cloud_run_v2_service" "api" {{
name = "{var.environment}-{var.app_name}-api"
location = var.region
template {{
service_account = google_service_account.cloud_run.email
containers {{
image = "{var.region}-docker.pkg.dev/{var.project_id}/{var.app_name}/{var.app_name}:latest"
resources {{
limits = {{
cpu = "1000m"
memory = "512Mi"
}}
}}
env {{
name = "PROJECT_ID"
value = var.project_id
}}
env {{
name = "ENVIRONMENT"
value = var.environment
}}
}}
scaling {{
min_instance_count = 0
max_instance_count = 10
}}
}}
depends_on = [google_project_service.apis["run.googleapis.com"]]
labels = {{
environment = var.environment
app = var.app_name
}}
}}
# Allow unauthenticated access (public API)
resource "google_cloud_run_v2_service_iam_member" "public" {{
project = var.project_id
location = var.region
name = google_cloud_run_v2_service.api.name
role = "roles/run.invoker"
member = "allUsers"
}}
# Cloud Storage bucket for static assets
resource "google_storage_bucket" "static" {{
name = "{var.project_id}-{var.app_name}-static"
location = var.region
uniform_bucket_level_access = true
website {{
main_page_suffix = "index.html"
not_found_page = "404.html"
}}
lifecycle_rule {{
condition {{
age = 30
}}
action {{
type = "SetStorageClass"
storage_class = "NEARLINE"
}}
}}
labels = {{
environment = var.environment
app = var.app_name
}}
}}
# Outputs
output "cloud_run_url" {{
description = "Cloud Run service URL"
value = google_cloud_run_v2_service.api.uri
}}
output "static_bucket" {{
description = "Static assets bucket name"
value = google_storage_bucket.static.name
}}
output "service_account" {{
description = "Cloud Run service account email"
value = google_service_account.cloud_run.email
}}
"""
def _terraform_gke_microservices(self) -> str:
"""Generate Terraform for GKE microservices pattern."""
return f"""terraform {{
required_version = ">= 1.0"
required_providers {{
google = {{
source = "hashicorp/google"
version = "~> 5.0"
}}
}}
}}
provider "google" {{
project = var.project_id
region = var.region
}}
variable "project_id" {{
description = "GCP project ID"
type = string
}}
variable "region" {{
description = "GCP region"
type = string
default = "{self.region}"
}}
variable "environment" {{
description = "Environment name"
type = string
default = "dev"
}}
variable "app_name" {{
description = "Application name"
type = string
default = "{self.app_name}"
}}
# Enable required APIs
resource "google_project_service" "apis" {{
for_each = toset([
"container.googleapis.com",
"sqladmin.googleapis.com",
"redis.googleapis.com",
"servicenetworking.googleapis.com",
"secretmanager.googleapis.com",
])
project = var.project_id
service = each.value
}}
# VPC Network
resource "google_compute_network" "main" {{
name = "{var.app_name}-vpc"
auto_create_subnetworks = false
}}
resource "google_compute_subnetwork" "main" {{
name = "{var.app_name}-subnet"
ip_cidr_range = "10.0.0.0/20"
region = var.region
network = google_compute_network.main.id
secondary_ip_range {{
range_name = "pods"
ip_cidr_range = "10.4.0.0/14"
}}
secondary_ip_range {{
range_name = "services"
ip_cidr_range = "10.8.0.0/20"
}}
}}
# GKE Autopilot Cluster
resource "google_container_cluster" "main" {{
name = "{var.environment}-{var.app_name}-cluster"
location = var.region
enable_autopilot = true
network = google_compute_network.main.name
subnetwork = google_compute_subnetwork.main.name
ip_allocation_policy {{
cluster_secondary_range_name = "pods"
services_secondary_range_name = "services"
}}
release_channel {{
channel = "REGULAR"
}}
depends_on = [google_project_service.apis["container.googleapis.com"]]
}}
# Private Services Access for Cloud SQL
resource "google_compute_global_address" "private_ip" {{
name = "private-ip-range"
purpose = "VPC_PEERING"
address_type = "INTERNAL"
prefix_length = 16
network = google_compute_network.main.id
}}
resource "google_service_networking_connection" "private_vpc" {{
network = google_compute_network.main.id
service = "servicenetworking.googleapis.com"
reserved_peering_ranges = [google_compute_global_address.private_ip.name]
}}
# Cloud SQL PostgreSQL
resource "google_sql_database_instance" "main" {{
name = "{var.environment}-{var.app_name}-db"
database_version = "POSTGRES_15"
region = var.region
settings {{
tier = "db-custom-2-8192"
availability_type = "REGIONAL"
backup_configuration {{
enabled = true
start_time = "02:00"
point_in_time_recovery_enabled = true
}}
ip_configuration {{
ipv4_enabled = false
private_network = google_compute_network.main.id
}}
disk_autoresize = true
}}
depends_on = [google_service_networking_connection.private_vpc]
}}
resource "google_sql_database" "app" {{
name = var.app_name
instance = google_sql_database_instance.main.name
}}
# Memorystore Redis
resource "google_redis_instance" "cache" {{
name = "{var.environment}-{var.app_name}-cache"
tier = "BASIC"
memory_size_gb = 1
region = var.region
redis_version = "REDIS_7_0"
authorized_network = google_compute_network.main.id
depends_on = [google_project_service.apis["redis.googleapis.com"]]
labels = {{
environment = var.environment
app = var.app_name
}}
}}
# Outputs
output "cluster_name" {{
description = "GKE cluster name"
value = google_container_cluster.main.name
}}
output "cloud_sql_connection" {{
description = "Cloud SQL connection name"
value = google_sql_database_instance.main.connection_name
}}
output "redis_host" {{
description = "Memorystore Redis host"
value = google_redis_instance.cache.host
}}
"""
def main():
parser = argparse.ArgumentParser(
description='GCP Deployment Manager - Generates gcloud CLI scripts and Terraform configurations'
)
parser.add_argument(
'--app-name', '-a',
type=str,
required=True,
help='Application name'
)
parser.add_argument(
'--pattern', '-p',
type=str,
choices=['serverless_web', 'gke_microservices', 'data_pipeline'],
default='serverless_web',
help='Architecture pattern (default: serverless_web)'
)
parser.add_argument(
'--region', '-r',
type=str,
default='us-central1',
help='GCP region (default: us-central1)'
)
parser.add_argument(
'--project-id',
type=str,
default='my-project',
help='GCP project ID (default: my-project)'
)
parser.add_argument(
'--format', '-f',
type=str,
choices=['gcloud', 'terraform', 'both'],
default='both',
help='Output format (default: both)'
)
parser.add_argument(
'--output', '-o',
type=str,
help='Output directory for generated files'
)
parser.add_argument(
'--json',
action='store_true',
help='Output as JSON format'
)
args = parser.parse_args()
requirements = {
'pattern': args.pattern,
'region': args.region,
'project_id': args.project_id
}
manager = DeploymentManager(args.app_name, requirements)
if args.json:
output = {}
if args.format in ('gcloud', 'both'):
output['gcloud_script'] = manager.generate_gcloud_script()
if args.format in ('terraform', 'both'):
output['terraform_config'] = manager.generate_terraform_configuration()
print(json.dumps(output, indent=2))
elif args.output:
import os
os.makedirs(args.output, exist_ok=True)
if args.format in ('gcloud', 'both'):
gcloud_path = os.path.join(args.output, 'deploy.sh')
with open(gcloud_path, 'w') as f:
f.write(manager.generate_gcloud_script())
os.chmod(gcloud_path, 0o755)
print(f"gcloud script written to {gcloud_path}")
if args.format in ('terraform', 'both'):
tf_path = os.path.join(args.output, 'main.tf')
with open(tf_path, 'w') as f:
f.write(manager.generate_terraform_configuration())
print(f"Terraform config written to {tf_path}")
else:
if args.format in ('gcloud', 'both'):
print("# ===== gcloud CLI Script =====")
print(manager.generate_gcloud_script())
if args.format in ('terraform', 'both'):
print("# ===== Terraform Configuration =====")
print(manager.generate_terraform_configuration())
if __name__ == '__main__':
main()
Khung ứng phó sự cố từ phát hiện đến xử lý và rà soát sau sự cố: phân loại mức độ, dựng dòng thời gian, phân tích có cấu trúc.
---
name: "incident-commander"
description: "Comprehensive incident response framework from detection through resolution and post-incident review. Battle-tested SRE/DevOps practices: severity classification, timeline reconstruction, structured post-incident analysis. Use when declaring an incident, coordinating multi-team response during an outage, leading a post-mortem, or setting up on-call practices for a new service."
---
# Incident Commander Skill
**Category:** Engineering Team
**Tier:** POWERFUL
**Author:** Claude Skills Team
**Version:** 1.0.0
**Last Updated:** February 2026
## Overview
The Incident Commander skill provides a comprehensive incident response framework for managing technology incidents from detection through resolution and post-incident review. This skill implements battle-tested practices from SRE and DevOps teams at scale, providing structured tools for severity classification, timeline reconstruction, and thorough post-incident analysis.
## Key Features
- **Automated Severity Classification** - Intelligent incident triage based on impact and urgency metrics
- **Timeline Reconstruction** - Transform scattered logs and events into coherent incident narratives
- **Post-Incident Review Generation** - Structured PIRs with multiple RCA frameworks
- **Communication Templates** - Pre-built templates for stakeholder updates and escalations
- **Runbook Integration** - Generate actionable runbooks from incident patterns
## Skills Included
### Core Tools
1. **Incident Classifier** (`incident_classifier.py`)
- Analyzes incident descriptions and outputs severity levels
- Recommends response teams and initial actions
- Generates communication templates based on severity
2. **Timeline Reconstructor** (`timeline_reconstructor.py`)
- Processes timestamped events from multiple sources
- Reconstructs chronological incident timeline
- Identifies gaps and provides duration analysis
3. **PIR Generator** (`pir_generator.py`)
- Creates comprehensive Post-Incident Review documents
- Applies multiple RCA frameworks (5 Whys, Fishbone, Timeline)
- Generates actionable follow-up items
## Incident Response Framework
### Severity Classification System
#### SEV1 - Critical Outage
**Definition:** Complete service failure affecting all users or critical business functions
**Characteristics:**
- Customer-facing services completely unavailable
- Data loss or corruption affecting users
- Security breaches with customer data exposure
- Revenue-generating systems down
- SLA violations with financial penalties
**Response Requirements:**
- Immediate escalation to on-call engineer
- Incident Commander assigned within 5 minutes
- Executive notification within 15 minutes
- Public status page update within 15 minutes
- War room established
- All hands on deck if needed
**Communication Frequency:** Every 15 minutes until resolution
#### SEV2 - Major Impact
**Definition:** Significant degradation affecting subset of users or non-critical functions
**Characteristics:**
- Partial service degradation (>25% of users affected)
- Performance issues causing user frustration
- Non-critical features unavailable
- Internal tools impacting productivity
- Data inconsistencies not affecting user experience
**Response Requirements:**
- On-call engineer response within 15 minutes
- Incident Commander assigned within 30 minutes
- Status page update within 30 minutes
- Stakeholder notification within 1 hour
- Regular team updates
**Communication Frequency:** Every 30 minutes during active response
#### SEV3 - Minor Impact
**Definition:** Limited impact with workarounds available
**Characteristics:**
- Single feature or component affected
- <25% of users impacted
- Workarounds available
- Performance degradation not significantly impacting UX
- Non-urgent monitoring alerts
**Response Requirements:**
- Response within 2 hours during business hours
- Next business day response acceptable outside hours
- Internal team notification
- Optional status page update
**Communication Frequency:** At key milestones only
#### SEV4 - Low Impact
**Definition:** Minimal impact, cosmetic issues, or planned maintenance
**Characteristics:**
- Cosmetic bugs
- Documentation issues
- Logging or monitoring gaps
- Performance issues with no user impact
- Development/test environment issues
**Response Requirements:**
- Response within 1-2 business days
- Standard ticket/issue tracking
- No special escalation required
**Communication Frequency:** Standard development cycle updates
### Incident Commander Role
#### Primary Responsibilities
1. **Command and Control**
- Own the incident response process
- Make critical decisions about resource allocation
- Coordinate between technical teams and stakeholders
- Maintain situational awareness across all response streams
2. **Communication Hub**
- Provide regular updates to stakeholders
- Manage external communications (status pages, customer notifications)
- Facilitate effective communication between response teams
- Shield responders from external distractions
3. **Process Management**
- Ensure proper incident tracking and documentation
- Drive toward resolution while maintaining quality
- Coordinate handoffs between team members
- Plan and execute rollback strategies if needed
4. **Post-Incident Leadership**
- Ensure thorough post-incident reviews are conducted
- Drive implementation of preventive measures
- Share learnings with broader organization
#### Decision-Making Framework
**Emergency Decisions (SEV1/2):**
- Incident Commander has full authority
- Bias toward action over analysis
- Document decisions for later review
- Consult subject matter experts but don't get blocked
**Resource Allocation:**
- Can pull in any necessary team members
- Authority to escalate to senior leadership
- Can approve emergency spend for external resources
- Make call on communication channels and timing
**Technical Decisions:**
- Lean on technical leads for implementation details
- Make final calls on trade-offs between speed and risk
- Approve rollback vs. fix-forward strategies
- Coordinate testing and validation approaches
### Communication Templates
#### Initial Incident Notification (SEV1/2)
```
Subject: [SEV{severity}] {Service Name} - {Brief Description}
Incident Details:
- Start Time: {timestamp}
- Severity: SEV{level}
- Impact: {user impact description}
- Current Status: {investigating/mitigating/resolved}
Technical Details:
- Affected Services: {service list}
- Symptoms: {what users are experiencing}
- Initial Assessment: {suspected root cause if known}
Response Team:
- Incident Commander: {name}
- Technical Lead: {name}
- SMEs Engaged: {list}
Next Update: {timestamp}
Status Page: {link}
War Room: {bridge/chat link}
---
{Incident Commander Name}
{Contact Information}
```
#### Executive Summary (SEV1)
```
Subject: URGENT - Customer-Impacting Outage - {Service Name}
Executive Summary:
{2-3 sentence description of customer impact and business implications}
Key Metrics:
- Time to Detection: {X minutes}
- Time to Engagement: {X minutes}
- Estimated Customer Impact: {number/percentage}
- Current Status: {status}
- ETA to Resolution: {time or "investigating"}
Leadership Actions Required:
- [ ] Customer communication approval
- [ ] PR/Communications coordination
- [ ] Resource allocation decisions
- [ ] External vendor engagement
Incident Commander: {name} ({contact})
Next Update: {time}
---
This is an automated alert from our incident response system.
```
#### Customer Communication Template
```
We are currently experiencing {brief description of issue} affecting {scope of impact}.
Our engineering team was alerted at {time} and is actively working to resolve the issue. We will provide updates every {frequency} until resolved.
What we know:
- {factual statement of impact}
- {factual statement of scope}
- {brief status of response}
What we're doing:
- {primary response action}
- {secondary response action}
Workaround (if available):
{workaround steps or "No workaround currently available"}
We apologize for the inconvenience and will share more information as it becomes available.
Next update: {time}
Status page: {link}
```
### Stakeholder Management
#### Stakeholder Classification
**Internal Stakeholders:**
- **Engineering Leadership** - Technical decisions and resource allocation
- **Product Management** - Customer impact assessment and feature implications
- **Customer Support** - User communication and support ticket management
- **Sales/Account Management** - Customer relationship management for enterprise clients
- **Executive Team** - Business impact decisions and external communication approval
- **Legal/Compliance** - Regulatory reporting and liability assessment
**External Stakeholders:**
- **Customers** - Service availability and impact communication
- **Partners** - API availability and integration impacts
- **Vendors** - Third-party service dependencies and support escalation
- **Regulators** - Compliance reporting for regulated industries
- **Public/Media** - Transparency for public-facing outages
#### Communication Cadence by Stakeholder
| Stakeholder | SEV1 | SEV2 | SEV3 | SEV4 |
|-------------|------|------|------|------|
| Engineering Leadership | Real-time | 30min | 4hrs | Daily |
| Executive Team | 15min | 1hr | EOD | Weekly |
| Customer Support | Real-time | 30min | 2hrs | As needed |
| Customers | 15min | 1hr | Optional | None |
| Partners | 30min | 2hrs | Optional | None |
### Runbook Generation Framework
#### Dynamic Runbook Components
1. **Detection Playbooks**
- Monitoring alert definitions
- Triage decision trees
- Escalation trigger points
- Initial response actions
2. **Response Playbooks**
- Step-by-step mitigation procedures
- Rollback instructions
- Validation checkpoints
- Communication checkpoints
3. **Recovery Playbooks**
- Service restoration procedures
- Data consistency checks
- Performance validation
- User notification processes
#### Runbook Template Structure
```markdown
# {Service/Component} Incident Response Runbook
## Quick Reference
- **Severity Indicators:** {list of conditions for each severity level}
- **Key Contacts:** {on-call rotations and escalation paths}
- **Critical Commands:** {list of emergency commands with descriptions}
## Detection
### Monitoring Alerts
- {Alert name}: {description and thresholds}
- {Alert name}: {description and thresholds}
### Manual Detection Signs
- {Symptom}: {what to look for and where}
- {Symptom}: {what to look for and where}
## Initial Response (0-15 minutes)
1. **Assess Severity**
- [ ] Check {primary metric}
- [ ] Verify {secondary indicator}
- [ ] Classify as SEV{level} based on {criteria}
2. **Establish Command**
- [ ] Page Incident Commander if SEV1/2
- [ ] Create incident tracking ticket
- [ ] Join war room: {link/bridge info}
3. **Initial Investigation**
- [ ] Check recent deployments: {deployment log location}
- [ ] Review error logs: {log location and queries}
- [ ] Verify dependencies: {dependency check commands}
## Mitigation Strategies
### Strategy 1: {Name}
**Use when:** {conditions}
**Steps:**
1. {detailed step with commands}
2. {detailed step with expected outcomes}
3. {validation step}
**Rollback Plan:**
1. {rollback step}
2. {verification step}
### Strategy 2: {Name}
{similar structure}
## Recovery and Validation
1. **Service Restoration**
- [ ] {restoration step}
- [ ] Wait for {metric} to return to normal
- [ ] Validate end-to-end functionality
2. **Communication**
- [ ] Update status page
- [ ] Notify stakeholders
- [ ] Schedule PIR
## Common Pitfalls
- **{Pitfall}:** {description and how to avoid}
- **{Pitfall}:** {description and how to avoid}
## Reference Information
→ See references/reference-information.md for details
## Usage Examples
### Example 1: Database Connection Pool Exhaustion
```bash
# Classify the incident
echo '{"description": "Users reporting 500 errors, database connections timing out", "affected_users": "80%", "business_impact": "high"}' | python scripts/incident_classifier.py
# Reconstruct timeline from logs
python scripts/timeline_reconstructor.py --input assets/db_incident_events.json --output timeline.md
# Generate PIR after resolution
python scripts/pir_generator.py --incident assets/db_incident_data.json --timeline timeline.md --output pir.md
```
### Example 2: API Rate Limiting Incident
```bash
# Quick classification from stdin
echo "API rate limits causing customer API calls to fail" | python scripts/incident_classifier.py --format text
# Build timeline from multiple sources
python scripts/timeline_reconstructor.py --input assets/api_incident_logs.json --detect-phases --gap-analysis
# Generate comprehensive PIR
python scripts/pir_generator.py --incident assets/api_incident_summary.json --rca-method fishbone --action-items
```
## Best Practices
### During Incident Response
1. **Maintain Calm Leadership**
- Stay composed under pressure
- Make decisive calls with incomplete information
- Communicate confidence while acknowledging uncertainty
2. **Document Everything**
- All actions taken and their outcomes
- Decision rationale, especially for controversial calls
- Timeline of events as they happen
3. **Effective Communication**
- Use clear, jargon-free language
- Provide regular updates even when there's no new information
- Manage stakeholder expectations proactively
4. **Technical Excellence**
- Prefer rollbacks to risky fixes under pressure
- Validate fixes before declaring resolution
- Plan for secondary failures and cascading effects
### Post-Incident
1. **Blameless Culture**
- Focus on system failures, not individual mistakes
- Encourage honest reporting of what went wrong
- Celebrate learning and improvement opportunities
2. **Action Item Discipline**
- Assign specific owners and due dates
- Track progress publicly
- Prioritize based on risk and effort
3. **Knowledge Sharing**
- Share PIRs broadly within the organization
- Update runbooks based on lessons learned
- Conduct training sessions for common failure modes
4. **Continuous Improvement**
- Look for patterns across multiple incidents
- Invest in tooling and automation
- Regularly review and update processes
## Integration with Existing Tools
### Monitoring and Alerting
- PagerDuty/Opsgenie integration for escalation
- Datadog/Grafana for metrics and dashboards
- ELK/Splunk for log analysis and correlation
### Communication Platforms
- Slack/Teams for war room coordination
- Zoom/Meet for video bridges
- Status page providers (Statuspage.io, etc.)
### Documentation Systems
- Confluence/Notion for PIR storage
- GitHub/GitLab for runbook version control
- JIRA/Linear for action item tracking
### Change Management
- CI/CD pipeline integration
- Deployment tracking systems
- Feature flag platforms for quick rollbacks
## Conclusion
The Incident Commander skill provides a comprehensive framework for managing incidents from detection through post-incident review. By implementing structured processes, clear communication templates, and thorough analysis tools, teams can improve their incident response capabilities and build more resilient systems.
The key to successful incident management is preparation, practice, and continuous learning. Use this framework as a starting point, but adapt it to your organization's specific needs, culture, and technical environment.
Remember: The goal isn't to prevent all incidents (which is impossible), but to detect them quickly, respond effectively, communicate clearly, and learn continuously.
FILE:assets/incident_report_template.md
# Incident Report: [INC-YYYY-NNNN] [Title]
**Severity:** SEV[1-4]
**Status:** [Active | Mitigated | Resolved]
**Incident Commander:** [Name]
**Date:** [YYYY-MM-DD]
---
## Executive Summary
[2-3 sentence summary of the incident: what happened, impact scope, resolution status. Written for executive audience — no jargon, focus on business impact.]
---
## Impact Statement
| Metric | Value |
|--------|-------|
| **Duration** | [X hours Y minutes] |
| **Affected Users** | [number or percentage] |
| **Failed Transactions** | [number] |
| **Revenue Impact** | $[amount] |
| **Data Loss** | [Yes/No — if yes, detail below] |
| **SLA Impact** | [X.XX% availability for period] |
| **Affected Regions** | [list regions] |
| **Affected Services** | [list services] |
### Customer-Facing Impact
[Describe what customers experienced: error messages, degraded functionality, complete outage. Be specific about which user journeys were affected.]
---
## Timeline
| Time (UTC) | Phase | Event |
|------------|-------|-------|
| HH:MM | Detection | [First alert or report] |
| HH:MM | Declaration | [Incident declared, channel created] |
| HH:MM | Investigation | [Key investigation findings] |
| HH:MM | Mitigation | [Mitigation action taken] |
| HH:MM | Resolution | [Permanent fix applied] |
| HH:MM | Closure | [Incident closed, monitoring confirmed stable] |
### Key Decision Points
1. **[HH:MM] [Decision]** — [Rationale and outcome]
2. **[HH:MM] [Decision]** — [Rationale and outcome]
### Timeline Gaps
[Note any periods >15 minutes without logged events. These represent potential blind spots in the response.]
---
## Root Cause Analysis
### Root Cause
[Clear, specific statement of the root cause. Not "human error" — describe the systemic failure.]
### Contributing Factors
1. **[Factor Category: Process/Tooling/Human/Environment]** — [Description]
2. **[Factor Category]** — [Description]
3. **[Factor Category]** — [Description]
### 5-Whys Analysis
**Why did the service degrade?**
→ [Answer]
**Why did [answer above] happen?**
→ [Answer]
**Why did [answer above] happen?**
→ [Answer]
**Why did [answer above] happen?**
→ [Answer]
**Why did [answer above] happen?**
→ [Root systemic cause]
---
## Response Metrics
| Metric | Value | Target | Status |
|--------|-------|--------|--------|
| **MTTD** (Mean Time to Detect) | [X min] | <5 min | [Met/Missed] |
| **Time to Declare** | [X min] | <10 min | [Met/Missed] |
| **Time to Mitigate** | [X min] | <60 min (SEV1) | [Met/Missed] |
| **MTTR** (Mean Time to Resolve) | [X min] | <4 hr (SEV1) | [Met/Missed] |
| **Postmortem Timeliness** | [X hours] | <72 hr | [Met/Missed] |
---
## Action Items
| # | Priority | Action | Owner | Deadline | Type | Status |
|---|----------|--------|-------|----------|------|--------|
| 1 | P1 | [Action description] | [owner] | [date] | Detection | Open |
| 2 | P1 | [Action description] | [owner] | [date] | Prevention | Open |
| 3 | P2 | [Action description] | [owner] | [date] | Prevention | Open |
| 4 | P2 | [Action description] | [owner] | [date] | Process | Open |
### Action Item Types
- **Detection**: Improve ability to detect this class of issue faster
- **Prevention**: Prevent this class of issue from occurring
- **Mitigation**: Reduce impact when this class of issue occurs
- **Process**: Improve response process and coordination
---
## Lessons Learned
### What Went Well
- [Specific positive outcome from the response]
- [Specific positive outcome]
### What Didn't Go Well
- [Specific area for improvement]
- [Specific area for improvement]
### Where We Got Lucky
- [Things that could have made this worse but didn't]
---
## Communication Log
| Time (UTC) | Channel | Audience | Summary |
|------------|---------|----------|---------|
| HH:MM | Status Page | External | [Summary of update] |
| HH:MM | Slack #exec | Internal | [Summary of update] |
| HH:MM | Email | Customers | [Summary of notification] |
---
## Participants
| Name | Role |
|------|------|
| [Name] | Incident Commander |
| [Name] | Operations Lead |
| [Name] | Communications Lead |
| [Name] | Subject Matter Expert |
---
## Appendix
### Related Incidents
- [INC-YYYY-NNNN] — [Brief description of related incident]
### Reference Links
- [Link to monitoring dashboard]
- [Link to deployment logs]
- [Link to incident channel archive]
---
*This report follows the blameless postmortem principle. The goal is systemic improvement, not individual accountability. All contributing factors should trace to process, tooling, or environmental gaps that can be addressed with concrete action items.*
FILE:assets/runbook_template.md
# Runbook: [Service/Component Name]
**Owner:** [Team Name]
**Last Updated:** [YYYY-MM-DD]
**Reviewed By:** [Name]
**Review Cadence:** Quarterly
---
## Service Overview
| Property | Value |
|----------|-------|
| **Service** | [service-name] |
| **Repository** | [repo URL] |
| **Dashboard** | [monitoring dashboard URL] |
| **On-Call Rotation** | [PagerDuty/OpsGenie schedule URL] |
| **SLA Tier** | [Tier 1/2/3] |
| **Availability Target** | [99.9% / 99.95% / 99.99%] |
| **Dependencies** | [list upstream/downstream services] |
| **Owner Team** | [team name] |
| **Escalation Contact** | [name/email] |
### Architecture Summary
[2-3 sentence description of the service architecture. Include key components, data stores, and external dependencies.]
---
## Alert Response Decision Tree
### High Error Rate (>5%)
```
Error Rate Alert Fired
├── Check: Is this a deployment-related issue?
│ ├── YES → Go to "Recent Deployment Rollback" section
│ └── NO → Continue
├── Check: Is a downstream dependency failing?
│ ├── YES → Go to "Dependency Failure" section
│ └── NO → Continue
├── Check: Is there unusual traffic volume?
│ ├── YES → Go to "Traffic Spike" section
│ └── NO → Continue
└── Escalate: Engage on-call secondary + service owner
```
### High Latency (p99 > [threshold]ms)
```
Latency Alert Fired
├── Check: Database query latency elevated?
│ ├── YES → Go to "Database Performance" section
│ └── NO → Continue
├── Check: Connection pool utilization >80%?
│ ├── YES → Go to "Connection Pool Exhaustion" section
│ └── NO → Continue
├── Check: Memory/CPU pressure on service instances?
│ ├── YES → Go to "Resource Exhaustion" section
│ └── NO → Continue
└── Escalate: Engage on-call secondary + service owner
```
### Service Unavailable (Health Check Failing)
```
Health Check Alert Fired
├── Check: Are all instances down?
│ ├── YES → Go to "Complete Outage" section
│ └── NO → Continue
├── Check: Is only one AZ affected?
│ ├── YES → Go to "AZ Failure" section
│ └── NO → Continue
├── Check: Can instances be restarted?
│ ├── YES → Go to "Instance Restart" section
│ └── NO → Continue
└── Escalate: Declare incident, engage IC
```
---
## Common Scenarios
### Recent Deployment Rollback
**Symptoms:** Error rate spike or latency increase within 60 minutes of a deployment.
**Diagnosis:**
1. Check deployment history: `kubectl rollout history deployment/[service-name]`
2. Compare error rate timing with deployment timestamp
3. Review deployment diff for risky changes
**Mitigation:**
1. Initiate rollback: `kubectl rollout undo deployment/[service-name]`
2. Verify rollback: `kubectl rollout status deployment/[service-name]`
3. Confirm error rate returns to baseline (allow 5 minutes)
4. If rollback fails: escalate immediately
**Communication:** If customer-impacting, update status page within 5 minutes of confirming impact.
---
### Database Performance
**Symptoms:** Elevated query latency, connection pool saturation, timeout errors.
**Diagnosis:**
1. Check active queries: `SELECT * FROM pg_stat_activity WHERE state = 'active';`
2. Check for long-running queries: `SELECT pid, now() - pg_stat_activity.query_start AS duration, query FROM pg_stat_activity WHERE state != 'idle' ORDER BY duration DESC;`
3. Check connection count: `SELECT count(*) FROM pg_stat_activity;`
4. Check table bloat and vacuum status
**Mitigation:**
1. Kill long-running queries if identified: `SELECT pg_terminate_backend([pid]);`
2. If connection pool exhausted: increase pool size via config (requires restart)
3. If read replica available: redirect read traffic
4. If write-heavy: identify and defer non-critical writes
**Escalation Trigger:** If query latency >10s for >5 minutes, escalate to DBA on-call.
---
### Connection Pool Exhaustion
**Symptoms:** Connection timeout errors, pool utilization >90%, requests queuing.
**Diagnosis:**
1. Check pool metrics: current size, active connections, waiting requests
2. Check for connection leaks: connections held >30s without activity
3. Review recent config changes or deployments
**Mitigation:**
1. Increase pool size (if infrastructure allows): update config, rolling restart
2. Kill idle connections exceeding timeout
3. If caused by leak: identify and restart affected instances
4. Enable connection pool auto-scaling if available
**Prevention:** Pool utilization alerting at 70% (warning) and 85% (critical).
---
### Dependency Failure
**Symptoms:** Errors correlated with downstream service failures, circuit breakers tripping.
**Diagnosis:**
1. Check dependency status dashboards
2. Verify circuit breaker state: open/half-open/closed
3. Check for correlation with dependency deployments or incidents
4. Test dependency health endpoints directly
**Mitigation:**
1. If circuit breaker not tripping: verify timeout/threshold configuration
2. Enable graceful degradation (serve cached/default responses)
3. If critical path: engage dependency team via incident process
4. If non-critical path: disable feature flag for affected functionality
**Communication:** Coordinate with dependency team IC if both services have active incidents.
---
### Traffic Spike
**Symptoms:** Sudden traffic increase beyond normal patterns, resource saturation.
**Diagnosis:**
1. Check traffic source: organic growth vs. bot traffic vs. DDoS
2. Review rate limiting effectiveness
3. Check auto-scaling status and capacity
**Mitigation:**
1. If bot/DDoS: enable rate limiting, engage security team
2. If organic: trigger manual scale-up, increase auto-scaling limits
3. Enable request queuing or load shedding if at capacity
4. Consider feature flag toggles to reduce per-request cost
---
### Complete Outage
**Symptoms:** All instances unreachable, health checks failing across AZs.
**Diagnosis:**
1. Check infrastructure status (AWS/GCP status page)
2. Verify network connectivity and DNS resolution
3. Check for infrastructure-level incidents (region outage)
4. Review recent infrastructure changes (Terraform, network config)
**Mitigation:**
1. If infra provider issue: activate disaster recovery plan
2. If DNS issue: update DNS records, reduce TTL
3. If deployment corruption: redeploy last known good version
4. If data corruption: engage data recovery procedures
**Escalation:** Immediately declare SEV1 incident. Engage infrastructure team and management.
---
### Instance Restart
**Symptoms:** Individual instances unhealthy, OOM kills, process crashes.
**Diagnosis:**
1. Check instance logs for crash reason
2. Review memory/CPU usage patterns before crash
3. Check for memory leaks or resource exhaustion
4. Verify configuration consistency across instances
**Mitigation:**
1. Restart unhealthy instances: `kubectl delete pod [pod-name]`
2. If recurring: cordon node and migrate workloads
3. If memory leak: schedule immediate patch with increased memory limit
4. Monitor for recurrence after restart
---
### AZ Failure
**Symptoms:** All instances in one availability zone failing, others healthy.
**Diagnosis:**
1. Confirm AZ-specific failure vs. instance-specific issues
2. Check cloud provider AZ status
3. Verify load balancer is routing around failed AZ
**Mitigation:**
1. Ensure load balancer marks AZ instances as unhealthy
2. Scale up remaining AZs to handle redirected traffic
3. If auto-scaling: verify it's responding to increased load
4. Monitor remaining AZs for cascade effects
---
## Key Metrics & Dashboards
| Metric | Normal Range | Warning | Critical | Dashboard |
|--------|-------------|---------|----------|-----------|
| Error Rate | <0.1% | >1% | >5% | [link] |
| p99 Latency | <200ms | >500ms | >2000ms | [link] |
| CPU Usage | <60% | >75% | >90% | [link] |
| Memory Usage | <70% | >80% | >90% | [link] |
| DB Pool Usage | <50% | >70% | >85% | [link] |
| Request Rate | [baseline]±20% | ±50% | ±100% | [link] |
---
## Escalation Contacts
| Level | Contact | When |
|-------|---------|------|
| L1: On-Call Primary | [name/rotation] | First responder |
| L2: On-Call Secondary | [name/rotation] | Primary unavailable or needs help |
| L3: Service Owner | [name] | Complex issues, architectural decisions |
| L4: Engineering Manager | [name] | SEV1/SEV2, customer impact, resource needs |
| L5: VP Engineering | [name] | SEV1 >30 min, major customer/revenue impact |
---
## Maintenance Procedures
### Planned Maintenance Checklist
- [ ] Maintenance window scheduled and communicated (72 hours advance for Tier 1)
- [ ] Status page updated with planned maintenance notice
- [ ] Rollback plan documented and tested
- [ ] On-call notified of maintenance window
- [ ] Customer notification sent (if SLA-impacting)
- [ ] Post-maintenance verification plan ready
### Health Verification After Changes
1. Check all health endpoints return 200
2. Verify error rate returns to baseline within 5 minutes
3. Confirm latency within normal range
4. Run synthetic transaction test
5. Monitor for 15 minutes before declaring success
---
## Revision History
| Date | Author | Change |
|------|--------|--------|
| [YYYY-MM-DD] | [Name] | Initial version |
| [YYYY-MM-DD] | [Name] | [Description of update] |
---
*This runbook should be reviewed quarterly and updated after every incident that reveals missing procedures. The on-call engineer should be able to follow this document without prior context about the service. If any section requires tribal knowledge to execute, it needs to be expanded.*
FILE:assets/sample_incident_classification.json
{
"description": "Database connection timeouts causing 500 errors for payment processing API. Users unable to complete checkout. Error rate spiked from 0.1% to 45% starting at 14:30 UTC. Database monitoring shows connection pool exhaustion with 200/200 connections active.",
"service": "payment-api",
"affected_users": "80%",
"business_impact": "high",
"duration_minutes": 95,
"metadata": {
"error_rate": "45%",
"connection_pool_utilization": "100%",
"affected_regions": ["us-west", "us-east", "eu-west"],
"detection_method": "monitoring_alert",
"customer_escalations": 12
}
}
FILE:assets/sample_incident_data.json
{
"incident": {
"id": "INC-2024-0142",
"title": "Payment Service Degradation",
"severity": "SEV1",
"status": "resolved",
"declared_at": "2024-01-15T14:23:00Z",
"resolved_at": "2024-01-15T16:45:00Z",
"commander": "Jane Smith",
"service": "payment-gateway",
"affected_services": ["checkout", "subscription-billing"]
},
"events": [
{
"timestamp": "2024-01-15T14:15:00Z",
"type": "trigger",
"actor": "system",
"description": "Database connection pool utilization reaches 95% on payment-gateway primary",
"metadata": {"metric": "db_pool_utilization", "value": 95, "threshold": 90}
},
{
"timestamp": "2024-01-15T14:20:00Z",
"type": "detection",
"actor": "monitoring",
"description": "PagerDuty alert fired: payment-gateway error rate >5% (current: 8.2%)",
"metadata": {"alert_id": "PD-98765", "source": "datadog", "error_rate": 8.2}
},
{
"timestamp": "2024-01-15T14:21:00Z",
"type": "detection",
"actor": "monitoring",
"description": "Datadog alert: p99 latency on /api/payments exceeds 5000ms (current: 8500ms)",
"metadata": {"alert_id": "DD-54321", "source": "datadog", "latency_p99_ms": 8500}
},
{
"timestamp": "2024-01-15T14:23:00Z",
"type": "declaration",
"actor": "Jane Smith",
"description": "SEV1 declared. Incident channel #inc-20240115-payment-degradation created. Bridge call started.",
"metadata": {"channel": "#inc-20240115-payment-degradation", "severity": "SEV1"}
},
{
"timestamp": "2024-01-15T14:25:00Z",
"type": "investigation",
"actor": "Alice Chen",
"description": "Confirmed: database connection pool at 100% utilization. All new connections being rejected.",
"metadata": {"pool_size": 20, "active_connections": 20, "waiting_requests": 147}
},
{
"timestamp": "2024-01-15T14:28:00Z",
"type": "investigation",
"actor": "Carol Davis",
"description": "Identified recent deployment of user-api v2.4.1 at 13:45 UTC. New ORM version (3.2.0) changed connection handling behavior.",
"metadata": {"deployment": "user-api-v2.4.1", "deployed_at": "2024-01-15T13:45:00Z"}
},
{
"timestamp": "2024-01-15T14:30:00Z",
"type": "communication",
"actor": "Bob Kim",
"description": "Status page updated: Investigating - We are investigating increased error rates affecting payment processing.",
"metadata": {"channel": "status_page", "status": "investigating"}
},
{
"timestamp": "2024-01-15T14:35:00Z",
"type": "escalation",
"actor": "Jane Smith",
"description": "Escalated to VP Engineering. Customer impact confirmed: 12,500+ users affected, failed transactions accumulating.",
"metadata": {"escalated_to": "VP Engineering", "reason": "revenue_impact"}
},
{
"timestamp": "2024-01-15T14:40:00Z",
"type": "mitigation",
"actor": "Alice Chen",
"description": "Attempting mitigation: increasing connection pool size from 20 to 50 via config override.",
"metadata": {"action": "pool_resize", "old_value": 20, "new_value": 50}
},
{
"timestamp": "2024-01-15T14:45:00Z",
"type": "communication",
"actor": "Bob Kim",
"description": "Status page updated: Identified - The issue has been identified as a database configuration problem. We are implementing a fix.",
"metadata": {"channel": "status_page", "status": "identified"}
},
{
"timestamp": "2024-01-15T14:50:00Z",
"type": "investigation",
"actor": "Carol Davis",
"description": "Pool resize partially effective. Error rate dropped from 23% to 12%. ORM 3.2.0 opens 3x more connections per request than 3.1.2.",
"metadata": {"error_rate_before": 23.5, "error_rate_after": 12.1}
},
{
"timestamp": "2024-01-15T15:00:00Z",
"type": "mitigation",
"actor": "Alice Chen",
"description": "Decision: roll back ORM version to 3.1.2. Initiating rollback deployment of user-api v2.3.9.",
"metadata": {"action": "rollback", "target_version": "2.3.9", "rollback_reason": "orm_connection_leak"}
},
{
"timestamp": "2024-01-15T15:15:00Z",
"type": "mitigation",
"actor": "Alice Chen",
"description": "Rollback deployment complete. user-api v2.3.9 running in production. Connection pool utilization dropping.",
"metadata": {"deployment_duration_minutes": 15, "pool_utilization": 45}
},
{
"timestamp": "2024-01-15T15:20:00Z",
"type": "communication",
"actor": "Bob Kim",
"description": "Status page updated: Monitoring - A fix has been implemented and we are monitoring the results.",
"metadata": {"channel": "status_page", "status": "monitoring"}
},
{
"timestamp": "2024-01-15T15:30:00Z",
"type": "mitigation",
"actor": "Jane Smith",
"description": "Error rate back to baseline (<0.1%). Payment processing fully restored. Entering monitoring phase.",
"metadata": {"error_rate": 0.08, "pool_utilization": 32}
},
{
"timestamp": "2024-01-15T16:30:00Z",
"type": "investigation",
"actor": "Carol Davis",
"description": "Confirmed stable for 60 minutes. No degradation detected. Root cause documented: ORM 3.2.0 connection pooling incompatibility.",
"metadata": {"monitoring_duration_minutes": 60, "stable": true}
},
{
"timestamp": "2024-01-15T16:45:00Z",
"type": "resolution",
"actor": "Jane Smith",
"description": "Incident resolved. All services nominal. Postmortem scheduled for 2024-01-17 10:00 UTC.",
"metadata": {"postmortem_scheduled": "2024-01-17T10:00:00Z"}
},
{
"timestamp": "2024-01-15T16:50:00Z",
"type": "communication",
"actor": "Bob Kim",
"description": "Status page updated: Resolved - The issue has been resolved. Payment processing is operating normally.",
"metadata": {"channel": "status_page", "status": "resolved"}
}
],
"communications": [
{
"timestamp": "2024-01-15T14:30:00Z",
"channel": "status_page",
"audience": "external",
"message": "Investigating - We are investigating increased error rates affecting payment processing. Some transactions may fail. We will provide an update within 15 minutes."
},
{
"timestamp": "2024-01-15T14:35:00Z",
"channel": "slack_exec",
"audience": "internal",
"message": "SEV1 ACTIVE: Payment service degradation. ~12,500 users affected. Failed transactions accumulating. IC: Jane Smith. Bridge: [link]. ETA for mitigation: investigating."
},
{
"timestamp": "2024-01-15T14:45:00Z",
"channel": "status_page",
"audience": "external",
"message": "Identified - The issue has been identified as a database configuration problem following a recent deployment. We are implementing a fix. Next update in 15 minutes."
},
{
"timestamp": "2024-01-15T15:20:00Z",
"channel": "status_page",
"audience": "external",
"message": "Monitoring - A fix has been implemented and we are monitoring the results. Payment processing is recovering. We will provide a final update once we confirm stability."
},
{
"timestamp": "2024-01-15T16:50:00Z",
"channel": "status_page",
"audience": "external",
"message": "Resolved - The issue affecting payment processing has been resolved. All systems are operating normally. We will publish a full incident report within 48 hours."
}
],
"impact": {
"revenue_impact": "high",
"affected_users_percentage": 45,
"affected_regions": ["us-east-1", "eu-west-1"],
"data_integrity_risk": false,
"security_breach": false,
"customer_facing": true,
"degradation_type": "partial",
"workaround_available": false
},
"signals": {
"error_rate_percentage": 23.5,
"latency_p99_ms": 8500,
"affected_endpoints": ["/api/payments", "/api/checkout", "/api/subscriptions"],
"dependent_services": ["checkout", "subscription-billing", "order-service"],
"alert_count": 12,
"customer_reports": 8
},
"context": {
"recent_deployments": [
{
"service": "user-api",
"deployed_at": "2024-01-15T13:45:00Z",
"version": "2.4.1",
"changes": "Upgraded ORM from 3.1.2 to 3.2.0"
}
],
"ongoing_incidents": [],
"maintenance_windows": [],
"on_call": {
"primary": "alice@company.com",
"secondary": "bob@company.com",
"escalation_manager": "director-eng@company.com"
}
},
"resolution": {
"root_cause": "Database connection pool exhaustion caused by ORM 3.2.0 opening 3x more connections per request than previous version 3.1.2, exceeding the pool size of 20",
"contributing_factors": [
"Insufficient load testing of new ORM version under production-scale connection patterns",
"Connection pool monitoring alert threshold set too high (90%) with no warning at 70%",
"No canary deployment process for database configuration or ORM changes",
"Missing connection pool sizing documentation for service dependencies"
],
"mitigation_steps": [
"Increased connection pool size from 20 to 50 as temporary relief",
"Rolled back user-api from v2.4.1 (ORM 3.2.0) to v2.3.9 (ORM 3.1.2)"
],
"permanent_fix": "Load test ORM 3.2.0 with production connection patterns, update pool sizing, implement canary deployment for ORM changes",
"customer_impact": {
"affected_users": 12500,
"failed_transactions": 342,
"revenue_impact_usd": 28500,
"data_loss": false
}
},
"action_items": [
{
"title": "Add connection pool utilization alerting at 70% warning and 85% critical thresholds",
"owner": "alice@company.com",
"priority": "P1",
"deadline": "2024-01-22",
"type": "detection",
"status": "open"
},
{
"title": "Implement canary deployment pipeline for database configuration and ORM changes",
"owner": "bob@company.com",
"priority": "P1",
"deadline": "2024-02-01",
"type": "prevention",
"status": "open"
},
{
"title": "Load test ORM v3.2.0 with production-scale connection patterns before re-deployment",
"owner": "carol@company.com",
"priority": "P2",
"deadline": "2024-01-29",
"type": "prevention",
"status": "open"
},
{
"title": "Document connection pool sizing requirements for all services in runbook",
"owner": "alice@company.com",
"priority": "P2",
"deadline": "2024-02-05",
"type": "process",
"status": "open"
},
{
"title": "Add ORM connection behavior to integration test suite",
"owner": "carol@company.com",
"priority": "P3",
"deadline": "2024-02-15",
"type": "prevention",
"status": "open"
}
],
"participants": [
{"name": "Jane Smith", "role": "Incident Commander"},
{"name": "Alice Chen", "role": "Operations Lead"},
{"name": "Bob Kim", "role": "Communications Lead"},
{"name": "Carol Davis", "role": "Database SME"}
]
}
FILE:assets/sample_incident_pir_data.json
{
"incident_id": "INC-2024-0315-001",
"title": "Payment API Database Connection Pool Exhaustion",
"description": "Database connection pool exhaustion caused widespread 500 errors in payment processing API, preventing users from completing purchases. Root cause was an inefficient database query introduced in deployment v2.3.1.",
"severity": "sev2",
"start_time": "2024-03-15T14:30:00Z",
"end_time": "2024-03-15T15:35:00Z",
"duration": "1h 5m",
"affected_services": ["payment-api", "checkout-service", "subscription-billing"],
"customer_impact": "80% of users unable to complete payments or checkout. Approximately 2,400 failed payment attempts during the incident. Users experienced immediate 500 errors when attempting to pay.",
"business_impact": "Estimated revenue loss of $45,000 during outage period. No SLA breaches as resolution was within 2-hour window. 12 customer escalations through support channels.",
"incident_commander": "Mike Rodriguez",
"responders": [
"Sarah Chen - On-call Engineer, Primary Responder",
"Tom Wilson - Database Team Lead",
"Lisa Park - Database Engineer",
"Mike Rodriguez - Incident Commander",
"David Kumar - DevOps Engineer"
],
"status": "resolved",
"detection_details": {
"detection_method": "automated_monitoring",
"detection_time": "2024-03-15T14:30:00Z",
"alert_source": "Datadog error rate threshold",
"time_to_detection": "immediate"
},
"response_details": {
"time_to_response": "5 minutes",
"time_to_escalation": "10 minutes",
"time_to_resolution": "65 minutes",
"war_room_established": "2024-03-15T14:45:00Z",
"executives_notified": false,
"status_page_updated": true
},
"technical_details": {
"root_cause": "Inefficient database query introduced in deployment v2.3.1 caused each payment validation to take 15 seconds instead of normal 0.1 seconds, exhausting the 200-connection database pool",
"affected_regions": ["us-west", "us-east", "eu-west"],
"error_metrics": {
"peak_error_rate": "45%",
"normal_error_rate": "0.1%",
"connection_pool_max": 200,
"connections_exhausted_at": "100%"
},
"resolution_method": "rollback",
"rollback_target": "v2.2.9",
"rollback_duration": "7 minutes"
},
"communication_log": [
{
"timestamp": "2024-03-15T14:50:00Z",
"type": "status_page",
"message": "Investigating payment processing issues",
"audience": "customers"
},
{
"timestamp": "2024-03-15T15:35:00Z",
"type": "status_page",
"message": "Payment processing issues resolved",
"audience": "customers"
}
],
"lessons_learned_preview": [
"Deployment v2.3.1 code review missed performance implications of query change",
"Load testing didn't include realistic database query patterns",
"Connection pool monitoring could have provided earlier warning",
"Rollback procedure worked effectively - 7 minute rollback time"
],
"preliminary_action_items": [
"Fix inefficient query for v2.3.2 deployment",
"Add database query performance checks to CI pipeline",
"Improve load testing to include database performance scenarios",
"Add connection pool utilization alerts"
]
}
FILE:assets/sample_timeline_events.json
[
{
"timestamp": "2024-03-15T14:30:00Z",
"source": "datadog",
"type": "alert",
"message": "High error rate detected on payment-api: 45% error rate (threshold: 5%)",
"severity": "critical",
"actor": "monitoring-system",
"metadata": {
"alert_id": "ALT-001",
"metric_value": "45%",
"threshold": "5%"
}
},
{
"timestamp": "2024-03-15T14:32:00Z",
"source": "pagerduty",
"type": "escalation",
"message": "Paged on-call engineer Sarah Chen for payment-api alerts",
"severity": "high",
"actor": "pagerduty-system",
"metadata": {
"incident_id": "PD-12345",
"responder": "sarah.chen@company.com"
}
},
{
"timestamp": "2024-03-15T14:35:00Z",
"source": "slack",
"type": "communication",
"message": "Sarah Chen acknowledged the alert and is investigating payment-api issues",
"severity": "medium",
"actor": "sarah.chen",
"metadata": {
"channel": "#incidents",
"message_id": "1234567890.123456"
}
},
{
"timestamp": "2024-03-15T14:38:00Z",
"source": "application_logs",
"type": "log",
"message": "Database connection pool exhausted: 200/200 connections active, unable to acquire new connections",
"severity": "critical",
"actor": "payment-api",
"metadata": {
"log_level": "ERROR",
"component": "database_pool",
"connection_count": 200,
"max_connections": 200
}
},
{
"timestamp": "2024-03-15T14:40:00Z",
"source": "slack",
"type": "escalation",
"message": "Sarah Chen: Escalating to incident commander - database connection pool exhausted, need database team",
"severity": "high",
"actor": "sarah.chen",
"metadata": {
"channel": "#incidents",
"escalation_reason": "database_expertise_needed"
}
},
{
"timestamp": "2024-03-15T14:42:00Z",
"source": "pagerduty",
"type": "escalation",
"message": "Incident commander Mike Rodriguez assigned to incident PD-12345",
"severity": "high",
"actor": "pagerduty-system",
"metadata": {
"incident_commander": "mike.rodriguez@company.com",
"role": "incident_commander"
}
},
{
"timestamp": "2024-03-15T14:45:00Z",
"source": "slack",
"type": "communication",
"message": "Mike Rodriguez: War room established in #war-room-payment-api. Engaging database team.",
"severity": "high",
"actor": "mike.rodriguez",
"metadata": {
"channel": "#incidents",
"war_room": "#war-room-payment-api"
}
},
{
"timestamp": "2024-03-15T14:47:00Z",
"source": "pagerduty",
"type": "escalation",
"message": "Database team engineers paged: Tom Wilson, Lisa Park",
"severity": "medium",
"actor": "pagerduty-system",
"metadata": {
"team": "database-team",
"responders": ["tom.wilson@company.com", "lisa.park@company.com"]
}
},
{
"timestamp": "2024-03-15T14:50:00Z",
"source": "statuspage",
"type": "communication",
"message": "Status page updated: Investigating payment processing issues",
"severity": "medium",
"actor": "mike.rodriguez",
"metadata": {
"status": "investigating",
"affected_systems": ["payment-api"]
}
},
{
"timestamp": "2024-03-15T14:52:00Z",
"source": "slack",
"type": "communication",
"message": "Tom Wilson: Joining war room. Looking at database metrics now. Seeing unusual query patterns from recent deployment.",
"severity": "medium",
"actor": "tom.wilson",
"metadata": {
"channel": "#war-room-payment-api",
"investigation_focus": "database_metrics"
}
},
{
"timestamp": "2024-03-15T14:55:00Z",
"source": "database_monitoring",
"type": "log",
"message": "Identified slow query introduced in deployment v2.3.1: payment validation taking 15s per request",
"severity": "critical",
"actor": "database-monitor",
"metadata": {
"deployment_version": "v2.3.1",
"query_time": "15s",
"normal_query_time": "0.1s"
}
},
{
"timestamp": "2024-03-15T15:00:00Z",
"source": "slack",
"type": "communication",
"message": "Tom Wilson: Root cause identified - inefficient query in v2.3.1 deployment. Recommending immediate rollback.",
"severity": "high",
"actor": "tom.wilson",
"metadata": {
"channel": "#war-room-payment-api",
"root_cause": "inefficient_query",
"recommendation": "rollback"
}
},
{
"timestamp": "2024-03-15T15:02:00Z",
"source": "slack",
"type": "communication",
"message": "Mike Rodriguez: Approved rollback to v2.2.9. Sarah initiating rollback procedure.",
"severity": "high",
"actor": "mike.rodriguez",
"metadata": {
"channel": "#war-room-payment-api",
"decision": "rollback_approved",
"target_version": "v2.2.9"
}
},
{
"timestamp": "2024-03-15T15:05:00Z",
"source": "deployment_system",
"type": "action",
"message": "Rollback initiated: payment-api v2.3.1 → v2.2.9",
"severity": "medium",
"actor": "sarah.chen",
"metadata": {
"from_version": "v2.3.1",
"to_version": "v2.2.9",
"deployment_type": "rollback"
}
},
{
"timestamp": "2024-03-15T15:12:00Z",
"source": "deployment_system",
"type": "action",
"message": "Rollback completed successfully: payment-api now running v2.2.9 across all regions",
"severity": "medium",
"actor": "deployment-system",
"metadata": {
"deployment_status": "completed",
"regions": ["us-west", "us-east", "eu-west"]
}
},
{
"timestamp": "2024-03-15T15:15:00Z",
"source": "datadog",
"type": "log",
"message": "Error rate decreasing: payment-api error rate dropped to 8% and continuing to decline",
"severity": "medium",
"actor": "monitoring-system",
"metadata": {
"error_rate": "8%",
"trend": "decreasing"
}
},
{
"timestamp": "2024-03-15T15:18:00Z",
"source": "database_monitoring",
"type": "log",
"message": "Connection pool utilization normalizing: 45/200 connections active",
"severity": "low",
"actor": "database-monitor",
"metadata": {
"connection_count": 45,
"max_connections": 200,
"utilization": "22.5%"
}
},
{
"timestamp": "2024-03-15T15:25:00Z",
"source": "datadog",
"type": "log",
"message": "Error rate returned to normal: payment-api error rate now 0.2% (within normal range)",
"severity": "low",
"actor": "monitoring-system",
"metadata": {
"error_rate": "0.2%",
"status": "normal"
}
},
{
"timestamp": "2024-03-15T15:30:00Z",
"source": "slack",
"type": "communication",
"message": "Mike Rodriguez: All metrics returned to normal. Declaring incident resolved. Thanks to all responders.",
"severity": "low",
"actor": "mike.rodriguez",
"metadata": {
"channel": "#war-room-payment-api",
"status": "resolved"
}
},
{
"timestamp": "2024-03-15T15:35:00Z",
"source": "statuspage",
"type": "communication",
"message": "Status page updated: Payment processing issues resolved. All systems operational.",
"severity": "low",
"actor": "mike.rodriguez",
"metadata": {
"status": "resolved",
"duration": "65 minutes"
}
},
{
"timestamp": "2024-03-15T15:40:00Z",
"source": "slack",
"type": "communication",
"message": "Mike Rodriguez: PIR scheduled for tomorrow 10am. Action item: fix the inefficient query in v2.3.2",
"severity": "low",
"actor": "mike.rodriguez",
"metadata": {
"channel": "#incidents",
"pir_time": "2024-03-16T10:00:00Z",
"action_item": "fix_query_v2.3.2"
}
}
]
FILE:assets/simple_incident.json
{
"description": "Users reporting slow page loads on the main website",
"service": "web-frontend",
"affected_users": "25%",
"business_impact": "medium"
}
FILE:assets/simple_timeline_events.json
[
{
"timestamp": "2024-03-10T09:00:00Z",
"source": "monitoring",
"message": "High CPU utilization detected on web servers",
"severity": "medium",
"actor": "system"
},
{
"timestamp": "2024-03-10T09:05:00Z",
"source": "slack",
"message": "Engineer investigating high CPU alerts",
"severity": "medium",
"actor": "john.doe"
},
{
"timestamp": "2024-03-10T09:15:00Z",
"source": "deployment",
"message": "Deployed hotfix to reduce CPU usage",
"severity": "low",
"actor": "john.doe"
},
{
"timestamp": "2024-03-10T09:25:00Z",
"source": "monitoring",
"message": "CPU utilization returned to normal levels",
"severity": "low",
"actor": "system"
}
]
FILE:expected_outputs/incident_classification_text_output.txt
============================================================
INCIDENT CLASSIFICATION REPORT
============================================================
CLASSIFICATION:
Severity: SEV1
Confidence: 100.0%
Reasoning: Classified as SEV1 based on: keywords: timeout, 500 error; user impact: 80%
Timestamp: 2026-02-16T12:41:46.644096+00:00
RECOMMENDED RESPONSE:
Primary Team: Analytics Team
Supporting Teams: SRE, API Team, Backend Engineering, Finance Engineering, Payments Team, DevOps, Compliance Team, Database Team, Platform Team, Data Engineering
Response Time: 5 minutes
INITIAL ACTIONS:
1. Establish incident command (Priority 1)
Timeout: 5 minutes
Page incident commander and establish war room
2. Create incident ticket (Priority 1)
Timeout: 2 minutes
Create tracking ticket with all known details
3. Update status page (Priority 2)
Timeout: 15 minutes
Post initial status page update acknowledging incident
4. Notify executives (Priority 2)
Timeout: 15 minutes
Alert executive team of customer-impacting outage
5. Engage subject matter experts (Priority 3)
Timeout: 10 minutes
Page relevant SMEs based on affected systems
COMMUNICATION:
Subject: 🚨 [SEV1] payment-api - Database connection timeouts causing 500 errors fo...
Urgency: SEV1
Recipients: on-call, engineering-leadership, executives, customer-success
Channels: pager, phone, slack, email, status-page
Update Frequency: Every 15 minutes
============================================================
FILE:expected_outputs/pir_markdown_output.md
# Post-Incident Review: Payment API Database Connection Pool Exhaustion
## Executive Summary
On March 15, 2024, we experienced a sev2 incident affecting ['payment-api', 'checkout-service', 'subscription-billing']. The incident lasted 1h 5m and had the following impact: 80% of users unable to complete payments or checkout. Approximately 2,400 failed payment attempts during the incident. Users experienced immediate 500 errors when attempting to pay. The incident has been resolved and we have identified specific actions to prevent recurrence.
## Incident Overview
- **Incident ID:** INC-2024-0315-001
- **Date & Time:** 2024-03-15 14:30:00 UTC
- **Duration:** 1h 5m
- **Severity:** SEV2
- **Status:** Resolved
- **Incident Commander:** Mike Rodriguez
- **Responders:** Sarah Chen - On-call Engineer, Primary Responder, Tom Wilson - Database Team Lead, Lisa Park - Database Engineer, Mike Rodriguez - Incident Commander, David Kumar - DevOps Engineer
### Customer Impact
80% of users unable to complete payments or checkout. Approximately 2,400 failed payment attempts during the incident. Users experienced immediate 500 errors when attempting to pay.
### Business Impact
Estimated revenue loss of $45,000 during outage period. No SLA breaches as resolution was within 2-hour window. 12 customer escalations through support channels.
## Timeline
No detailed timeline available.
## Root Cause Analysis
### Analysis Method: 5 Whys Analysis
#### Why Analysis
**Why 1:** Why did Database connection pool exhaustion caused widespread 500 errors in payment processing API, preventing users from completing purchases. Root cause was an inefficient database query introduced in deployment v2.3.1.?
**Answer:** New deployment introduced a regression
**Why 2:** Why wasn't this detected earlier?
**Answer:** Code review process missed the issue
**Why 3:** Why didn't existing safeguards prevent this?
**Answer:** Testing environment didn't match production
**Why 4:** Why wasn't there a backup mechanism?
**Answer:** Further investigation needed
**Why 5:** Why wasn't this scenario anticipated?
**Answer:** Further investigation needed
## What Went Well
- The incident was successfully resolved
- Incident command was established
- Multiple team members collaborated on resolution
## What Didn't Go Well
- Analysis in progress
## Lessons Learned
Lessons learned to be documented following detailed analysis.
## Action Items
Action items to be defined.
## Follow-up and Prevention
### Prevention Measures
Based on the root cause analysis, the following preventive measures have been identified:
- Implement comprehensive testing for similar scenarios
- Improve monitoring and alerting coverage
- Enhance error handling and resilience patterns
### Follow-up Schedule
- 1 week: Review action item progress
- 1 month: Evaluate effectiveness of implemented changes
- 3 months: Conduct follow-up assessment and update preventive measures
## Appendix
### Additional Information
- Incident ID: INC-2024-0315-001
- Severity Classification: sev2
- Affected Services: payment-api, checkout-service, subscription-billing
### References
- Incident tracking ticket: [Link TBD]
- Monitoring dashboards: [Link TBD]
- Communication thread: [Link TBD]
---
*Generated on 2026-02-16 by PIR Generator*
FILE:expected_outputs/simple_incident_classification.txt
============================================================
INCIDENT CLASSIFICATION REPORT
============================================================
CLASSIFICATION:
Severity: SEV2
Confidence: 100.0%
Reasoning: Classified as SEV2 based on: keywords: slow; user impact: 25%
Timestamp: 2026-02-16T12:42:41.889774+00:00
RECOMMENDED RESPONSE:
Primary Team: UX Engineering
Supporting Teams: Product Engineering, Frontend Team
Response Time: 15 minutes
INITIAL ACTIONS:
1. Assign incident commander (Priority 1)
Timeout: 30 minutes
Assign IC and establish coordination channel
2. Create incident tracking (Priority 1)
Timeout: 5 minutes
Create incident ticket with details and timeline
3. Assess customer impact (Priority 2)
Timeout: 15 minutes
Determine scope and severity of user impact
4. Engage response team (Priority 2)
Timeout: 30 minutes
Page appropriate technical responders
5. Begin investigation (Priority 3)
Timeout: 15 minutes
Start technical analysis and debugging
COMMUNICATION:
Subject: ⚠️ [SEV2] web-frontend - Users reporting slow page loads on the main websit...
Urgency: SEV2
Recipients: on-call, engineering-leadership, product-team
Channels: pager, slack, email
Update Frequency: Every 30 minutes
============================================================
FILE:expected_outputs/timeline_reconstruction_text_output.txt
================================================================================
INCIDENT TIMELINE RECONSTRUCTION
================================================================================
OVERVIEW:
Time Range: 2024-03-15T14:30:00+00:00 to 2024-03-15T15:40:00+00:00
Total Duration: 70 minutes
Total Events: 21
Phases Detected: 12
PHASES:
DETECTION:
Start: 2024-03-15T14:30:00+00:00
Duration: 0.0 minutes
Events: 1
Description: Initial detection of the incident through monitoring or observation
ESCALATION:
Start: 2024-03-15T14:32:00+00:00
Duration: 0.0 minutes
Events: 1
Description: Escalation to additional resources or higher severity response
TRIAGE:
Start: 2024-03-15T14:35:00+00:00
Duration: 0.0 minutes
Events: 1
Description: Assessment and initial investigation of the incident
ESCALATION:
Start: 2024-03-15T14:38:00+00:00
Duration: 9.0 minutes
Events: 5
Description: Escalation to additional resources or higher severity response
TRIAGE:
Start: 2024-03-15T14:50:00+00:00
Duration: 0.0 minutes
Events: 1
Description: Assessment and initial investigation of the incident
ESCALATION:
Start: 2024-03-15T14:52:00+00:00
Duration: 10.0 minutes
Events: 4
Description: Escalation to additional resources or higher severity response
TRIAGE:
Start: 2024-03-15T15:05:00+00:00
Duration: 7.0 minutes
Events: 2
Description: Assessment and initial investigation of the incident
DETECTION:
Start: 2024-03-15T15:15:00+00:00
Duration: 0.0 minutes
Events: 1
Description: Initial detection of the incident through monitoring or observation
RESOLUTION:
Start: 2024-03-15T15:18:00+00:00
Duration: 0.0 minutes
Events: 1
Description: Confirmation that the incident has been resolved
DETECTION:
Start: 2024-03-15T15:25:00+00:00
Duration: 0.0 minutes
Events: 1
Description: Initial detection of the incident through monitoring or observation
RESOLUTION:
Start: 2024-03-15T15:30:00+00:00
Duration: 5.0 minutes
Events: 2
Description: Confirmation that the incident has been resolved
TRIAGE:
Start: 2024-03-15T15:40:00+00:00
Duration: 0.0 minutes
Events: 1
Description: Assessment and initial investigation of the incident
KEY METRICS:
Time to Mitigation: 0 minutes
Time to Resolution: 48.0 minutes
Events per Hour: 18.0
Unique Sources: 7
INCIDENT NARRATIVE:
Incident Timeline Summary:
The incident began at 2024-03-15 14:30:00 UTC and concluded at 2024-03-15 15:40:00 UTC, lasting approximately 70 minutes.
The incident progressed through 12 distinct phases: detection, escalation, triage, escalation, triage, escalation, triage, detection, resolution, detection, resolution, triage.
Key milestones:
- Detection: 14:30 (0 min)
- Escalation: 14:32 (0 min)
- Triage: 14:35 (0 min)
- Escalation: 14:38 (9 min)
- Triage: 14:50 (0 min)
- Escalation: 14:52 (10 min)
- Triage: 15:05 (7 min)
- Detection: 15:15 (0 min)
- Resolution: 15:18 (0 min)
- Detection: 15:25 (0 min)
- Resolution: 15:30 (5 min)
- Triage: 15:40 (0 min)
================================================================================
FILE:README.md
# Incident Commander Skill
A comprehensive incident response framework providing structured tools for managing technology incidents from detection through resolution and post-incident review.
## Overview
This skill implements battle-tested practices from SRE and DevOps teams at scale, providing:
- **Automated Severity Classification** - Intelligent incident triage
- **Timeline Reconstruction** - Transform scattered events into coherent narratives
- **Post-Incident Review Generation** - Structured PIRs with RCA frameworks
- **Communication Templates** - Pre-built stakeholder communication
- **Comprehensive Documentation** - Reference guides for incident response
## Quick Start
### Classify an Incident
```bash
# From JSON file
python scripts/incident_classifier.py --input incident.json --format text
# From stdin text
echo "Database is down affecting all users" | python scripts/incident_classifier.py --format text
# Interactive mode
python scripts/incident_classifier.py --interactive
```
### Reconstruct Timeline
```bash
# Analyze event timeline
python scripts/timeline_reconstructor.py --input events.json --format text
# With gap analysis
python scripts/timeline_reconstructor.py --input events.json --gap-analysis --format markdown
```
### Generate PIR Document
```bash
# Basic PIR
python scripts/pir_generator.py --incident incident.json --format markdown
# Comprehensive PIR with timeline
python scripts/pir_generator.py --incident incident.json --timeline timeline.json --rca-method fishbone
```
## Scripts
### incident_classifier.py
**Purpose:** Analyzes incident descriptions and provides severity classification, team recommendations, and response templates.
**Input:** JSON object with incident details or plain text description
**Output:** JSON + human-readable classification report
**Example Input:**
```json
{
"description": "Database connection timeouts causing 500 errors",
"service": "payment-api",
"affected_users": "80%",
"business_impact": "high"
}
```
**Key Features:**
- SEV1-4 severity classification
- Recommended response teams
- Initial action prioritization
- Communication templates
- Response timelines
### timeline_reconstructor.py
**Purpose:** Reconstructs incident timelines from timestamped events, identifies phases, and performs gap analysis.
**Input:** JSON array of timestamped events
**Output:** Formatted timeline with phase analysis and metrics
**Example Input:**
```json
[
{
"timestamp": "2024-01-01T12:00:00Z",
"source": "monitoring",
"message": "High error rate detected",
"severity": "critical",
"actor": "system"
}
]
```
**Key Features:**
- Phase detection (detection → triage → mitigation → resolution)
- Duration analysis
- Gap identification
- Communication effectiveness analysis
- Response metrics
### pir_generator.py
**Purpose:** Generates comprehensive Post-Incident Review documents with multiple RCA frameworks.
**Input:** Incident data JSON, optional timeline data
**Output:** Structured PIR document with RCA analysis
**Key Features:**
- Multiple RCA methods (5 Whys, Fishbone, Timeline, Bow Tie)
- Automated action item generation
- Lessons learned categorization
- Follow-up planning
- Completeness assessment
## Sample Data
The `assets/` directory contains sample data files for testing:
- `sample_incident_classification.json` - Database connection pool exhaustion incident
- `sample_timeline_events.json` - Complete timeline with 21 events across phases
- `sample_incident_pir_data.json` - Comprehensive incident data for PIR generation
- `simple_incident.json` - Minimal incident for basic testing
- `simple_timeline_events.json` - Simple 4-event timeline
## Expected Outputs
The `expected_outputs/` directory contains reference outputs showing what each script produces:
- `incident_classification_text_output.txt` - Detailed classification report
- `timeline_reconstruction_text_output.txt` - Complete timeline analysis
- `pir_markdown_output.md` - Full PIR document
- `simple_incident_classification.txt` - Basic classification example
## Reference Documentation
### references/incident_severity_matrix.md
Complete severity classification system with:
- SEV1-4 definitions and criteria
- Response requirements and timelines
- Escalation paths
- Communication requirements
- Decision trees and examples
### references/rca_frameworks_guide.md
Detailed guide for root cause analysis:
- 5 Whys methodology
- Fishbone (Ishikawa) diagram analysis
- Timeline analysis techniques
- Bow Tie analysis for high-risk incidents
- Framework selection guidelines
### references/communication_templates.md
Standardized communication templates:
- Severity-specific notification templates
- Stakeholder-specific messaging
- Escalation communications
- Resolution notifications
- Customer communication guidelines
## Usage Patterns
### End-to-End Incident Workflow
1. **Initial Classification**
```bash
echo "Payment API returning 500 errors for 70% of requests" | \
python scripts/incident_classifier.py --format text
```
2. **Timeline Reconstruction** (after collecting events)
```bash
python scripts/timeline_reconstructor.py \
--input events.json \
--gap-analysis \
--format markdown \
--output timeline.md
```
3. **PIR Generation** (after incident resolution)
```bash
python scripts/pir_generator.py \
--incident incident.json \
--timeline timeline.md \
--rca-method fishbone \
--output pir.md
```
### Integration Examples
**CI/CD Pipeline Integration:**
```bash
# Classify deployment issues
cat deployment_error.log | python scripts/incident_classifier.py --format json
```
**Monitoring Integration:**
```bash
# Process alert events
curl -s "monitoring-api/events" | python scripts/timeline_reconstructor.py --format text
```
**Runbook Generation:**
Use classification output to automatically select appropriate runbooks and escalation procedures.
## Quality Standards
- **Zero External Dependencies** - All scripts use only Python standard library
- **Dual Output Format** - Both JSON (machine-readable) and text (human-readable)
- **Robust Input Handling** - Graceful handling of missing or malformed data
- **Professional Defaults** - Opinionated, battle-tested configurations
- **Comprehensive Testing** - Sample data and expected outputs included
## Technical Requirements
- Python 3.6+
- No external dependencies required
- Works with standard Unix tools (pipes, redirection)
- Cross-platform compatible
## Severity Classification Reference
| Severity | Description | Response Time | Update Frequency |
|----------|-------------|---------------|------------------|
| **SEV1** | Complete outage | 5 minutes | Every 15 minutes |
| **SEV2** | Major degradation | 15 minutes | Every 30 minutes |
| **SEV3** | Minor impact | 2 hours | At milestones |
| **SEV4** | Low impact | 1-2 days | Weekly |
## Getting Help
Each script includes comprehensive help:
```bash
python scripts/incident_classifier.py --help
python scripts/timeline_reconstructor.py --help
python scripts/pir_generator.py --help
```
For methodology questions, refer to the reference documentation in the `references/` directory.
## Contributing
When adding new features:
1. Maintain zero external dependencies
2. Add comprehensive examples to `assets/`
3. Update expected outputs in `expected_outputs/`
4. Follow the established patterns for argument parsing and output formatting
## License
This skill is part of the claude-skills repository. See the main repository LICENSE for details.
FILE:references/communication_templates.md
# Incident Communication Templates
## Overview
This document provides standardized communication templates for incident response. These templates ensure consistent, clear communication across different severity levels and stakeholder groups.
## Template Usage Guidelines
### General Principles
1. **Be Clear and Concise** - Use simple language, avoid jargon
2. **Be Factual** - Only state what is known, avoid speculation
3. **Be Timely** - Send updates at committed intervals
4. **Be Actionable** - Include next steps and expected timelines
5. **Be Accountable** - Include contact information for follow-up
### Template Selection
- Choose templates based on incident severity and audience
- Customize templates with specific incident details
- Always include next update time and contact information
- Escalate template types as severity increases
---
## SEV1 Templates
### Initial Alert - Internal Teams
**Subject:** 🚨 [SEV1] CRITICAL: {Service} Complete Outage - Immediate Response Required
```
CRITICAL INCIDENT ALERT - IMMEDIATE ATTENTION REQUIRED
Incident Summary:
- Service: {Service Name}
- Status: Complete Outage
- Start Time: {Timestamp}
- Customer Impact: {Impact Description}
- Estimated Affected Users: {Number/Percentage}
Immediate Actions Needed:
✓ Incident Commander: {Name} - ASSIGNED
✓ War Room: {Bridge/Chat Link} - JOIN NOW
✓ On-Call Response: {Team} - PAGED
⏳ Executive Notification: In progress
⏳ Status Page Update: Within 15 minutes
Current Situation:
{Brief description of what we know}
What We're Doing:
{Immediate response actions being taken}
Next Update: {Timestamp - 15 minutes from now}
Incident Commander: {Name}
Contact: {Phone/Slack}
THIS IS A CUSTOMER-IMPACTING INCIDENT REQUIRING IMMEDIATE ATTENTION
```
### Executive Notification - SEV1
**Subject:** 🚨 URGENT: Customer-Impacting Outage - {Service}
```
EXECUTIVE ALERT: Critical customer-facing incident
Service: {Service Name}
Impact: {Customer impact description}
Duration: {Current duration} (started {start time})
Business Impact: {Revenue/SLA/compliance implications}
Customer Impact Summary:
- Affected Users: {Number/percentage}
- Revenue Impact: {$ amount if known}
- SLA Status: {Breach status}
- Customer Escalations: {Number if any}
Response Status:
- Incident Commander: {Name} ({contact})
- Response Team Size: {Number of engineers}
- Root Cause: {If known, otherwise "Under investigation"}
- ETA to Resolution: {If known, otherwise "Investigating"}
Executive Actions Required:
- [ ] Customer communication approval needed
- [ ] Legal/compliance notification: {If applicable}
- [ ] PR/Media response preparation: {If needed}
- [ ] Resource allocation decisions: {If escalation needed}
War Room: {Link}
Next Update: {15 minutes from now}
This incident meets SEV1 criteria and requires executive oversight.
{Incident Commander contact information}
```
### Customer Communication - SEV1
**Subject:** Service Disruption - Immediate Action Being Taken
```
We are currently experiencing a service disruption affecting {service description}.
What's Happening:
{Clear, customer-friendly description of the issue}
Impact:
{What customers are experiencing - be specific}
What We're Doing:
We detected this issue at {time} and immediately mobilized our engineering team. We are actively working to resolve this issue and will provide updates every 15 minutes.
Current Actions:
• {Action 1 - customer-friendly description}
• {Action 2 - customer-friendly description}
• {Action 3 - customer-friendly description}
Workaround:
{If available, provide clear steps}
{If not available: "We are working on alternative solutions and will share them as soon as available."}
Next Update: {Timestamp}
Status Page: {Link}
Support: {Contact information if different from usual}
We sincerely apologize for the inconvenience and are committed to resolving this as quickly as possible.
{Company Name} Team
```
### Status Page Update - SEV1
**Status:** Major Outage
```
{Timestamp} - Investigating
We are currently investigating reports of {service} being unavailable. Our team has been alerted and is actively investigating the cause.
Affected Services: {List of affected services}
Impact: {Customer-facing impact description}
We will provide an update within 15 minutes.
```
```
{Timestamp} - Identified
We have identified the cause of the {service} outage. Our engineering team is implementing a fix.
Root Cause: {Brief, customer-friendly explanation}
Expected Resolution: {Timeline if known}
Next update in 15 minutes.
```
```
{Timestamp} - Monitoring
The fix has been implemented and we are monitoring the service recovery.
Current Status: {Recovery progress}
Next Steps: {What we're monitoring}
We expect full service restoration within {timeframe}.
```
```
{Timestamp} - Resolved
{Service} is now fully operational. We have confirmed that all functionality is working as expected.
Total Duration: {Duration}
Root Cause: {Brief summary}
We apologize for the inconvenience. A full post-incident review will be conducted and shared within 24 hours.
```
---
## SEV2 Templates
### Team Notification - SEV2
**Subject:** ⚠️ [SEV2] {Service} Performance Issues - Response Team Mobilizing
```
SEV2 INCIDENT: Performance degradation requiring active response
Incident Details:
- Service: {Service Name}
- Issue: {Description of performance issue}
- Start Time: {Timestamp}
- Affected Users: {Percentage/description}
- Business Impact: {Impact on business operations}
Current Status:
{What we know about the issue}
Response Team:
- Incident Commander: {Name} ({contact})
- Primary Responder: {Name} ({team})
- Supporting Teams: {List of engaged teams}
Immediate Actions:
✓ {Action 1 - completed}
⏳ {Action 2 - in progress}
⏳ {Action 3 - next step}
Metrics:
- Error Rate: {Current vs normal}
- Response Time: {Current vs normal}
- Throughput: {Current vs normal}
Communication Plan:
- Internal Updates: Every 30 minutes
- Stakeholder Notification: {If needed}
- Status Page Update: {Planned/not needed}
Coordination Channel: {Slack channel}
Next Update: {30 minutes from now}
Incident Commander: {Name} | {Contact}
```
### Stakeholder Update - SEV2
**Subject:** [SEV2] Service Performance Update - {Service}
```
Service Performance Incident Update
Service: {Service Name}
Duration: {Current duration}
Impact: {Description of user impact}
Current Status:
{Brief status of the incident and response efforts}
What We Know:
• {Key finding 1}
• {Key finding 2}
• {Key finding 3}
What We're Doing:
• {Response action 1}
• {Response action 2}
• {Monitoring/verification steps}
Customer Impact:
{Realistic assessment of what users are experiencing}
Workaround:
{If available, provide steps}
Expected Resolution:
{Timeline if known, otherwise "Continuing investigation"}
Next Update: {30 minutes}
Contact: {Incident Commander information}
This incident is being actively managed and does not currently require escalation.
```
### Customer Communication - SEV2 (Optional)
**Subject:** Temporary Service Performance Issues
```
We are currently experiencing performance issues with {service name} that may affect your experience.
What You Might Notice:
{Specific symptoms users might experience}
What We're Doing:
Our team identified this issue at {time} and is actively working on a resolution. We expect to have this resolved within {timeframe}.
Workaround:
{If applicable, provide simple workaround steps}
We will update our status page at {link} with progress information.
Thank you for your patience as we work to resolve this issue quickly.
{Company Name} Support Team
```
---
## SEV3 Templates
### Team Assignment - SEV3
**Subject:** [SEV3] Issue Assignment - {Component} Issue
```
SEV3 Issue Assignment
Service/Component: {Affected component}
Issue: {Description}
Reported: {Timestamp}
Reporter: {Person/system that reported}
Issue Details:
{Detailed description of the problem}
Impact Assessment:
- Affected Users: {Scope}
- Business Impact: {Assessment}
- Urgency: {Business hours response appropriate}
Assignment:
- Primary: {Engineer name}
- Team: {Responsible team}
- Expected Response: {Within 2-4 hours}
Investigation Plan:
1. {Investigation step 1}
2. {Investigation step 2}
3. {Communication checkpoint}
Workaround:
{If known, otherwise "Investigating alternatives"}
This issue will be tracked in {ticket system} as {ticket number}.
Team Lead: {Name} | {Contact}
```
### Status Update - SEV3
**Subject:** [SEV3] Progress Update - {Component}
```
SEV3 Issue Progress Update
Issue: {Brief description}
Assigned to: {Engineer/Team}
Investigation Status: {Current progress}
Findings So Far:
{What has been discovered during investigation}
Next Steps:
{Planned actions and timeline}
Impact Update:
{Any changes to scope or urgency}
Expected Resolution:
{Timeline if known}
This issue continues to be tracked as SEV3 with no escalation required.
Contact: {Assigned engineer} | {Team lead}
```
---
## SEV4 Templates
### Issue Documentation - SEV4
**Subject:** [SEV4] Issue Documented - {Description}
```
SEV4 Issue Logged
Description: {Clear description of the issue}
Reporter: {Name/system}
Date: {Date reported}
Impact:
{Minimal impact description}
Priority Assessment:
This issue has been classified as SEV4 and will be addressed in the normal development cycle.
Assignment:
- Team: {Responsible team}
- Sprint: {Target sprint}
- Estimated Effort: {Story points/hours}
This issue is tracked as {ticket number} in {system}.
Product Owner: {Name}
```
---
## Escalation Templates
### Severity Escalation
**Subject:** ESCALATION: {Original Severity} → {New Severity} - {Service}
```
SEVERITY ESCALATION NOTIFICATION
Original Classification: {Original severity}
New Classification: {New severity}
Escalation Time: {Timestamp}
Escalated By: {Name and role}
Escalation Reasons:
• {Reason 1 - scope expansion/duration/impact}
• {Reason 2}
• {Reason 3}
Updated Impact:
{New assessment of customer/business impact}
Updated Response Requirements:
{New response team, communication frequency, etc.}
Previous Response Actions:
{Summary of actions taken under previous severity}
New Incident Commander: {If changed}
Updated Communication Plan: {New frequency/recipients}
All stakeholders should adjust response according to {new severity} protocols.
Incident Commander: {Name} | {Contact}
```
### Management Escalation
**Subject:** MANAGEMENT ESCALATION: Extended {Severity} Incident - {Service}
```
Management Escalation Required
Incident: {Service} {brief description}
Original Severity: {Severity}
Duration: {Current duration}
Escalation Trigger: {Duration threshold/scope change/customer escalation}
Current Status:
{Brief status of incident response}
Challenges Encountered:
• {Challenge 1}
• {Challenge 2}
• {Resource/expertise needs}
Business Impact:
{Updated assessment of business implications}
Management Decision Required:
• {Decision 1 - resource allocation/external expertise/communication}
• {Decision 2}
Recommended Actions:
{Incident Commander's recommendations}
This escalation follows standard procedures for {trigger type}.
Incident Commander: {Name}
Contact: {Phone/Slack}
War Room: {Link}
```
---
## Resolution Templates
### Resolution Confirmation - All Severities
**Subject:** RESOLVED: [{Severity}] {Service} Incident - {Brief Description}
```
INCIDENT RESOLVED
Service: {Service Name}
Issue: {Brief description}
Duration: {Total duration}
Resolution Time: {Timestamp}
Resolution Summary:
{Brief description of how the issue was resolved}
Root Cause:
{Brief explanation - detailed PIR to follow}
Impact Summary:
- Users Affected: {Final count/percentage}
- Business Impact: {Final assessment}
- Services Affected: {List}
Resolution Actions Taken:
• {Action 1}
• {Action 2}
• {Verification steps}
Monitoring:
We will continue monitoring {service} for {duration} to ensure stability.
Next Steps:
• Post-incident review scheduled for {date}
• Action items to be tracked in {system}
• Follow-up communication: {If needed}
Thank you to everyone who participated in the incident response.
Incident Commander: {Name}
```
### Customer Resolution Communication
**Subject:** Service Restored - Thank You for Your Patience
```
Service Update: Issue Resolved
We're pleased to report that the {service} issues have been fully resolved as of {timestamp}.
What Was Fixed:
{Customer-friendly explanation of the resolution}
Duration:
The issue lasted {duration} from {start time} to {end time}.
What We Learned:
{Brief, high-level takeaway}
Our Commitment:
We are conducting a thorough review of this incident and will implement improvements to prevent similar issues in the future. A summary of our findings and improvements will be shared {timeframe}.
We sincerely apologize for any inconvenience this may have caused and appreciate your patience while we worked to resolve the issue.
If you continue to experience any problems, please contact our support team at {contact information}.
Thank you,
{Company Name} Team
```
---
## Template Customization Guidelines
### Placeholders to Always Replace
- `{Service}` / `{Service Name}` - Specific service or component
- `{Timestamp}` - Specific date/time in consistent format
- `{Name}` / `{Contact}` - Actual names and contact information
- `{Duration}` - Actual time durations
- `{Link}` - Real URLs to war rooms, status pages, etc.
### Language Guidelines
- Use active voice ("We are investigating" not "The issue is being investigated")
- Be specific about timelines ("within 30 minutes" not "soon")
- Avoid technical jargon in customer communications
- Include empathy in customer-facing messages
- Use consistent terminology throughout incident lifecycle
### Timing Guidelines
| Severity | Initial Notification | Update Frequency | Resolution Notification |
|----------|---------------------|------------------|------------------------|
| SEV1 | Immediate (< 5 min) | Every 15 minutes | Immediate |
| SEV2 | Within 15 minutes | Every 30 minutes | Within 15 minutes |
| SEV3 | Within 2 hours | At milestones | Within 1 hour |
| SEV4 | Within 1 business day | Weekly | When resolved |
### Audience-Specific Considerations
#### Engineering Teams
- Include technical details
- Provide specific metrics and logs
- Include coordination channels
- List specific actions and owners
#### Executive/Business
- Focus on business impact
- Include customer and revenue implications
- Provide clear timeline and resource needs
- Highlight any external factors (PR, legal, compliance)
#### Customers
- Use plain language
- Focus on customer impact and workarounds
- Provide realistic timelines
- Include support contact information
- Show empathy and accountability
---
**Last Updated:** February 2026
**Next Review:** May 2026
**Owner:** Incident Management Team
FILE:references/incident-response-framework.md
# Incident Response Framework Reference
Production-grade incident management knowledge base synthesizing PagerDuty, Google SRE, and Atlassian methodologies into a unified, opinionated framework. This document is the source of truth for incident commanders operating under pressure.
---
## 1. Industry Framework Comparison
### PagerDuty Incident Response Model
PagerDuty's open-source incident response process defines four core roles and five process phases. The model prioritizes **speed of mobilization** over process perfection.
**Roles:**
- **Incident Commander (IC):** Owns the incident end-to-end. Does NOT perform technical investigation. Delegates, coordinates, and makes final escalation decisions. The IC is the single point of authority; conflicting opinions are resolved by the IC, not by committee.
- **Scribe:** Captures timestamped decisions, actions, and findings in the incident channel. The scribe never participates in technical work. A good scribe reduces postmortem preparation time by 70%.
- **Subject Matter Expert (SME):** Pulled in on-demand for specific subsystems. SMEs report findings to the IC, not to each other. Parallel SME investigations must be coordinated through the IC to avoid duplicated effort.
- **Customer Liaison:** Owns all outbound customer communication. Drafts status page updates for IC approval. Shields the technical team from inbound customer inquiries during active incidents.
**Process Phases:** Detect, Triage, Mobilize, Mitigate, Resolve, Postmortem.
**Communication Protocol:** PagerDuty mandates a dedicated Slack channel per incident, a bridge call for SEV1/SEV2, and status updates at fixed cadences (every 15 min for SEV1, every 30 min for SEV2). All decisions are announced in the channel, never in DMs or side threads.
### Google SRE: Managing Incidents (Chapter 14)
Google's SRE model, documented in *Site Reliability Engineering* (O'Reilly, 2016), emphasizes **role separation** and **clear handoffs** as the primary mechanisms for preventing incident chaos.
**Key Principles:**
- **Operational vs. Communication Tracks:** Google splits incident work into two parallel tracks. The operational track handles technical mitigation. The communication track handles stakeholder updates, executive briefings, and customer notifications. These tracks run independently with the IC bridging them.
- **Role Separation is Non-Negotiable:** The person debugging the system must never be the person updating stakeholders. Cognitive load from context-switching between technical work and communication degrades both outputs. Google measured a 40% increase in mean-time-to-resolution (MTTR) when a single person attempted both.
- **Clear Handoffs:** When an IC rotates out (recommended every 60-90 minutes for SEV1), the handoff includes: current status summary, active hypotheses, pending actions, and escalation state. Handoffs happen on the bridge call, not asynchronously.
- **Defined Command Post:** All communication flows through a single channel. Google uses the term "command post" -- a virtual or physical location where all incident participants converge.
### Atlassian Incident Management Model
Atlassian's model, published in their *Incident Management Handbook*, is **severity-driven** and **template-heavy**. It favors structured playbooks over improvisation.
**Key Characteristics:**
- **Severity Levels Drive Everything:** The assigned severity determines who gets paged, what communication templates are used, response time SLAs, and postmortem requirements. Severity is assigned at triage and reassessed every 30 minutes.
- **Handbook-Driven Approach:** Atlassian maintains runbooks for every known failure mode. During incidents, responders follow documented playbooks before improvising. This reduces MTTR for known issues by 50-60% but requires significant upfront investment in documentation.
- **Communication Templates:** Pre-written templates for status page updates, customer emails, and executive summaries. Templates include severity-specific language and are reviewed quarterly. This eliminates wordsmithing during active incidents.
- **Values-Based Decisions:** When runbooks do not cover the situation, Atlassian defaults to a decision hierarchy: (1) protect customer data, (2) restore service, (3) preserve evidence for root cause analysis.
### Framework Comparison Table
| Dimension | PagerDuty | Google SRE | Atlassian |
|-----------|-----------|------------|-----------|
| Primary strength | Speed of mobilization | Role separation discipline | Structured playbooks |
| IC authority model | IC has final say | IC coordinates, escalates to VP if blocked | IC follows handbook, escalates if off-script |
| Communication style | Dedicated channel + bridge | Command post with dual tracks | Template-driven status updates |
| Handoff protocol | Informal | Formal on-call handoff script | Rotation policy in handbook |
| Postmortem requirement | All SEV1/SEV2 | All incidents | SEV1/SEV2 mandatory, SEV3 optional |
| Best for | Fast-moving startups | Large-scale distributed systems | Regulated or process-heavy orgs |
| Weakness | Under-documented for edge cases | Heavyweight for small teams | Rigid, slow to adapt to novel failures |
### When to Use Which Framework
- **Teams under 20 engineers:** Start with PagerDuty's model. It is lightweight and prescriptive enough to work without heavy process investment. Add Atlassian-style runbooks as you identify recurring failure modes.
- **Teams running 50+ microservices:** Adopt Google SRE's dual-track model. The operational/communication split becomes critical when incidents span multiple teams and subsystems.
- **Regulated industries (finance, healthcare, government):** Use Atlassian's handbook-driven approach as the foundation. Regulatory auditors expect documented procedures, and templates satisfy compliance requirements for incident communication records.
- **Hybrid (recommended for most teams at scale):** Use PagerDuty's role definitions, Google's track separation, and Atlassian's template library. This is the approach codified in the rest of this document.
---
## 2. Severity Definitions
### Severity Classification Matrix
| Severity | Impact | Response Time | Update Cadence | Escalation Trigger | Example |
|----------|--------|---------------|----------------|---------------------|---------|
| **SEV1** | Total service outage or data breach affecting all users. Revenue loss exceeding $10K/hour. Security incident with active exfiltration. | Page IC + on-call within 5 min. All hands mobilized within 15 min. | Every 15 min to stakeholders. Continuous updates in incident channel. | Immediate executive notification. Board notification for data breaches. | Primary database cluster down. Payment processing system offline. Active ransomware attack. |
| **SEV2** | Major feature degraded for >30% of users. Revenue impact $1K-$10K/hour. Data integrity concerns without confirmed loss. | IC assigned within 15 min. Responders mobilized within 30 min. | Every 30 min to stakeholders. Every 15 min in incident channel. | Executive notification if unresolved after 1 hour. Upgrade to SEV1 if impact expands. | Search functionality returning errors for 40% of queries. Checkout flow failing intermittently. Authentication latency exceeding 10s. |
| **SEV3** | Minor feature degraded or non-critical service impaired. Workaround available. No direct revenue impact. | Acknowledged within 1 hour. Investigation started within 4 hours. | Every 2 hours to stakeholders if actively worked. Daily if deferred. | Escalate to SEV2 if workaround fails or user complaints exceed 50 in 1 hour. | Admin dashboard loading slowly. Email notifications delayed by 30+ minutes. Non-critical API endpoint returning 5xx for <5% of requests. |
| **SEV4** | Cosmetic issue, minor bug, or internal tooling degradation. No user-facing impact or negligible impact. | Acknowledged within 1 business day. Prioritized against backlog. | No scheduled updates. Tracked in issue tracker. | Escalate to SEV3 if internal productivity impact exceeds 2 hours/day across team. | Logging pipeline dropping non-critical debug logs. Internal metrics dashboard showing stale data. Minor UI alignment issue on one browser. |
### Customer-Facing Signals by Severity
**SEV1 Signals:** Support ticket volume spikes >500% of baseline within 15 minutes. Social media mentions of outage trend upward. Revenue dashboards show >95% drop in transaction volume. Multiple monitoring systems alarm simultaneously.
**SEV2 Signals:** Support ticket volume spikes 100-500% of baseline. Specific feature-related complaints cluster in support channels. Partial transaction failures visible in payment dashboards. Single monitoring system shows sustained alerting.
**SEV3 Signals:** Sporadic support tickets with a common pattern (under 20/hour). Users report intermittent issues with workarounds. Monitoring shows degraded but not critical metrics.
**SEV4 Signals:** Internal team notices issue during routine work. Occasional user mention with no pattern or urgency. Monitoring shows minor anomaly within acceptable thresholds.
### Severity Upgrade and Downgrade Criteria
**Upgrade from SEV2 to SEV1:** Impact expands to >80% of users, revenue impact confirmed above $10K/hour, data integrity compromise confirmed, or mitigation attempt fails after 45 minutes.
**Downgrade from SEV1 to SEV2:** Partial mitigation restores service for >70% of users, revenue impact drops below $10K/hour, and no ongoing data integrity concern.
**Downgrade from SEV2 to SEV3:** Workaround deployed and communicated, impact limited to <10% of users, and no revenue impact.
Severity changes must be announced by the IC in the incident channel with justification. The scribe logs the timestamp and rationale.
---
## 3. Role Definitions
### Incident Commander (IC)
The IC is the single decision-maker during an incident. This role exists to eliminate decision-by-committee, which adds 20-40 minutes to MTTR in measured studies.
**Responsibilities:**
- Assign severity level at triage (reassess every 30 minutes)
- Assign all other incident roles
- Approve status page updates before publication
- Make go/no-go decisions on mitigation strategies (rollback, feature flag, scaling)
- Decide when to escalate to executive leadership
- Declare incident resolved and initiate postmortem scheduling
**Decision Authority:** The IC can authorize rollbacks, page any team member regardless of org chart, approve customer communications, and override objections from individual contributors during active mitigation. The IC cannot approve financial expenditures above $50K or public press statements -- those require VP/C-level approval.
**What the IC Must NOT Do:** Debug code, write queries, SSH into production servers, or perform any hands-on technical work. The moment an IC starts debugging, incident coordination degrades. If the IC is the only person with domain expertise, they must hand off IC duties before engaging technically.
### Communications Lead
**Responsibilities:**
- Draft all status page updates using severity-appropriate templates
- Coordinate with Customer Liaison on outbound customer messaging
- Maintain the executive summary document (updated every 30 min for SEV1/SEV2)
- Manage the stakeholder notification list and delivery
- Post scheduled updates even when there is no new information ("We are continuing to investigate" is a valid update)
### Operations Lead
**Responsibilities:**
- Coordinate technical investigation across SMEs
- Maintain the running hypothesis list and assign investigation tasks
- Report technical findings to the IC in plain language
- Execute mitigation actions approved by the IC
- Track parallel workstreams and prevent duplicated effort
### Scribe
**Responsibilities:**
- Maintain a timestamped log of all decisions, actions, and findings
- Document who said what and when in the incident channel
- Capture rollback decisions, hypothesis changes, and escalation triggers
- Produce the initial postmortem timeline (saves 2-4 hours of postmortem prep)
### Subject Matter Experts (SMEs)
SMEs are paged on-demand by the IC for specific subsystems. They report findings to the Operations Lead, not directly to stakeholders. An SME who identifies a potential fix proposes it to the IC for approval before executing. SMEs are released from the incident explicitly by the IC when their subsystem is cleared.
### Customer Liaison
Owns the customer-facing voice during the incident. Monitors support channels for inbound customer reports. Drafts customer notification emails. Updates the public status page (after IC approval). Shields the technical team from direct customer inquiries during active mitigation.
---
## 4. Communication Protocols
### Incident Channel Naming Convention
Format: `#inc-YYYYMMDD-brief-desc`
Examples:
- `#inc-20260216-payment-api-timeout`
- `#inc-20260216-db-primary-failover`
- `#inc-20260216-auth-service-degraded`
Channel topic must include: severity, IC name, bridge call link, status page link.
Example topic: `SEV1 | IC: @jane.smith | Bridge: https://meet.example.com/inc-20260216 | Status: https://status.example.com`
### Internal Status Update Templates
**SEV1/SEV2 Update Template (posted in incident channel and executive Slack channel):**
```
INCIDENT UPDATE - [SEV1/SEV2] - [HH:MM UTC]
Status: [Investigating | Identified | Mitigating | Resolved]
Impact: [Specific user-facing impact in plain language]
Current Action: [What is actively being done right now]
Next Update: [HH:MM UTC]
IC: @[name]
```
**Executive Summary Template (for SEV1, updated every 30 min):**
```
EXECUTIVE SUMMARY - [Incident Title] - [HH:MM UTC]
Severity: SEV1
Duration: [X hours Y minutes]
Customer Impact: [Number of affected users/transactions]
Revenue Impact: [Estimated $ if known, "assessing" if not]
Current Status: [One sentence]
Mitigation ETA: [Estimated time or "unknown"]
Next Escalation Point: [What triggers executive action]
```
### Status Page Update Templates
**SEV1 Initial Post:**
```
Title: [Service Name] - Service Disruption
Body: We are currently experiencing a disruption affecting [service/feature].
Users may encounter [specific symptom: errors, timeouts, inability to access].
Our engineering team has been mobilized and is actively investigating.
We will provide an update within 15 minutes.
```
**SEV1 Update (mitigation in progress):**
```
Title: [Service Name] - Service Disruption (Update)
Body: We have identified the cause of the disruption affecting [service/feature]
and are implementing a fix. Some users may continue to experience [symptom].
We expect to have an update on resolution within [X] minutes.
```
**SEV1 Resolution:**
```
Title: [Service Name] - Resolved
Body: The disruption affecting [service/feature] has been resolved as of [HH:MM UTC].
Service has been restored to normal operation. Users should no longer experience
[symptom]. We will publish a full incident report within 48 hours.
We apologize for the inconvenience.
```
**SEV2 Initial Post:**
```
Title: [Service Name] - Degraded Performance
Body: We are investigating reports of degraded performance affecting [feature].
Some users may experience [specific symptom]. A workaround is [available/not yet available].
Our team is actively investigating and we will provide an update within 30 minutes.
```
### Bridge Call / War Room Etiquette
1. **Mute by default.** Unmute only when speaking to the IC or Operations Lead.
2. **Identify yourself before speaking.** "This is [name] from [team]." Every time.
3. **State findings, then recommendations.** "Database replication lag is 45 seconds and climbing. I recommend we fail over to the secondary cluster."
4. **IC confirms before action.** No unilateral action on production systems during an incident. The IC says "approved" or "hold" before anyone executes.
5. **No side conversations.** If two SMEs need to discuss a hypothesis, they take it to a breakout channel and report back findings to the main bridge.
6. **Time-box debugging.** The IC sets 15-minute timers for investigation threads. If a hypothesis is not confirmed or denied in 15 minutes, pivot to the next hypothesis or escalate.
### Customer Notification Templates
**SEV1 Customer Email (B2B, enterprise accounts):**
```
Subject: [Company Name] Service Incident - [Date]
Dear [Customer Name],
We are writing to inform you of a service incident affecting [product/service]
that began at [HH:MM UTC] on [date].
Impact: [Specific impact to this customer's usage]
Current Status: [Brief status]
Expected Resolution: [ETA if known, or "We are working to resolve this as quickly as possible"]
We will continue to provide updates every [15/30] minutes until resolution.
Your dedicated account team is available at [contact info] for any questions.
Sincerely,
[Name], [Title]
```
---
## 5. Escalation Matrix
### Escalation Tiers
**Tier 1 - Within Team (0-15 minutes):**
On-call engineer investigates. If the issue is within the team's domain and matches a known runbook, resolve without escalation. Page the IC if severity is SEV2 or higher, or if the issue is not resolved within 15 minutes.
**Tier 2 - Cross-Team (15-45 minutes):**
IC pages SMEs from adjacent teams. Common cross-team escalations: database team for replication issues, networking team for connectivity failures, security team for suspicious activity. Cross-team SMEs join the incident channel and bridge call.
**Tier 3 - Executive (45+ minutes or immediate for SEV1):**
VP of Engineering notified for all SEV1 incidents immediately. CTO notified if SEV1 exceeds 1 hour without mitigation progress. CEO notified if SEV1 involves data breach or regulatory implications. Executive involvement is for resource allocation and external communication decisions, not technical direction.
### Time-Based Escalation Triggers
| Elapsed Time | SEV1 Action | SEV2 Action |
|-------------|-------------|-------------|
| 0 min | Page IC + all on-call. Notify VP Eng. | Page IC + primary on-call. |
| 15 min | Confirm all roles staffed. Open bridge call. | IC assesses if additional SMEs needed. |
| 30 min | If no mitigation path identified, page backup on-call for all related services. | First stakeholder update. Reassess severity. |
| 45 min | Escalate to CTO if no progress. Consider customer notification. | If no progress, consider escalating to SEV1. |
| 60 min | CTO briefing. Initiate customer notification if not already done. | Notify VP Eng. Page cross-team SMEs. |
| 90 min | IC rotation (fresh IC takes over). Reassess all hypotheses. | IC rotation if needed. |
| 120 min | CEO briefing if data breach or regulatory risk. External PR team engaged. | Escalate to SEV1 if impact has not decreased. |
### Escalation Path Examples
**Database failover failure:**
On-call DBA (Tier 1, 0-15 min) -> IC + DBA team lead (Tier 2, 15 min) -> Infrastructure VP + cloud provider support (Tier 3, 45 min)
**Payment processing outage:**
On-call payments engineer (Tier 1, 0-5 min) -> IC + payments team lead + payment provider liaison (Tier 2, 5 min, immediate due to revenue impact) -> CFO + VP Eng (Tier 3, 15 min if provider-side issue confirmed)
**Security incident (suspected breach):**
Security on-call (Tier 1, 0-5 min) -> CISO + IC + legal counsel (Tier 2, immediate) -> CEO + external incident response firm (Tier 3, within 1 hour if breach confirmed)
### On-Call Rotation Best Practices
- **Primary + secondary on-call** for every critical service. Secondary is paged automatically if primary does not acknowledge within 5 minutes.
- **On-call shifts are 7 days maximum.** Longer rotations degrade alertness and response quality.
- **Handoff checklist:** Current open issues, recent deploys in the last 48 hours, known risks or maintenance windows, escalation contacts for dependent services.
- **On-call load budget:** No more than 2 pages per night on average, measured weekly. Exceeding this indicates systemic reliability issues that must be addressed with engineering investment, not heroic on-call effort.
---
## 6. Incident Lifecycle Phases
### Phase 1: Detection
Detection comes from three sources, in order of preference:
1. **Automated monitoring (preferred):** Alerting rules on latency (p99 > 2x baseline), error rates (5xx > 1% of requests), saturation (CPU > 85%, memory > 90%, disk > 80%), and business metrics (transaction volume drops > 20% from 15-minute rolling average). Alerts should fire within 60 seconds of threshold breach.
2. **Internal reports:** An engineer notices anomalous behavior during routine work. Internal detection typically adds 5-15 minutes to response time compared to automated monitoring.
3. **Customer reports:** Customers contact support about issues. This is the worst detection source. If customers detect incidents before monitoring, the monitoring coverage has a gap that must be closed in the postmortem.
**Detection SLA:** SEV1 incidents must be detected within 5 minutes of impact onset. If detection latency exceeds this, the postmortem must include a monitoring improvement action item.
### Phase 2: Triage
The first responder performs initial triage within 5 minutes of detection:
1. **Scope assessment:** How many users, services, or regions are affected? Check dashboards, not assumptions.
2. **Severity assignment:** Use the severity matrix in Section 2. When in doubt, assign higher severity. Downgrading is cheap; delayed escalation is expensive.
3. **IC assignment:** For SEV1/SEV2, page the on-call IC immediately. For SEV3, the first responder may self-assign IC duties.
4. **Initial hypothesis:** What changed in the last 2 hours? Check deploy logs, config changes, upstream dependency status, and traffic patterns. 70% of incidents correlate with a change deployed in the prior 2 hours.
### Phase 3: Mobilization
The IC executes mobilization within 10 minutes of assignment:
1. **Create incident channel:** `#inc-YYYYMMDD-brief-desc`. Set topic with severity, IC name, bridge link.
2. **Assign roles:** Communications Lead, Operations Lead, Scribe. For SEV3/SEV4, the IC may cover multiple roles.
3. **Open bridge call (SEV1/SEV2):** Share link in incident channel. All responders join within 5 minutes.
4. **Post initial summary:** Current understanding, affected services, assigned roles, first actions.
5. **Notify stakeholders:** Page dependent teams. Notify customer support leadership. For SEV1, notify executive chain per escalation matrix.
### Phase 4: Investigation
Investigation runs as parallel workstreams coordinated by the Operations Lead:
- **Workstream discipline:** Each SME investigates one hypothesis at a time. The Operations Lead tracks active hypotheses on a shared list. Completed investigations report: confirmed, denied, or inconclusive.
- **Hypothesis testing priority:** (1) Recent changes (deploys, configs, feature flags), (2) Upstream dependency failures, (3) Capacity exhaustion, (4) Data corruption, (5) Security compromise.
- **15-minute rule:** If a hypothesis is not confirmed or denied within 15 minutes, the IC decides whether to continue, pivot, or escalate. Unbounded investigation is the leading cause of extended MTTR.
- **Evidence collection:** Screenshots, log snippets, metric graphs, and query results are posted in the incident channel, not described verbally. The scribe tags evidence with timestamps.
### Phase 5: Mitigation
Mitigation prioritizes restoring service over finding root cause:
- **Rollback first:** If a deploy correlates with the incident, roll it back before investigating further. A 5-minute rollback beats a 45-minute investigation. Rollback authority rests with the IC.
- **Feature flags:** Disable the suspected feature via feature flag if available. This is faster and less risky than a full rollback.
- **Scaling:** If the issue is capacity-related, scale horizontally before investigating the traffic source.
- **Failover:** If a primary system is unrecoverable, fail over to the secondary. Test failover procedures quarterly so this is a routine, not a gamble.
- **Customer workaround:** If mitigation will take time, publish a workaround for customers (e.g., "Use the mobile app while we restore web access").
**Mitigation verification:** After applying mitigation, monitor key metrics for 15 minutes before declaring the issue mitigated. Premature declarations that the issue is mitigated followed by recurrence damage team credibility and customer trust.
### Phase 6: Resolution
Resolution is declared when the root cause is addressed and service is operating normally:
- **Verification checklist:** Error rates returned to baseline, latency returned to baseline, no ongoing customer reports, monitoring confirms stability for 30+ minutes.
- **Incident channel update:** IC posts final status with resolution summary, total duration, and next steps.
- **Status page update:** Post resolution notice within 15 minutes of declaring resolved.
- **Stand down:** IC explicitly releases all responders. SMEs return to normal work. Bridge call is closed.
### Phase 7: Postmortem
Postmortem is mandatory for SEV1 and SEV2. Optional but recommended for SEV3. Never conducted for SEV4.
- **Timeline:** Postmortem document drafted within 24 hours. Postmortem meeting held within 72 hours (3 business days). Action items assigned and tracked in the team's issue tracker.
- **Blameless standard:** The postmortem examines systems, processes, and tools -- not individual performance. "Why did the system allow this?" not "Why did [person] do this?"
- **Required sections:** Timeline (from scribe's log), root cause analysis (using 5 Whys or fault tree), impact summary (users, revenue, duration), what went well, what went poorly, action items with owners and due dates.
- **Action items and recurrence:** Every postmortem produces 3-7 concrete action items. Items without owners and due dates are not action items. Teams should close 80%+ within 30 days. If the same root cause appears in two postmortems within 6 months, escalate to engineering leadership as a systemic reliability investment area.
FILE:references/incident_severity_matrix.md
# Incident Severity Classification Matrix
## Overview
This document defines the severity classification system used for incident response. The classification determines response requirements, escalation paths, and communication frequency.
## Severity Levels
### SEV1 - Critical Outage
**Definition:** Complete service failure affecting all users or critical business functions
#### Impact Criteria
- Customer-facing services completely unavailable
- Data loss or corruption affecting users
- Security breaches with customer data exposure
- Revenue-generating systems down
- SLA violations with financial penalties
- > 75% of users affected
#### Response Requirements
| Metric | Requirement |
|--------|-------------|
| **Response Time** | Immediate (0-5 minutes) |
| **Incident Commander** | Assigned within 5 minutes |
| **War Room** | Established within 10 minutes |
| **Executive Notification** | Within 15 minutes |
| **Public Status Page** | Updated within 15 minutes |
| **Customer Communication** | Within 30 minutes |
#### Escalation Path
1. **Immediate**: On-call Engineer → Incident Commander
2. **15 minutes**: VP Engineering + Customer Success VP
3. **30 minutes**: CTO
4. **60 minutes**: CEO + Full Executive Team
#### Communication Requirements
- **Frequency**: Every 15 minutes until resolution
- **Channels**: PagerDuty, Phone, Slack, Email, Status Page
- **Recipients**: All engineering, executives, customer success
- **Template**: SEV1 Executive Alert Template
---
### SEV2 - Major Impact
**Definition:** Significant degradation affecting subset of users or non-critical functions
#### Impact Criteria
- Partial service degradation (25-75% of users affected)
- Performance issues causing user frustration
- Non-critical features unavailable
- Internal tools impacting productivity
- Data inconsistencies not affecting user experience
- API errors affecting integrations
#### Response Requirements
| Metric | Requirement |
|--------|-------------|
| **Response Time** | 15 minutes |
| **Incident Commander** | Assigned within 30 minutes |
| **Status Page Update** | Within 30 minutes |
| **Stakeholder Notification** | Within 1 hour |
| **Team Assembly** | Within 30 minutes |
#### Escalation Path
1. **Immediate**: On-call Engineer → Team Lead
2. **30 minutes**: Engineering Manager
3. **2 hours**: VP Engineering
4. **4 hours**: CTO (if unresolved)
#### Communication Requirements
- **Frequency**: Every 30 minutes during active response
- **Channels**: PagerDuty, Slack, Email
- **Recipients**: Engineering team, product team, relevant stakeholders
- **Template**: SEV2 Major Impact Template
---
### SEV3 - Minor Impact
**Definition:** Limited impact with workarounds available
#### Impact Criteria
- Single feature or component affected
- < 25% of users impacted
- Workarounds available
- Performance degradation not significantly impacting UX
- Non-urgent monitoring alerts
- Development/test environment issues
#### Response Requirements
| Metric | Requirement |
|--------|-------------|
| **Response Time** | 2 hours (business hours) |
| **After Hours Response** | Next business day |
| **Team Assignment** | Within 4 hours |
| **Status Page Update** | Optional |
| **Internal Notification** | Within 2 hours |
#### Escalation Path
1. **Immediate**: Assigned Engineer
2. **4 hours**: Team Lead
3. **1 business day**: Engineering Manager (if needed)
#### Communication Requirements
- **Frequency**: At key milestones only
- **Channels**: Slack, Email
- **Recipients**: Assigned team, team lead
- **Template**: SEV3 Minor Impact Template
---
### SEV4 - Low Impact
**Definition:** Minimal impact, cosmetic issues, or planned maintenance
#### Impact Criteria
- Cosmetic bugs
- Documentation issues
- Logging or monitoring gaps
- Performance issues with no user impact
- Development/test environment issues
- Feature requests or enhancements
#### Response Requirements
| Metric | Requirement |
|--------|-------------|
| **Response Time** | 1-2 business days |
| **Assignment** | Next sprint planning |
| **Tracking** | Standard ticket system |
| **Escalation** | None required |
#### Communication Requirements
- **Frequency**: Standard development cycle updates
- **Channels**: Ticket system
- **Recipients**: Product owner, assigned developer
- **Template**: Standard issue template
## Classification Guidelines
### User Impact Assessment
| Impact Scope | Description | Typical Severity |
|--------------|-------------|------------------|
| **All Users** | 100% of users affected | SEV1 |
| **Major Subset** | 50-75% of users affected | SEV1/SEV2 |
| **Significant Subset** | 25-50% of users affected | SEV2 |
| **Limited Users** | 5-25% of users affected | SEV2/SEV3 |
| **Few Users** | < 5% of users affected | SEV3/SEV4 |
| **No User Impact** | Internal only | SEV4 |
### Business Impact Assessment
| Business Impact | Description | Severity Boost |
|-----------------|-------------|----------------|
| **Revenue Loss** | Direct revenue impact | +1 severity level |
| **SLA Breach** | Contract violations | +1 severity level |
| **Regulatory** | Compliance implications | +1 severity level |
| **Brand Damage** | Public-facing issues | +1 severity level |
| **Security** | Data or system security | +2 severity levels |
### Duration Considerations
| Duration | Impact on Classification |
|----------|--------------------------|
| **< 15 minutes** | May reduce severity by 1 level |
| **15-60 minutes** | Standard classification |
| **1-4 hours** | May increase severity by 1 level |
| **> 4 hours** | Significant severity increase |
## Decision Tree
```
1. Is this a security incident with data exposure?
→ YES: SEV1 (regardless of user count)
→ NO: Continue to step 2
2. Are revenue-generating services completely down?
→ YES: SEV1
→ NO: Continue to step 3
3. What percentage of users are affected?
→ > 75%: SEV1
→ 25-75%: SEV2
→ 5-25%: SEV3
→ < 5%: SEV4
4. Apply business impact modifiers
5. Consider duration factors
6. When in doubt, err on higher severity
```
## Examples
### SEV1 Examples
- Payment processing system completely down
- All user authentication failing
- Database corruption causing data loss
- Security breach with customer data exposed
- Website returning 500 errors for all users
### SEV2 Examples
- Payment processing slow (30-second delays)
- Search functionality returning incomplete results
- API rate limits causing partner integration issues
- Dashboard displaying stale data (> 1 hour old)
- Mobile app crashing for 40% of users
### SEV3 Examples
- Single feature in admin panel not working
- Email notifications delayed by 1 hour
- Non-critical API endpoint returning errors
- Cosmetic UI bug in settings page
- Development environment deployment failing
### SEV4 Examples
- Typo in help documentation
- Log format change needed for analysis
- Non-critical performance optimization
- Internal tool enhancement request
- Test data cleanup needed
## Escalation Triggers
### Automatic Escalation
- SEV1 incidents automatically escalate every 30 minutes if unresolved
- SEV2 incidents escalate after 2 hours without significant progress
- Any incident with expanding scope increases severity
- Customer escalation to support triggers severity review
### Manual Escalation
- Incident Commander can escalate at any time
- Technical leads can request escalation
- Business stakeholders can request severity review
- External factors (media attention, regulatory) trigger escalation
## Communication Templates
### SEV1 Executive Alert
```
Subject: 🚨 CRITICAL INCIDENT - [Service] Complete Outage
URGENT: Customer-facing service outage requiring immediate attention
Service: [Service Name]
Start Time: [Timestamp]
Impact: [Description of customer impact]
Estimated Affected Users: [Number/Percentage]
Business Impact: [Revenue/SLA/Brand implications]
Incident Commander: [Name] ([Contact])
Response Team: [Team members engaged]
Current Status: [Brief status update]
Next Update: [Timestamp - 15 minutes from now]
War Room: [Bridge/Chat link]
This is a customer-impacting incident requiring executive awareness.
```
### SEV2 Major Impact
```
Subject: ⚠️ [SEV2] [Service] - Major Performance Impact
Major service degradation affecting user experience
Service: [Service Name]
Start Time: [Timestamp]
Impact: [Description of user impact]
Scope: [Affected functionality/users]
Response Team: [Team Lead] + [Team members]
Status: [Current mitigation efforts]
Workaround: [If available]
Next Update: 30 minutes
Status Page: [Link if updated]
```
## Review and Updates
This severity matrix should be reviewed quarterly and updated based on:
- Incident response learnings
- Business priority changes
- Service architecture evolution
- Regulatory requirement changes
- Customer feedback and SLA updates
**Last Updated:** February 2026
**Next Review:** May 2026
**Owner:** Engineering Leadership
FILE:references/rca_frameworks_guide.md
# Root Cause Analysis (RCA) Frameworks Guide
## Overview
This guide provides detailed instructions for applying various Root Cause Analysis frameworks during Post-Incident Reviews. Each framework offers a different perspective and approach to identifying underlying causes of incidents.
## Framework Selection Guidelines
| Incident Type | Recommended Framework | Why |
|---------------|----------------------|-----|
| **Process Failure** | 5 Whys | Simple, direct cause-effect chain |
| **Complex System Failure** | Fishbone + Timeline | Multiple contributing factors |
| **Human Error** | Fishbone | Systematic analysis of contributing factors |
| **Extended Incidents** | Timeline Analysis | Understanding decision points |
| **High-Risk Incidents** | Bow Tie | Comprehensive barrier analysis |
| **Recurring Issues** | 5 Whys + Fishbone | Deep dive into systemic issues |
---
## 5 Whys Analysis Framework
### Purpose
Iteratively drill down through cause-effect relationships to identify root causes.
### When to Use
- Simple, linear cause-effect chains
- Time-pressured analysis
- Process-related failures
- Individual component failures
### Process Steps
#### Step 1: Problem Statement
Write a clear, specific problem statement.
**Good Example:**
> "The payment API returned 500 errors for 2 hours on March 15, affecting 80% of checkout attempts."
**Poor Example:**
> "The system was broken."
#### Step 2: First Why
Ask why the problem occurred. Focus on immediate, observable causes.
**Example:**
- **Why 1:** Why did the payment API return 500 errors?
- **Answer:** The database connection pool was exhausted.
#### Step 3: Subsequent Whys
For each answer, ask "why" again. Continue until you reach a root cause.
**Example Chain:**
- **Why 2:** Why was the database connection pool exhausted?
- **Answer:** The application was creating more connections than usual.
- **Why 3:** Why was the application creating more connections?
- **Answer:** A new feature wasn't properly closing connections.
- **Why 4:** Why wasn't the feature properly closing connections?
- **Answer:** Code review missed the connection leak pattern.
- **Why 5:** Why did code review miss this pattern?
- **Answer:** We don't have automated checks for connection pooling best practices.
#### Step 4: Validation
Verify that addressing the root cause would prevent the original problem.
### Best Practices
1. **Ask at least 3 "whys"** - Surface causes are rarely root causes
2. **Focus on process failures, not people** - Avoid blame, focus on system improvements
3. **Use evidence** - Support each answer with data or observations
4. **Consider multiple paths** - Some problems have multiple root causes
5. **Test the logic** - Work backwards from root cause to problem
### Common Pitfalls
- **Stopping too early** - First few whys often reveal symptoms, not causes
- **Single-cause assumption** - Complex systems often have multiple contributing factors
- **Blame focus** - Focusing on individual mistakes rather than system failures
- **Vague answers** - Use specific, actionable answers
### 5 Whys Template
```markdown
## 5 Whys Analysis
**Problem Statement:** [Clear description of the incident]
**Why 1:** [First why question]
**Answer:** [Specific, evidence-based answer]
**Evidence:** [Supporting data, logs, observations]
**Why 2:** [Second why question]
**Answer:** [Specific answer based on Why 1]
**Evidence:** [Supporting evidence]
[Continue for 3-7 iterations]
**Root Cause(s) Identified:**
1. [Primary root cause]
2. [Secondary root cause if applicable]
**Validation:** [Confirm that addressing root causes would prevent recurrence]
```
---
## Fishbone (Ishikawa) Diagram Framework
### Purpose
Systematically analyze potential causes across multiple categories to identify contributing factors.
### When to Use
- Complex incidents with multiple potential causes
- When human factors are suspected
- Systemic or organizational issues
- When 5 Whys doesn't reveal clear root causes
### Categories
#### People (Human Factors)
- **Training and Skills**
- Insufficient training on new systems
- Lack of domain expertise
- Skill gaps in team
- Knowledge not shared across team
- **Communication**
- Poor communication between teams
- Unclear responsibilities
- Information not reaching right people
- Language/cultural barriers
- **Decision Making**
- Decisions made under pressure
- Insufficient information for decisions
- Risk assessment inadequate
- Approval processes bypassed
#### Process (Procedures and Workflows)
- **Documentation**
- Outdated procedures
- Missing runbooks
- Unclear instructions
- Process not documented
- **Change Management**
- Inadequate change review
- Rushed deployments
- Insufficient testing
- Rollback procedures unclear
- **Review and Approval**
- Code review gaps
- Architecture review skipped
- Security review insufficient
- Performance review missing
#### Technology (Systems and Tools)
- **Architecture**
- Single points of failure
- Insufficient redundancy
- Scalability limitations
- Tight coupling between systems
- **Monitoring and Alerting**
- Missing monitoring
- Alert fatigue
- Inadequate thresholds
- Poor alert routing
- **Tools and Automation**
- Manual processes prone to error
- Tool limitations
- Automation gaps
- Integration issues
#### Environment (External Factors)
- **Infrastructure**
- Hardware failures
- Network issues
- Capacity limitations
- Geographic dependencies
- **Dependencies**
- Third-party service failures
- External API changes
- Vendor issues
- Supply chain problems
- **External Pressure**
- Time pressure from business
- Resource constraints
- Regulatory changes
- Market conditions
### Process Steps
#### Step 1: Define the Problem
Place the incident at the "head" of the fishbone diagram.
#### Step 2: Brainstorm Causes
For each category, brainstorm potential contributing factors.
#### Step 3: Drill Down
For each factor, ask what caused that factor (sub-causes).
#### Step 4: Identify Primary Causes
Mark the most likely contributing factors based on evidence.
#### Step 5: Validate
Gather evidence to support or refute each suspected cause.
### Fishbone Template
```markdown
## Fishbone Analysis
**Problem:** [Incident description]
### People
**Training/Skills:**
- [Factor 1]: [Evidence/likelihood]
- [Factor 2]: [Evidence/likelihood]
**Communication:**
- [Factor 1]: [Evidence/likelihood]
**Decision Making:**
- [Factor 1]: [Evidence/likelihood]
### Process
**Documentation:**
- [Factor 1]: [Evidence/likelihood]
**Change Management:**
- [Factor 1]: [Evidence/likelihood]
**Review/Approval:**
- [Factor 1]: [Evidence/likelihood]
### Technology
**Architecture:**
- [Factor 1]: [Evidence/likelihood]
**Monitoring:**
- [Factor 1]: [Evidence/likelihood]
**Tools:**
- [Factor 1]: [Evidence/likelihood]
### Environment
**Infrastructure:**
- [Factor 1]: [Evidence/likelihood]
**Dependencies:**
- [Factor 1]: [Evidence/likelihood]
**External Factors:**
- [Factor 1]: [Evidence/likelihood]
### Primary Contributing Factors
1. [Factor with highest evidence/impact]
2. [Second most significant factor]
3. [Third most significant factor]
### Root Cause Hypothesis
[Synthesized explanation of how factors combined to cause incident]
```
---
## Timeline Analysis Framework
### Purpose
Analyze the chronological sequence of events to identify decision points, missed opportunities, and process gaps.
### When to Use
- Extended incidents (> 1 hour)
- Complex multi-phase incidents
- When response effectiveness is questioned
- Communication or coordination failures
### Analysis Dimensions
#### Detection Analysis
- **Time to Detection:** How long from onset to first alert?
- **Detection Method:** How was the incident first identified?
- **Alert Effectiveness:** Were the right people notified quickly?
- **False Negatives:** What signals were missed?
#### Response Analysis
- **Time to Response:** How long from detection to first response action?
- **Escalation Timing:** Were escalations timely and appropriate?
- **Resource Mobilization:** How quickly were the right people engaged?
- **Decision Points:** What key decisions were made and when?
#### Communication Analysis
- **Internal Communication:** How effective was team coordination?
- **External Communication:** Were stakeholders informed appropriately?
- **Communication Gaps:** Where did information flow break down?
- **Update Frequency:** Were updates provided at appropriate intervals?
#### Resolution Analysis
- **Mitigation Strategy:** Was the chosen approach optimal?
- **Alternative Paths:** What other options were considered?
- **Resource Allocation:** Were resources used effectively?
- **Verification:** How was resolution confirmed?
### Process Steps
#### Step 1: Event Reconstruction
Create comprehensive timeline with all available events.
#### Step 2: Phase Identification
Identify distinct phases (detection, triage, escalation, mitigation, resolution).
#### Step 3: Gap Analysis
Identify time gaps and analyze their causes.
#### Step 4: Decision Point Analysis
Examine key decision points and alternative paths.
#### Step 5: Effectiveness Assessment
Evaluate the overall effectiveness of the response.
### Timeline Template
```markdown
## Timeline Analysis
### Incident Phases
1. **Detection** ([start] - [end], [duration])
2. **Triage** ([start] - [end], [duration])
3. **Escalation** ([start] - [end], [duration])
4. **Mitigation** ([start] - [end], [duration])
5. **Resolution** ([start] - [end], [duration])
### Key Decision Points
**[Timestamp]:** [Decision made]
- **Context:** [Situation at time of decision]
- **Alternatives:** [Other options considered]
- **Outcome:** [Result of decision]
- **Assessment:** [Was this optimal?]
### Communication Timeline
**[Timestamp]:** [Communication event]
- **Channel:** [Slack/Email/Phone/etc.]
- **Audience:** [Who was informed]
- **Content:** [What was communicated]
- **Effectiveness:** [Assessment]
### Gaps and Delays
**[Time Period]:** [Description of gap]
- **Duration:** [Length of gap]
- **Cause:** [Why did gap occur]
- **Impact:** [Effect on incident response]
### Response Effectiveness
**Strengths:**
- [What went well]
- [Effective decisions/actions]
**Weaknesses:**
- [What could be improved]
- [Missed opportunities]
### Root Causes from Timeline
1. [Process-based root cause]
2. [Communication-based root cause]
3. [Decision-making root cause]
```
---
## Bow Tie Analysis Framework
### Purpose
Analyze both preventive measures (left side) and protective measures (right side) around an incident.
### When to Use
- High-severity incidents (SEV1)
- Security incidents
- Safety-critical systems
- When comprehensive barrier analysis is needed
### Components
#### Hazards
What conditions create the potential for incidents?
**Examples:**
- High traffic loads
- Software deployments
- Human interactions with critical systems
- Third-party dependencies
#### Top Event
What actually went wrong? This is the center of the bow tie.
**Examples:**
- "Database became unresponsive"
- "Payment processing failed"
- "User authentication service crashed"
#### Threats (Left Side)
What specific causes could lead to the top event?
**Examples:**
- Code defects in new deployment
- Database connection pool exhaustion
- Network connectivity issues
- DDoS attack
#### Consequences (Right Side)
What are the potential impacts of the top event?
**Examples:**
- Revenue loss
- Customer churn
- Regulatory violations
- Brand damage
- Data loss
#### Barriers
What controls exist (or could exist) to prevent threats or mitigate consequences?
**Preventive Barriers (Left Side):**
- Code reviews
- Automated testing
- Load testing
- Input validation
- Rate limiting
**Protective Barriers (Right Side):**
- Circuit breakers
- Failover systems
- Backup procedures
- Customer communication
- Rollback capabilities
### Process Steps
#### Step 1: Define the Top Event
Clearly state what went wrong.
#### Step 2: Identify Threats
Brainstorm all possible causes that could lead to the top event.
#### Step 3: Identify Consequences
List all potential impacts of the top event.
#### Step 4: Map Existing Barriers
Identify current controls for each threat and consequence.
#### Step 5: Assess Barrier Effectiveness
Evaluate how well each barrier worked (or failed).
#### Step 6: Recommend Additional Barriers
Identify new controls needed to prevent recurrence.
### Bow Tie Template
```markdown
## Bow Tie Analysis
**Top Event:** [What went wrong]
### Threats (Potential Causes)
1. **[Threat 1]**
- Likelihood: [High/Medium/Low]
- Current Barriers: [Preventive controls]
- Barrier Effectiveness: [Assessment]
2. **[Threat 2]**
- Likelihood: [High/Medium/Low]
- Current Barriers: [Preventive controls]
- Barrier Effectiveness: [Assessment]
### Consequences (Potential Impacts)
1. **[Consequence 1]**
- Severity: [High/Medium/Low]
- Current Barriers: [Protective controls]
- Barrier Effectiveness: [Assessment]
2. **[Consequence 2]**
- Severity: [High/Medium/Low]
- Current Barriers: [Protective controls]
- Barrier Effectiveness: [Assessment]
### Barrier Analysis
**Effective Barriers:**
- [Barrier that worked well]
- [Why it was effective]
**Failed Barriers:**
- [Barrier that failed]
- [Why it failed]
- [How to improve]
**Missing Barriers:**
- [Needed preventive control]
- [Needed protective control]
### Recommendations
**Preventive Measures:**
1. [New barrier to prevent threat]
2. [Improvement to existing barrier]
**Protective Measures:**
1. [New barrier to mitigate consequence]
2. [Improvement to existing barrier]
```
---
## Framework Comparison
| Framework | Time Required | Complexity | Best For | Output |
|-----------|---------------|------------|----------|---------|
| **5 Whys** | 30-60 minutes | Low | Simple, linear causes | Clear cause chain |
| **Fishbone** | 1-2 hours | Medium | Complex, multi-factor | Comprehensive factor map |
| **Timeline** | 2-3 hours | Medium | Extended incidents | Process improvements |
| **Bow Tie** | 2-4 hours | High | High-risk incidents | Barrier strategy |
## Combining Frameworks
### 5 Whys + Fishbone
Use 5 Whys for initial analysis, then Fishbone to explore contributing factors.
### Timeline + 5 Whys
Use Timeline to identify key decision points, then 5 Whys on critical failures.
### Fishbone + Bow Tie
Use Fishbone to identify causes, then Bow Tie to develop comprehensive prevention strategy.
## Quality Checklist
- [ ] Root causes address systemic issues, not symptoms
- [ ] Analysis is backed by evidence, not assumptions
- [ ] Multiple perspectives considered (technical, process, human)
- [ ] Recommendations are specific and actionable
- [ ] Analysis focuses on prevention, not blame
- [ ] Findings are validated against incident timeline
- [ ] Contributing factors are prioritized by impact
- [ ] Root causes link clearly to preventive actions
## Common Anti-Patterns
- **Human Error as Root Cause** - Dig deeper into why human error occurred
- **Single Root Cause** - Complex systems usually have multiple contributing factors
- **Technology-Only Focus** - Consider process and organizational factors
- **Blame Assignment** - Focus on system improvements, not individual fault
- **Generic Recommendations** - Provide specific, measurable actions
- **Surface-Level Analysis** - Ensure you've reached true root causes
---
**Last Updated:** February 2026
**Next Review:** August 2026
**Owner:** SRE Team + Engineering Leadership
FILE:references/reference-information.md
# incident-commander reference
## Reference Information
- **Architecture Diagram:** {link}
- **Monitoring Dashboard:** {link}
- **Related Runbooks:** {links to dependent service runbooks}
```
### Post-Incident Review (PIR) Framework
#### PIR Timeline and Ownership
**Timeline:**
- **24 hours:** Initial PIR draft completed by Incident Commander
- **3 business days:** Final PIR published with all stakeholder input
- **1 week:** Action items assigned with owners and due dates
- **4 weeks:** Follow-up review on action item progress
**Roles:**
- **PIR Owner:** Incident Commander (can delegate writing but owns completion)
- **Technical Contributors:** All engineers involved in response
- **Review Committee:** Engineering leadership, affected product teams
- **Action Item Owners:** Assigned based on expertise and capacity
#### Root Cause Analysis Frameworks
#### 1. Five Whys Method
The Five Whys technique involves asking "why" repeatedly to drill down to root causes:
**Example Application:**
- **Problem:** Database became unresponsive during peak traffic
- **Why 1:** Why did the database become unresponsive? → Connection pool was exhausted
- **Why 2:** Why was the connection pool exhausted? → Application was creating more connections than usual
- **Why 3:** Why was the application creating more connections? → New feature wasn't properly connection pooling
- **Why 4:** Why wasn't the feature properly connection pooling? → Code review missed this pattern
- **Why 5:** Why did code review miss this? → No automated checks for connection pooling patterns
**Best Practices:**
- Ask "why" at least 3 times, often need 5+ iterations
- Focus on process failures, not individual blame
- Each "why" should point to a actionable system improvement
- Consider multiple root cause paths, not just one linear chain
#### 2. Fishbone (Ishikawa) Diagram
Systematic analysis across multiple categories of potential causes:
**Categories:**
- **People:** Training, experience, communication, handoffs
- **Process:** Procedures, change management, review processes
- **Technology:** Architecture, tooling, monitoring, automation
- **Environment:** Infrastructure, dependencies, external factors
**Application Method:**
1. State the problem clearly at the "head" of the fishbone
2. For each category, brainstorm potential contributing factors
3. For each factor, ask what caused that factor (sub-causes)
4. Identify the factors most likely to be root causes
5. Validate root causes with evidence from the incident
#### 3. Timeline Analysis
Reconstruct the incident chronologically to identify decision points and missed opportunities:
**Timeline Elements:**
- **Detection:** When was the issue first observable? When was it first detected?
- **Notification:** How quickly were the right people informed?
- **Response:** What actions were taken and how effective were they?
- **Communication:** When were stakeholders updated?
- **Resolution:** What finally resolved the issue?
**Analysis Questions:**
- Where were there delays and what caused them?
- What decisions would we make differently with perfect information?
- Where did communication break down?
- What automation could have detected/resolved faster?
### Escalation Paths
#### Technical Escalation
**Level 1:** On-call engineer
- **Responsibility:** Initial response and common issue resolution
- **Escalation Trigger:** Issue not resolved within SLA timeframe
- **Timeframe:** 15 minutes (SEV1), 30 minutes (SEV2)
**Level 2:** Senior engineer/Team lead
- **Responsibility:** Complex technical issues requiring deeper expertise
- **Escalation Trigger:** Level 1 requests help or timeout occurs
- **Timeframe:** 30 minutes (SEV1), 1 hour (SEV2)
**Level 3:** Engineering Manager/Staff Engineer
- **Responsibility:** Cross-team coordination and architectural decisions
- **Escalation Trigger:** Issue spans multiple systems or teams
- **Timeframe:** 45 minutes (SEV1), 2 hours (SEV2)
**Level 4:** Director of Engineering/CTO
- **Responsibility:** Resource allocation and business impact decisions
- **Escalation Trigger:** Extended outage or significant business impact
- **Timeframe:** 1 hour (SEV1), 4 hours (SEV2)
#### Business Escalation
**Customer Impact Assessment:**
- **High:** Revenue loss, SLA breaches, customer churn risk
- **Medium:** User experience degradation, support ticket volume
- **Low:** Internal tools, development impact only
**Escalation Matrix:**
| Severity | Duration | Business Escalation |
|----------|----------|-------------------|
| SEV1 | Immediate | VP Engineering |
| SEV1 | 30 minutes | CTO + Customer Success VP |
| SEV1 | 1 hour | CEO + Full Executive Team |
| SEV2 | 2 hours | VP Engineering |
| SEV2 | 4 hours | CTO |
| SEV3 | 1 business day | Engineering Manager |
### Status Page Management
#### Update Principles
1. **Transparency:** Provide factual information without speculation
2. **Timeliness:** Update within committed timeframes
3. **Clarity:** Use customer-friendly language, avoid technical jargon
4. **Completeness:** Include impact scope, status, and next update time
#### Status Categories
- **Operational:** All systems functioning normally
- **Degraded Performance:** Some users may experience slowness
- **Partial Outage:** Subset of features unavailable
- **Major Outage:** Service unavailable for most/all users
- **Under Maintenance:** Planned maintenance window
#### Update Template
```
{Timestamp} - {Status Category}
{Brief description of current state}
Impact: {who is affected and how}
Cause: {root cause if known, "under investigation" if not}
Resolution: {what's being done to fix it}
Next update: {specific time}
We apologize for any inconvenience this may cause.
```
### Action Item Framework
#### Action Item Categories
1. **Immediate Fixes**
- Critical bugs discovered during incident
- Security vulnerabilities exposed
- Data integrity issues
2. **Process Improvements**
- Communication gaps
- Escalation procedure updates
- Runbook additions/updates
3. **Technical Debt**
- Architecture improvements
- Monitoring enhancements
- Automation opportunities
4. **Organizational Changes**
- Team structure adjustments
- Training requirements
- Tool/platform investments
#### Action Item Template
```
**Title:** {Concise description of the action}
**Priority:** {Critical/High/Medium/Low}
**Category:** {Fix/Process/Technical/Organizational}
**Owner:** {Assigned person}
**Due Date:** {Specific date}
**Success Criteria:** {How will we know this is complete}
**Dependencies:** {What needs to happen first}
**Related PIRs:** {Links to other incidents this addresses}
**Description:**
{Detailed description of what needs to be done and why}
**Implementation Plan:**
1. {Step 1}
2. {Step 2}
3. {Validation step}
**Progress Updates:**
- {Date}: {Progress update}
- {Date}: {Progress update}
```
FILE:references/sla-management-guide.md
# SLA Management Guide
> Comprehensive reference for Service Level Agreements, Objectives, and Indicators.
> Designed for incident commanders who must understand, protect, and communicate SLA status during and after incidents.
---
## 1. Definitions & Relationships
### Service Level Indicator (SLI)
An SLI is the quantitative measurement of a specific aspect of service quality. SLIs are the raw data that feed everything above them. They must be precisely defined, automatically collected, and unambiguous.
**Common SLI types by service:**
| Service Type | SLI | Measurement Method |
|---|---|---|
| Web Application | Request latency (p50, p95, p99) | Server-side histogram |
| Web Application | Availability (successful responses / total requests) | Load balancer logs |
| REST API | Error rate (5xx responses / total responses) | API gateway metrics |
| REST API | Throughput (requests per second) | Counter metric |
| Database | Query latency (p99) | Slow query log + APM |
| Database | Replication lag (seconds) | Replica monitoring |
| Message Queue | End-to-end delivery latency | Timestamp comparison |
| Message Queue | Message loss rate | Producer vs consumer counts |
| Storage | Durability (objects lost / objects stored) | Integrity checksums |
| CDN | Cache hit ratio | Edge server logs |
**SLI specification formula:**
```
SLI = (good events / total events) x 100
```
For availability: `SLI = (successful requests / total requests) x 100`
For latency: `SLI = (requests faster than threshold / total requests) x 100`
### Service Level Objective (SLO)
An SLO is the target value or range for an SLI. It defines the acceptable level of reliability. SLOs are internal goals that engineering teams commit to.
**Setting meaningful SLOs:**
1. Measure the current baseline over 30 days minimum
2. Subtract a safety margin (typically 0.05%-0.1% below actual performance)
3. Validate against user expectations and business requirements
4. Never set an SLO higher than what the system can sustain without heroics
**Common pitfall:** Setting 99.99% availability when 99.9% meets every user need. The jump from 99.9% to 99.99% is a 10x reduction in allowed downtime and typically requires 3-5x the engineering investment.
**SLO examples:**
- `99.9% of HTTP requests return a non-5xx response within each calendar month`
- `95% of API requests complete in under 200ms (p95 latency)`
- `99.95% of messages are delivered within 30 seconds of production`
### Service Level Agreement (SLA)
An SLA is a formal contract between a service provider and its customers that specifies consequences for failing to meet defined service levels. SLAs must always be looser than SLOs to provide a buffer zone.
**Rule of thumb:** If your SLO is 99.95%, your SLA should be 99.9% or lower. The gap between SLO and SLA is your safety margin.
### The Hierarchy
```
SLA (99.9%) ← Contract with customers, financial penalties
↑ backs
SLO (99.95%) ← Internal target, triggers error budget policy
↑ targets
SLI (measured) ← Raw metric: actual uptime = 99.97% this month
```
**Standard combinations by tier:**
| Tier | SLI (Metric) | SLO (Target) | SLA (Contract) | Allowed Downtime/Month |
|---|---|---|---|---|
| Critical (payments) | Availability | 99.99% | 99.95% | SLO: 4.38 min / SLA: 21.9 min |
| High (core API) | Availability | 99.95% | 99.9% | SLO: 21.9 min / SLA: 43.8 min |
| Standard (dashboard) | Availability | 99.9% | 99.5% | SLO: 43.8 min / SLA: 3.65 hrs |
| Low (internal tools) | Availability | 99.5% | 99.0% | SLO: 3.65 hrs / SLA: 7.3 hrs |
---
## 2. Error Budget Policy
### What Is an Error Budget
An error budget is the maximum amount of unreliability a service can have within a given period while still meeting its SLO. It is calculated as:
```
Error Budget = 1 - SLO target
```
For a 99.9% SLO over a 30-day month (43,200 minutes):
```
Error Budget = 1 - 0.999 = 0.001 = 0.1%
Allowed Downtime = 43,200 x 0.001 = 43.2 minutes
```
### Downtime Allowances by SLO
| SLO | Error Budget | Monthly Downtime | Quarterly Downtime | Annual Downtime |
|---|---|---|---|---|
| 99.0% | 1.0% | 7 hrs 18 min | 21 hrs 54 min | 3 days 15 hrs |
| 99.5% | 0.5% | 3 hrs 39 min | 10 hrs 57 min | 1 day 19 hrs |
| 99.9% | 0.1% | 43.8 min | 2 hrs 11 min | 8 hrs 46 min |
| 99.95% | 0.05% | 21.9 min | 1 hr 6 min | 4 hrs 23 min |
| 99.99% | 0.01% | 4.38 min | 13.1 min | 52.6 min |
| 99.999% | 0.001% | 26.3 sec | 78.9 sec | 5.26 min |
### Error Budget Consumption Tracking
Track budget consumption as a percentage of the total budget used so far in the current window:
```
Budget Consumed (%) = (actual bad minutes / allowed bad minutes) x 100
```
Example: SLO is 99.9% (43.8 min budget/month). On day 10, you have had 15 minutes of downtime.
```
Budget Consumed = (15 / 43.8) x 100 = 34.2%
Expected consumption at day 10 = (10/30) x 100 = 33.3%
Status: Slightly over pace (34.2% consumed at 33.3% of month elapsed)
```
### Burn Rate
Burn rate measures how fast the error budget is being consumed relative to the steady-state rate:
```
Burn Rate = (error rate observed / error rate allowed by SLO)
```
A burn rate of 1.0 means the budget will be exactly exhausted by the end of the window. A burn rate of 10 means the budget will be exhausted in 1/10th of the window.
**Burn rate to time-to-exhaustion (30-day month):**
| Burn Rate | Budget Exhausted In | Urgency |
|---|---|---|
| 1x | 30 days | On pace, monitoring only |
| 2x | 15 days | Elevated attention |
| 6x | 5 days | Active investigation required |
| 14.4x | 2.08 days (~50 hours) | Immediate page |
| 36x | 20 hours | Critical, all-hands |
| 720x | 1 hour | Total outage scenario |
### Error Budget Exhaustion Policy
When the error budget is consumed, the following actions trigger based on threshold:
**Tier 1 - Budget at 75% consumed (Yellow):**
- Notify service team lead via automated alert
- Freeze non-critical deployments to the affected service
- Conduct pre-emptive review of upcoming changes for risk
- Increase monitoring sensitivity (lower alert thresholds)
**Tier 2 - Budget at 100% consumed (Orange):**
- Hard feature freeze on the affected service
- Mandatory reliability sprint: all engineering effort redirected to reliability
- Daily status updates to engineering leadership
- Postmortem required for the incidents that consumed the budget
- Freeze lasts until budget replenishes to 50% or systemic fixes are verified
**Tier 3 - Budget at 150% consumed / SLA breach imminent (Red):**
- Escalation to VP Engineering and CTO
- Cross-team war room if dependencies are involved
- Customer communication prepared and staged
- Legal and finance teams briefed on potential SLA credit obligations
- Recovery plan with specific milestones required within 24 hours
### Error Budget Policy Template
```
SERVICE: [service-name]
SLO: [target]% availability over [rolling 30-day / calendar month] window
ERROR BUDGET: [calculated] minutes per window
BUDGET THRESHOLDS:
- 50% consumed: Team notification, increased vigilance
- 75% consumed: Feature freeze for this service, reliability focus
- 100% consumed: Full feature freeze, reliability sprint mandatory
- SLA threshold crossed: Executive escalation, customer communication
REVIEW CADENCE: Monthly budget review on [day], quarterly SLO adjustment
EXCEPTIONS: Planned maintenance windows excluded if communicated 72+ hours in advance
and within agreed maintenance allowance.
APPROVED BY: [Engineering Lead] / [Product Lead] / [Date]
```
---
## 3. SLA Breach Handling
### Detection Methods
**Automated detection (primary):**
- Real-time monitoring dashboards with SLA burn-rate alerts
- Automated SLA compliance calculations running every 5 minutes
- Threshold-based alerts when cumulative downtime approaches SLA limits
- Synthetic monitoring (external probes) for customer-perspective validation
**Manual review (secondary):**
- Monthly SLA compliance reports generated on the 1st of each month
- Customer-reported incidents cross-referenced with internal metrics
- Quarterly audits comparing measured SLIs against contracted SLAs
- Discrepancy review between internal metrics and customer-perceived availability
### Breach Classification
**Minor Breach:**
- SLA missed by less than 0.05 percentage points (e.g., 99.85% vs 99.9% SLA)
- Fewer than 3 discrete incidents contributed
- No single incident exceeded 30 minutes
- Customer impact was limited or partial degradation only
- Financial credit: typically 5-10% of monthly service fee
**Major Breach:**
- SLA missed by 0.05 to 0.5 percentage points
- Extended outage of 1-4 hours in a single incident, or multiple significant incidents
- Clear customer impact with support tickets generated
- Financial credit: typically 10-25% of monthly service fee
**Critical Breach:**
- SLA missed by more than 0.5 percentage points
- Total outage exceeding 4 hours, or repeated major incidents in same window
- Data loss, security incident, or compliance violation involved
- Financial credit: typically 25-100% of monthly service fee
- May trigger contract termination clauses
### Response Protocol
**For Minor Breach (within 3 business days):**
1. Generate SLA compliance report with exact metrics
2. Document contributing incidents with root causes
3. Send proactive notification to customer success manager
4. Issue service credits if contractually required (do not wait for customer to ask)
5. File internal improvement ticket with 30-day remediation target
**For Major Breach (within 24 hours):**
1. Incident commander confirms SLA impact calculation
2. Draft customer communication (see template below)
3. Executive sponsor reviews and approves communication
4. Issue service credits with detailed breakdown
5. Schedule root cause review with customer within 5 business days
6. Produce remediation plan with committed timelines
**For Critical Breach (immediate):**
1. Activate executive escalation chain
2. Legal team reviews contractual exposure
3. Finance team calculates credit obligations
4. Customer communication from VP or C-level within 4 hours
5. Dedicated remediation task force assigned
6. Weekly status updates to customer until remediation complete
7. Formal postmortem document shared with customer within 10 business days
### Customer Communication Template
```
Subject: Service Level Update - [Service Name] - [Month Year]
Dear [Customer Name],
We are writing to inform you that [Service Name] did not meet the committed
service level of [SLA target]% availability during [time period].
MEASURED PERFORMANCE: [actual]% availability
COMMITTED SLA: [SLA target]% availability
SHORTFALL: [delta] percentage points
CONTRIBUTING FACTORS:
- [Date/Time]: [Brief description of incident] ([duration] impact)
- [Date/Time]: [Brief description of incident] ([duration] impact)
SERVICE CREDIT: In accordance with our agreement, a credit of [amount/percentage]
will be applied to your next invoice.
REMEDIATION ACTIONS:
1. [Specific technical fix with completion date]
2. [Process improvement with implementation date]
3. [Monitoring enhancement with deployment date]
We take our service commitments seriously. [Name], [Title] is personally
overseeing the remediation and is available to discuss further at your convenience.
Sincerely,
[Name, Title]
```
### Legal and Compliance Considerations
- Maintain auditable records of all SLA measurements for the full contract term plus 2 years
- SLA calculations must use the measurement methodology defined in the contract, not internal approximations
- Force majeure clauses typically exclude natural disasters, but verify per contract
- Planned maintenance exclusions must match the exact notification procedures in the contract
- Multi-region SLAs may have separate calculations per region; verify aggregation method
---
## 4. Incident-to-SLA Mapping
### Downtime Calculation Methodologies
**Full outage:** Service completely unavailable. Every minute counts as a full minute of downtime.
```
Downtime = End Time - Start Time (in minutes)
```
**Partial degradation:** Service available but impaired. Apply a degradation factor:
```
Effective Downtime = Actual Duration x Degradation Factor
```
| Degradation Level | Factor | Description |
|---|---|---|
| Complete outage | 1.0 | Service fully unavailable |
| Severe degradation | 0.75 | >50% of requests failing or >10x latency |
| Moderate degradation | 0.5 | 10-50% of requests affected or 3-10x latency |
| Minor degradation | 0.25 | <10% of requests affected or <3x latency increase |
| Cosmetic / non-functional | 0.0 | No impact on core SLI metrics |
**Note:** The exact degradation factors must be agreed upon in the SLA contract. The above are industry-standard starting points.
### Planned vs Unplanned Downtime
Most SLAs exclude pre-announced maintenance windows from availability calculations, subject to conditions:
- Notification provided N hours/days in advance (commonly 72 hours)
- Maintenance occurs within an agreed window (e.g., Sunday 02:00-06:00 UTC)
- Total planned downtime does not exceed the monthly maintenance allowance (e.g., 4 hours/month)
- Any overrun beyond the planned window counts as unplanned downtime
```
SLA Availability = (Total Minutes - Excluded Maintenance - Unplanned Downtime) / (Total Minutes - Excluded Maintenance) x 100
```
### Multi-Service SLA Composition
When a customer-facing product depends on multiple services, composite SLA is calculated as:
**Serial dependency (all must be up):**
```
Composite SLA = SLA_A x SLA_B x SLA_C
Example: 99.9% x 99.95% x 99.99% = 99.84%
```
**Parallel / redundant (any one must be up):**
```
Composite Availability = 1 - ((1 - SLA_A) x (1 - SLA_B))
Example: 1 - ((1 - 0.999) x (1 - 0.999)) = 1 - 0.000001 = 99.9999%
```
This is critical during incidents: an outage in a shared dependency may breach SLAs for multiple customer-facing products simultaneously.
### Worked Examples
**Example 1: Simple outage**
- Service: Core API (SLA: 99.9%)
- Month: 30 days = 43,200 minutes
- Incident: Full outage from 14:23 to 14:38 UTC on the 12th (15 minutes)
- No other incidents this month
```
Availability = (43,200 - 15) / 43,200 x 100 = 99.965%
SLA Status: PASS (99.965% > 99.9%)
Error Budget Consumed: 15 / 43.2 = 34.7%
```
**Example 2: Partial degradation**
- Service: Payment Processing (SLA: 99.95%)
- Month: 30 days = 43,200 minutes
- Incident: 50% of transactions failing for 4 hours (240 minutes)
- Degradation factor: 0.5 (moderate - 50% of requests affected)
```
Effective Downtime = 240 x 0.5 = 120 minutes
Availability = (43,200 - 120) / 43,200 x 100 = 99.722%
SLA Status: FAIL (99.722% < 99.95%)
Shortfall: 0.228 percentage points → Major Breach
```
**Example 3: Multiple incidents**
- Service: Dashboard (SLA: 99.5%)
- Month: 31 days = 44,640 minutes
- Incident A: 45-minute full outage on the 5th
- Incident B: 2-hour severe degradation (factor 0.75) on the 18th
- Incident C: 30-minute full outage on the 25th
```
Total Effective Downtime = 45 + (120 x 0.75) + 30 = 45 + 90 + 30 = 165 minutes
Availability = (44,640 - 165) / 44,640 x 100 = 99.630%
SLA Status: PASS (99.630% > 99.5%)
Error Budget Consumed: 165 / 223.2 = 73.9% → Yellow threshold, feature freeze recommended
```
---
## 5. SLO Best Practices
### Start with User Journeys
Do not set SLOs based on infrastructure metrics. Start from what users experience:
1. Identify critical user journeys (e.g., "User completes checkout")
2. Map each journey to the services and dependencies involved
3. Define what "good" looks like for each journey (fast, error-free, complete)
4. Select the SLIs that most directly measure that user experience
5. Set SLO targets that reflect the minimum acceptable user experience
A database with 99.99% uptime is meaningless if the API in front of it has a bug causing 5% error rates.
### The Four Golden Signals as SLI Sources
From Google SRE, the four golden signals provide comprehensive service health:
| Signal | SLI Example | Typical SLO |
|---|---|---|
| Latency | p99 request duration < 500ms | 99% of requests under threshold |
| Traffic | Requests per second | N/A (capacity planning, not SLO) |
| Errors | 5xx rate as % of total requests | < 0.1% error rate over rolling window |
| Saturation | CPU/memory/queue depth | < 80% utilization (capacity SLI) |
For most services, latency and error rate are the two most important SLIs to back with SLOs.
### Setting SLO Targets
1. Collect 90 days of historical SLI data
2. Calculate the 5th percentile performance (worst 5% of days)
3. Set SLO slightly above that baseline (this ensures the SLO is achievable without heroics)
4. Validate: would a breach at this level actually impact users negatively?
5. Adjust upward only if user impact analysis demands it
**Never set SLOs by aspiration.** A 99.99% SLO on a service that has historically achieved 99.93% is a guaranteed source of perpetual firefighting with no reliability improvement.
### Review Cadence
- **Weekly:** Review current error budget burn rate, flag services approaching thresholds
- **Monthly:** Full SLO compliance review, adjust alert thresholds if needed
- **Quarterly:** Reassess SLO targets based on 90-day data, review SLA contract alignment
- **Annually:** Strategic SLO review tied to product roadmap and infrastructure investments
### Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Vanity SLOs | Setting 99.99% to impress, then ignoring breaches | Set achievable targets, enforce budget policy |
| SLO Inflation | Ratcheting SLOs up whenever performance is good | Only increase SLOs when users demonstrably need it |
| Unmeasured SLAs | Committing contractual SLAs without actual SLI measurement | Instrument SLIs before signing SLA contracts |
| Copy-Paste SLOs | Same SLO for every service regardless of criticality | Tier services by business impact, set SLOs accordingly |
| Ignoring Dependencies | Setting aggressive SLOs without accounting for dependency reliability | Calculate composite SLA; your SLO cannot exceed dependency chain |
| Alert-Free SLOs | Having SLOs but no automated alerting on budget consumption | Every SLO must have corresponding burn rate alerts |
---
## 6. Monitoring & Alerting for SLAs
### Multi-Window Burn Rate Alerting
The Google SRE approach uses multiple time windows to balance speed of detection against alert noise. Each alert condition requires both a short window (for speed) and a long window (for confirmation):
**Alert configuration matrix:**
| Severity | Short Window | Short Threshold | Long Window | Long Threshold | Action |
|---|---|---|---|---|---|
| Critical (Page) | 1 hour | > 14.4x burn rate | 5 minutes | > 14.4x burn rate | Wake someone up |
| High (Page) | 6 hours | > 6x burn rate | 30 minutes | > 6x burn rate | Page on-call within 30 min |
| Medium (Ticket) | 3 days | > 1x burn rate | 6 hours | > 1x burn rate | Create ticket, next business day |
**Why these specific numbers:**
- 14.4x burn rate over 1 hour consumes 2% of monthly budget in that hour. At this rate, the entire 30-day budget is gone in ~50 hours. This demands immediate human attention.
- 6x burn rate over 6 hours consumes 5% of monthly budget. The budget will be exhausted in 5 days. Urgent but not wake-up-at-3am urgent.
- 1x burn rate over 3 days means you are on pace to exactly exhaust the budget. This needs investigation but is not an emergency.
### Burn Rate Alert Formulas
For a given time window, calculate the burn rate:
```
burn_rate = (error_count_in_window / request_count_in_window) / (1 - SLO_target)
```
Example for a 99.9% SLO, observing 50 errors out of 10,000 requests in a 1-hour window:
```
observed_error_rate = 50 / 10,000 = 0.005 (0.5%)
allowed_error_rate = 1 - 0.999 = 0.001 (0.1%)
burn_rate = 0.005 / 0.001 = 5.0
```
A burn rate of 5.0 means the error budget is being consumed 5 times faster than the sustainable rate.
### Alert Severity to SLA Risk Mapping
| Burn Rate | Budget Impact | SLA Risk | Response |
|---|---|---|---|
| < 1x | Under budget pace | None | Routine monitoring |
| 1x - 3x | On pace or slightly over | Low | Investigate next business day |
| 3x - 6x | Budget will exhaust in 5-10 days | Moderate | Investigate within 4 hours |
| 6x - 14.4x | Budget will exhaust in 2-5 days | High | Page on-call, respond in 30 min |
| > 14.4x | Budget will exhaust in < 2 days | Critical | Immediate page, incident declared |
| > 100x | Active major outage | SLA breach imminent | All-hands incident response |
### Dashboard Design for SLA Tracking
Every SLA-tracked service should have a dashboard with these panels:
**Row 1 - Current Status:**
- Current availability (real-time, rolling 5-minute window)
- Current error rate (real-time)
- Current p99 latency (real-time)
**Row 2 - Budget Status:**
- Error budget remaining (% of monthly budget, gauge visualization)
- Budget consumption timeline (line chart, actual vs expected burn)
- Budget burn rate (current 1h, 6h, and 3d burn rates)
**Row 3 - Historical Context:**
- 30-day availability trend (daily granularity)
- SLA compliance status for current and previous 3 months
- Incident markers overlaid on availability timeline
**Row 4 - Dependencies:**
- Upstream dependency availability (services this service depends on)
- Downstream impact scope (services that depend on this service)
- Composite SLA calculation for customer-facing products
### Alert Fatigue Prevention
Alert fatigue is the primary reason SLA monitoring fails in practice. Mitigation strategies:
1. **Require dual-window confirmation.** Never page on a single short window. Always require both the short window (for speed) and long window (for persistence) to fire simultaneously.
2. **Separate page-worthy from ticket-worthy.** Only two conditions should wake someone up: >14.4x burn rate sustained, or >6x burn rate sustained. Everything else is a ticket.
3. **Deduplicate aggressively.** If the same service triggers both a latency and error rate alert for the same underlying issue, group them into a single notification.
4. **Auto-resolve.** Alerts must auto-resolve when the burn rate drops below threshold. Never leave stale alerts open.
5. **Review alert quality monthly.** Track the ratio of actionable alerts to total alerts. Target >80% actionable rate. If an alert fires and no human action is needed, tune or remove it.
6. **Escalation, not repetition.** If an alert is not acknowledged within the response window, escalate to the next tier. Do not re-send the same alert every 5 minutes.
### Practical Monitoring Stack
| Layer | Tool Category | Purpose |
|---|---|---|
| Collection | Prometheus, OpenTelemetry, StatsD | Gather SLI metrics from services |
| Storage | Prometheus TSDB, Thanos, Mimir | Retain metrics for SLO window + 90 days |
| Calculation | Prometheus recording rules, Sloth | Pre-compute burn rates and budget consumption |
| Alerting | Alertmanager, PagerDuty, OpsGenie | Route alerts by severity and schedule |
| Visualization | Grafana, Datadog | Dashboards for real-time and historical SLA views |
| Reporting | Custom scripts, SLO generators | Monthly SLA compliance reports for customers |
**Retention requirement:** SLI data must be retained for at least the SLA reporting period (typically monthly or quarterly) plus a 90-day dispute window. Annual SLA reviews require 12 months of data at daily granularity minimum.
---
*Last updated: February 2026*
*For use with: incident-commander skill*
*Maintainer: Engineering Team*
FILE:scripts/incident_classifier.py
#!/usr/bin/env python3
"""
Incident Classifier
Analyzes incident descriptions and outputs severity levels, recommended response teams,
initial actions, and communication templates.
This tool uses pattern matching and keyword analysis to classify incidents according to
SEV1-4 criteria and provide structured response guidance.
Usage:
python incident_classifier.py --input incident.json
echo "Database is down" | python incident_classifier.py --format text
python incident_classifier.py --interactive
"""
import argparse
import json
import sys
import re
from datetime import datetime, timezone
from typing import Dict, List, Tuple, Optional, Any
class IncidentClassifier:
"""
Classifies incidents based on description, impact metrics, and business context.
Provides severity assessment, team recommendations, and response templates.
"""
def __init__(self):
"""Initialize the classifier with rules and templates."""
self.severity_rules = self._load_severity_rules()
self.team_mappings = self._load_team_mappings()
self.communication_templates = self._load_communication_templates()
self.action_templates = self._load_action_templates()
def _load_severity_rules(self) -> Dict[str, Dict]:
"""Load severity classification rules and keywords."""
return {
"sev1": {
"keywords": [
"down", "outage", "offline", "unavailable", "crashed", "failed",
"critical", "emergency", "dead", "broken", "timeout", "500 error",
"data loss", "corrupted", "breach", "security incident",
"revenue impact", "customer facing", "all users", "complete failure"
],
"impact_indicators": [
"100%", "all users", "entire service", "complete",
"revenue loss", "sla violation", "customer churn",
"security breach", "data corruption", "regulatory"
],
"duration_threshold": 0, # Immediate classification
"response_time": 300, # 5 minutes
"description": "Complete service failure affecting all users or critical business functions"
},
"sev2": {
"keywords": [
"degraded", "slow", "performance", "errors", "partial",
"intermittent", "high latency", "timeouts", "some users",
"feature broken", "api errors", "database slow"
],
"impact_indicators": [
"50%", "25-75%", "many users", "significant",
"performance degradation", "feature unavailable",
"support tickets", "user complaints"
],
"duration_threshold": 300, # 5 minutes
"response_time": 900, # 15 minutes
"description": "Significant degradation affecting subset of users or non-critical functions"
},
"sev3": {
"keywords": [
"minor", "cosmetic", "single feature", "workaround available",
"edge case", "rare issue", "non-critical", "internal tool",
"logging issue", "monitoring gap"
],
"impact_indicators": [
"<25%", "few users", "limited impact",
"workaround exists", "internal only",
"development environment"
],
"duration_threshold": 3600, # 1 hour
"response_time": 7200, # 2 hours
"description": "Limited impact with workarounds available"
},
"sev4": {
"keywords": [
"cosmetic", "documentation", "typo", "minor bug",
"enhancement", "nice to have", "low priority",
"test environment", "dev tools"
],
"impact_indicators": [
"no impact", "cosmetic only", "documentation",
"development", "testing", "non-production"
],
"duration_threshold": 86400, # 24 hours
"response_time": 172800, # 2 days
"description": "Minimal impact, cosmetic issues, or planned maintenance"
}
}
def _load_team_mappings(self) -> Dict[str, List[str]]:
"""Load team assignment rules based on service/component keywords."""
return {
"database": ["Database Team", "SRE", "Backend Engineering"],
"frontend": ["Frontend Team", "UX Engineering", "Product Engineering"],
"api": ["API Team", "Backend Engineering", "Platform Team"],
"infrastructure": ["SRE", "DevOps", "Platform Team"],
"security": ["Security Team", "SRE", "Compliance Team"],
"network": ["Network Engineering", "SRE", "Infrastructure Team"],
"authentication": ["Identity Team", "Security Team", "Backend Engineering"],
"payment": ["Payments Team", "Finance Engineering", "Compliance Team"],
"mobile": ["Mobile Team", "API Team", "QA Engineering"],
"monitoring": ["SRE", "Platform Team", "DevOps"],
"deployment": ["DevOps", "Release Engineering", "SRE"],
"data": ["Data Engineering", "Analytics Team", "Backend Engineering"]
}
def _load_communication_templates(self) -> Dict[str, Dict]:
"""Load communication templates for each severity level."""
return {
"sev1": {
"subject": "🚨 [SEV1] {service} - {brief_description}",
"body": """CRITICAL INCIDENT ALERT
Incident Details:
- Start Time: {timestamp}
- Severity: SEV1 - Critical Outage
- Service: {service}
- Impact: {impact_description}
- Current Status: Investigating
Customer Impact:
{customer_impact}
Response Team:
- Incident Commander: TBD (assigning now)
- Primary Responder: {primary_responder}
- SMEs Required: {subject_matter_experts}
Immediate Actions Taken:
{initial_actions}
War Room: {war_room_link}
Status Page: Will be updated within 15 minutes
Next Update: {next_update_time}
This is a customer-impacting incident requiring immediate attention.
{incident_commander_contact}"""
},
"sev2": {
"subject": "⚠️ [SEV2] {service} - {brief_description}",
"body": """MAJOR INCIDENT NOTIFICATION
Incident Details:
- Start Time: {timestamp}
- Severity: SEV2 - Major Impact
- Service: {service}
- Impact: {impact_description}
- Current Status: Investigating
User Impact:
{customer_impact}
Response Team:
- Primary Responder: {primary_responder}
- Supporting Team: {supporting_teams}
- Incident Commander: {incident_commander}
Initial Assessment:
{initial_assessment}
Next Steps:
{next_steps}
Updates will be provided every 30 minutes.
Status page: {status_page_link}
{contact_information}"""
},
"sev3": {
"subject": "ℹ️ [SEV3] {service} - {brief_description}",
"body": """MINOR INCIDENT NOTIFICATION
Incident Details:
- Start Time: {timestamp}
- Severity: SEV3 - Minor Impact
- Service: {service}
- Impact: {impact_description}
- Status: {current_status}
Details:
{incident_details}
Assigned Team: {assigned_team}
Estimated Resolution: {eta}
Workaround: {workaround}
This incident has limited customer impact and is being addressed during normal business hours.
{team_contact}"""
},
"sev4": {
"subject": "[SEV4] {service} - {brief_description}",
"body": """LOW PRIORITY ISSUE
Issue Details:
- Reported: {timestamp}
- Severity: SEV4 - Low Impact
- Component: {service}
- Description: {description}
This issue will be addressed in the normal development cycle.
Assigned to: {assigned_team}
Target Resolution: {target_date}
{standard_contact}"""
}
}
def _load_action_templates(self) -> Dict[str, List[Dict]]:
"""Load initial action templates for each severity level."""
return {
"sev1": [
{
"action": "Establish incident command",
"priority": 1,
"timeout_minutes": 5,
"description": "Page incident commander and establish war room"
},
{
"action": "Create incident ticket",
"priority": 1,
"timeout_minutes": 2,
"description": "Create tracking ticket with all known details"
},
{
"action": "Update status page",
"priority": 2,
"timeout_minutes": 15,
"description": "Post initial status page update acknowledging incident"
},
{
"action": "Notify executives",
"priority": 2,
"timeout_minutes": 15,
"description": "Alert executive team of customer-impacting outage"
},
{
"action": "Engage subject matter experts",
"priority": 3,
"timeout_minutes": 10,
"description": "Page relevant SMEs based on affected systems"
},
{
"action": "Begin technical investigation",
"priority": 3,
"timeout_minutes": 5,
"description": "Start technical diagnosis and mitigation efforts"
}
],
"sev2": [
{
"action": "Assign incident commander",
"priority": 1,
"timeout_minutes": 30,
"description": "Assign IC and establish coordination channel"
},
{
"action": "Create incident tracking",
"priority": 1,
"timeout_minutes": 5,
"description": "Create incident ticket with details and timeline"
},
{
"action": "Assess customer impact",
"priority": 2,
"timeout_minutes": 15,
"description": "Determine scope and severity of user impact"
},
{
"action": "Engage response team",
"priority": 2,
"timeout_minutes": 30,
"description": "Page appropriate technical responders"
},
{
"action": "Begin investigation",
"priority": 3,
"timeout_minutes": 15,
"description": "Start technical analysis and debugging"
},
{
"action": "Plan status communication",
"priority": 3,
"timeout_minutes": 30,
"description": "Determine if status page update is needed"
}
],
"sev3": [
{
"action": "Assign to appropriate team",
"priority": 1,
"timeout_minutes": 120,
"description": "Route to team with relevant expertise"
},
{
"action": "Create tracking ticket",
"priority": 1,
"timeout_minutes": 30,
"description": "Document issue in standard ticketing system"
},
{
"action": "Assess scope and impact",
"priority": 2,
"timeout_minutes": 60,
"description": "Understand full scope of the issue"
},
{
"action": "Identify workarounds",
"priority": 2,
"timeout_minutes": 60,
"description": "Find temporary solutions if possible"
},
{
"action": "Plan resolution approach",
"priority": 3,
"timeout_minutes": 120,
"description": "Develop plan for permanent fix"
}
],
"sev4": [
{
"action": "Create backlog item",
"priority": 1,
"timeout_minutes": 1440, # 24 hours
"description": "Add to team backlog for future sprint planning"
},
{
"action": "Triage and prioritize",
"priority": 2,
"timeout_minutes": 2880, # 2 days
"description": "Review and prioritize against other work"
},
{
"action": "Assign owner",
"priority": 3,
"timeout_minutes": 4320, # 3 days
"description": "Assign to appropriate developer when capacity allows"
}
]
}
def classify_incident(self, incident_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Main classification method that analyzes incident data and returns
comprehensive response recommendations.
Args:
incident_data: Dictionary containing incident information
Returns:
Dictionary with classification results and recommendations
"""
# Extract key information from incident data
description = incident_data.get('description', '').lower()
affected_users = incident_data.get('affected_users', '0%')
business_impact = incident_data.get('business_impact', 'unknown')
service = incident_data.get('service', 'unknown service')
duration = incident_data.get('duration_minutes', 0)
# Classify severity
severity = self._classify_severity(description, affected_users, business_impact, duration)
# Determine response teams
response_teams = self._determine_teams(description, service)
# Generate initial actions
initial_actions = self._generate_initial_actions(severity, incident_data)
# Create communication template
communication = self._generate_communication(severity, incident_data)
# Calculate response timeline
timeline = self._generate_timeline(severity)
# Determine escalation path
escalation = self._determine_escalation(severity, business_impact)
return {
"classification": {
"severity": severity.upper(),
"confidence": self._calculate_confidence(description, affected_users, business_impact),
"reasoning": self._explain_classification(severity, description, affected_users),
"timestamp": datetime.now(timezone.utc).isoformat()
},
"response": {
"primary_team": response_teams[0] if response_teams else "General Engineering",
"supporting_teams": response_teams[1:] if len(response_teams) > 1 else [],
"all_teams": response_teams,
"response_time_minutes": self.severity_rules[severity]["response_time"] // 60
},
"initial_actions": initial_actions,
"communication": communication,
"timeline": timeline,
"escalation": escalation,
"incident_data": {
"service": service,
"description": incident_data.get('description', ''),
"affected_users": affected_users,
"business_impact": business_impact,
"duration_minutes": duration
}
}
def _classify_severity(self, description: str, affected_users: str,
business_impact: str, duration: int) -> str:
"""Classify incident severity based on multiple factors."""
scores = {"sev1": 0, "sev2": 0, "sev3": 0, "sev4": 0}
# Keyword analysis
for severity, rules in self.severity_rules.items():
for keyword in rules["keywords"]:
if keyword in description:
scores[severity] += 2
for indicator in rules["impact_indicators"]:
if indicator.lower() in description or indicator.lower() in affected_users.lower():
scores[severity] += 3
# Business impact weighting
if business_impact.lower() in ['critical', 'high', 'severe']:
scores["sev1"] += 5
scores["sev2"] += 3
elif business_impact.lower() in ['medium', 'moderate']:
scores["sev2"] += 3
scores["sev3"] += 2
elif business_impact.lower() in ['low', 'minimal']:
scores["sev3"] += 2
scores["sev4"] += 3
# User impact analysis
if '%' in affected_users:
try:
percentage = float(re.findall(r'\d+', affected_users)[0])
if percentage >= 75:
scores["sev1"] += 4
elif percentage >= 25:
scores["sev2"] += 4
elif percentage >= 5:
scores["sev3"] += 3
else:
scores["sev4"] += 2
except (IndexError, ValueError):
pass
# Duration consideration
if duration > 0:
if duration >= 3600: # 1 hour
scores["sev1"] += 2
scores["sev2"] += 1
elif duration >= 1800: # 30 minutes
scores["sev2"] += 2
scores["sev3"] += 1
# Return highest scoring severity
return max(scores, key=scores.get)
def _determine_teams(self, description: str, service: str) -> List[str]:
"""Determine which teams should respond based on affected systems."""
teams = set()
text_to_analyze = f"{description} {service}".lower()
for component, team_list in self.team_mappings.items():
if component in text_to_analyze:
teams.update(team_list)
# Default teams if no specific match
if not teams:
teams = {"General Engineering", "SRE"}
return list(teams)
def _generate_initial_actions(self, severity: str, incident_data: Dict) -> List[Dict]:
"""Generate prioritized initial actions based on severity."""
base_actions = self.action_templates[severity].copy()
# Customize actions based on incident details
for action in base_actions:
if severity in ["sev1", "sev2"]:
action["urgency"] = "immediate" if severity == "sev1" else "high"
else:
action["urgency"] = "normal" if severity == "sev3" else "low"
return base_actions
def _generate_communication(self, severity: str, incident_data: Dict) -> Dict:
"""Generate communication template filled with incident data."""
template = self.communication_templates[severity]
# Fill template with incident data
now = datetime.now(timezone.utc)
service = incident_data.get('service', 'Unknown Service')
description = incident_data.get('description', 'Incident detected')
communication = {
"subject": template["subject"].format(
service=service,
brief_description=description[:50] + "..." if len(description) > 50 else description
),
"body": template["body"],
"urgency": severity,
"recipients": self._determine_recipients(severity),
"channels": self._determine_channels(severity),
"frequency_minutes": self._get_update_frequency(severity)
}
return communication
def _generate_timeline(self, severity: str) -> Dict:
"""Generate expected response timeline."""
rules = self.severity_rules[severity]
now = datetime.now(timezone.utc)
milestones = []
if severity == "sev1":
milestones = [
{"milestone": "Incident Commander assigned", "minutes": 5},
{"milestone": "War room established", "minutes": 10},
{"milestone": "Initial status page update", "minutes": 15},
{"milestone": "Executive notification", "minutes": 15},
{"milestone": "First customer update", "minutes": 30}
]
elif severity == "sev2":
milestones = [
{"milestone": "Response team assembled", "minutes": 15},
{"milestone": "Initial assessment complete", "minutes": 30},
{"milestone": "Stakeholder notification", "minutes": 60},
{"milestone": "Status page update (if needed)", "minutes": 60}
]
elif severity == "sev3":
milestones = [
{"milestone": "Team assignment", "minutes": 120},
{"milestone": "Initial triage complete", "minutes": 240},
{"milestone": "Resolution plan created", "minutes": 480}
]
else: # sev4
milestones = [
{"milestone": "Backlog creation", "minutes": 1440},
{"milestone": "Priority assessment", "minutes": 2880}
]
return {
"response_time_minutes": rules["response_time"] // 60,
"milestones": milestones,
"update_frequency_minutes": self._get_update_frequency(severity)
}
def _determine_escalation(self, severity: str, business_impact: str) -> Dict:
"""Determine escalation requirements and triggers."""
escalation_rules = {
"sev1": {
"immediate": ["Incident Commander", "Engineering Manager"],
"15_minutes": ["VP Engineering", "Customer Success"],
"30_minutes": ["CTO"],
"60_minutes": ["CEO", "All C-Suite"],
"triggers": ["Extended outage", "Revenue impact", "Media attention"]
},
"sev2": {
"immediate": ["Team Lead", "On-call Engineer"],
"30_minutes": ["Engineering Manager"],
"120_minutes": ["VP Engineering"],
"triggers": ["No progress", "Expanding scope", "Customer escalation"]
},
"sev3": {
"immediate": ["Assigned Engineer"],
"240_minutes": ["Team Lead"],
"triggers": ["Issue complexity", "Multiple teams needed"]
},
"sev4": {
"immediate": ["Product Owner"],
"triggers": ["Customer request", "Stakeholder priority"]
}
}
return escalation_rules.get(severity, escalation_rules["sev4"])
def _determine_recipients(self, severity: str) -> List[str]:
"""Determine who should receive notifications."""
recipients = {
"sev1": ["on-call", "engineering-leadership", "executives", "customer-success"],
"sev2": ["on-call", "engineering-leadership", "product-team"],
"sev3": ["assigned-team", "team-lead"],
"sev4": ["assigned-engineer"]
}
return recipients.get(severity, recipients["sev4"])
def _determine_channels(self, severity: str) -> List[str]:
"""Determine communication channels to use."""
channels = {
"sev1": ["pager", "phone", "slack", "email", "status-page"],
"sev2": ["pager", "slack", "email"],
"sev3": ["slack", "email"],
"sev4": ["ticket-system"]
}
return channels.get(severity, channels["sev4"])
def _get_update_frequency(self, severity: str) -> int:
"""Get recommended update frequency in minutes."""
frequencies = {"sev1": 15, "sev2": 30, "sev3": 240, "sev4": 0}
return frequencies.get(severity, 0)
def _calculate_confidence(self, description: str, affected_users: str, business_impact: str) -> float:
"""Calculate confidence score for the classification."""
confidence = 0.5 # Base confidence
# Higher confidence with more specific information
if '%' in affected_users and any(char.isdigit() for char in affected_users):
confidence += 0.2
if business_impact.lower() in ['critical', 'high', 'medium', 'low']:
confidence += 0.15
if len(description.split()) > 5: # Detailed description
confidence += 0.15
return min(confidence, 1.0)
def _explain_classification(self, severity: str, description: str, affected_users: str) -> str:
"""Provide explanation for the classification decision."""
rules = self.severity_rules[severity]
matched_keywords = []
for keyword in rules["keywords"]:
if keyword in description.lower():
matched_keywords.append(keyword)
explanation = f"Classified as {severity.upper()} based on: "
reasons = []
if matched_keywords:
reasons.append(f"keywords: {', '.join(matched_keywords[:3])}")
if '%' in affected_users:
reasons.append(f"user impact: {affected_users}")
if not reasons:
reasons.append("default classification based on available information")
return explanation + "; ".join(reasons)
def format_json_output(result: Dict) -> str:
"""Format result as pretty JSON."""
return json.dumps(result, indent=2, ensure_ascii=False)
def format_text_output(result: Dict) -> str:
"""Format result as human-readable text."""
classification = result["classification"]
response = result["response"]
actions = result["initial_actions"]
communication = result["communication"]
output = []
output.append("=" * 60)
output.append("INCIDENT CLASSIFICATION REPORT")
output.append("=" * 60)
output.append("")
# Classification section
output.append("CLASSIFICATION:")
output.append(f" Severity: {classification['severity']}")
output.append(f" Confidence: {classification['confidence']:.1%}")
output.append(f" Reasoning: {classification['reasoning']}")
output.append(f" Timestamp: {classification['timestamp']}")
output.append("")
# Response section
output.append("RECOMMENDED RESPONSE:")
output.append(f" Primary Team: {response['primary_team']}")
if response['supporting_teams']:
output.append(f" Supporting Teams: {', '.join(response['supporting_teams'])}")
output.append(f" Response Time: {response['response_time_minutes']} minutes")
output.append("")
# Actions section
output.append("INITIAL ACTIONS:")
for i, action in enumerate(actions[:5], 1): # Show first 5 actions
output.append(f" {i}. {action['action']} (Priority {action['priority']})")
output.append(f" Timeout: {action['timeout_minutes']} minutes")
output.append(f" {action['description']}")
output.append("")
# Communication section
output.append("COMMUNICATION:")
output.append(f" Subject: {communication['subject']}")
output.append(f" Urgency: {communication['urgency'].upper()}")
output.append(f" Recipients: {', '.join(communication['recipients'])}")
output.append(f" Channels: {', '.join(communication['channels'])}")
if communication['frequency_minutes'] > 0:
output.append(f" Update Frequency: Every {communication['frequency_minutes']} minutes")
output.append("")
output.append("=" * 60)
return "\n".join(output)
def parse_input_text(text: str) -> Dict[str, Any]:
"""Parse free-form text input into structured incident data."""
# Basic parsing - in a real system, this would be more sophisticated
incident_data = {
"description": text.strip(),
"service": "unknown service",
"affected_users": "unknown",
"business_impact": "unknown"
}
# Try to extract service name
service_patterns = [
r'(?:service|api|database|server|application)\s+(\w+)',
r'(\w+)(?:\s+(?:is|has|service|api|database))',
r'(?:^|\s)(\w+)\s+(?:down|failed|broken)'
]
for pattern in service_patterns:
match = re.search(pattern, text.lower())
if match:
incident_data["service"] = match.group(1)
break
# Try to extract user impact
impact_patterns = [
r'(\d+%)\s+(?:of\s+)?(?:users?|customers?)',
r'(?:all|every|100%)\s+(?:users?|customers?)',
r'(?:some|many|several)\s+(?:users?|customers?)'
]
for pattern in impact_patterns:
match = re.search(pattern, text.lower())
if match:
incident_data["affected_users"] = match.group(1) if match.group(1) else match.group(0)
break
# Try to infer business impact
if any(word in text.lower() for word in ['critical', 'urgent', 'emergency', 'down', 'outage']):
incident_data["business_impact"] = "high"
elif any(word in text.lower() for word in ['slow', 'degraded', 'performance']):
incident_data["business_impact"] = "medium"
elif any(word in text.lower() for word in ['minor', 'cosmetic', 'small']):
incident_data["business_impact"] = "low"
return incident_data
def interactive_mode():
"""Run in interactive mode, prompting user for input."""
classifier = IncidentClassifier()
print("🚨 Incident Classifier - Interactive Mode")
print("=" * 50)
print("Enter incident details (or 'quit' to exit):")
print()
while True:
try:
description = input("Incident description: ").strip()
if description.lower() in ['quit', 'exit', 'q']:
break
if not description:
print("Please provide an incident description.")
continue
service = input("Affected service (optional): ").strip() or "unknown"
affected_users = input("Affected users (e.g., '50%', 'all users'): ").strip() or "unknown"
business_impact = input("Business impact (high/medium/low): ").strip() or "unknown"
incident_data = {
"description": description,
"service": service,
"affected_users": affected_users,
"business_impact": business_impact
}
result = classifier.classify_incident(incident_data)
print("\n" + "=" * 50)
print(format_text_output(result))
print("=" * 50)
print()
except KeyboardInterrupt:
print("\n\nExiting...")
break
except Exception as e:
print(f"Error: {e}")
def main():
"""Main function with argument parsing and execution."""
parser = argparse.ArgumentParser(
description="Classify incidents and provide response recommendations",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python incident_classifier.py --input incident.json
echo "Database is down" | python incident_classifier.py --format text
python incident_classifier.py --interactive
Input JSON format:
{
"description": "Database connection timeouts",
"service": "user-service",
"affected_users": "80%",
"business_impact": "high"
}
"""
)
parser.add_argument(
"--input", "-i",
help="Input file path (JSON format) or '-' for stdin"
)
parser.add_argument(
"--format", "-f",
choices=["json", "text"],
default="json",
help="Output format (default: json)"
)
parser.add_argument(
"--interactive",
action="store_true",
help="Run in interactive mode"
)
parser.add_argument(
"--output", "-o",
help="Output file path (default: stdout)"
)
args = parser.parse_args()
# Interactive mode
if args.interactive:
interactive_mode()
return
classifier = IncidentClassifier()
try:
# Read input
if args.input == "-" or (not args.input and not sys.stdin.isatty()):
# Read from stdin
input_text = sys.stdin.read().strip()
if not input_text:
parser.error("No input provided")
# Try to parse as JSON first, then as text
try:
incident_data = json.loads(input_text)
except json.JSONDecodeError:
incident_data = parse_input_text(input_text)
elif args.input:
# Read from file
with open(args.input, 'r') as f:
incident_data = json.load(f)
else:
parser.error("No input specified. Use --input, --interactive, or pipe data to stdin.")
# Validate required fields
if not isinstance(incident_data, dict):
parser.error("Input must be a JSON object")
if "description" not in incident_data:
parser.error("Input must contain 'description' field")
# Classify incident
result = classifier.classify_incident(incident_data)
# Format output
if args.format == "json":
output = format_json_output(result)
else:
output = format_text_output(result)
# Write output
if args.output:
with open(args.output, 'w') as f:
f.write(output)
f.write('\n')
else:
print(output)
except FileNotFoundError as e:
print(f"Error: File not found - {e}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON - {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
FILE:scripts/incident_timeline_builder.py
#!/usr/bin/env python3
"""
Incident Timeline Builder
Builds structured incident timelines with automatic phase detection, gap analysis,
communication template generation, and response metrics calculation. Produces
professional reports suitable for post-incident review and stakeholder briefing.
Usage:
python incident_timeline_builder.py incident_data.json
python incident_timeline_builder.py incident_data.json --format json
python incident_timeline_builder.py incident_data.json --format markdown
cat incident_data.json | python incident_timeline_builder.py --format text
"""
import argparse
import json
import sys
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Tuple
# ---------------------------------------------------------------------------
# Configuration Constants
# ---------------------------------------------------------------------------
ISO_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
EVENT_TYPES = [
"detection", "declaration", "escalation", "investigation",
"mitigation", "communication", "resolution", "action_item",
]
SEVERITY_LEVELS = {
"SEV1": {"label": "Critical", "rank": 1},
"SEV2": {"label": "Major", "rank": 2},
"SEV3": {"label": "Minor", "rank": 3},
"SEV4": {"label": "Low", "rank": 4},
}
PHASE_DEFINITIONS = [
{"name": "Detection", "trigger_types": ["detection"],
"description": "Issue detected via monitoring, alerting, or user report."},
{"name": "Triage", "trigger_types": ["declaration", "escalation"],
"description": "Incident declared, severity assessed, commander assigned."},
{"name": "Investigation", "trigger_types": ["investigation"],
"description": "Root cause analysis and impact assessment underway."},
{"name": "Mitigation", "trigger_types": ["mitigation"],
"description": "Active work to reduce or eliminate customer impact."},
{"name": "Resolution", "trigger_types": ["resolution"],
"description": "Service restored to normal operating parameters."},
]
GAP_THRESHOLD_MINUTES = 15
DECISION_EVENT_TYPES = {"escalation", "mitigation", "declaration", "resolution"}
# ---------------------------------------------------------------------------
# Data Model Classes
# ---------------------------------------------------------------------------
class IncidentEvent:
"""Represents a single event in the incident timeline."""
def __init__(self, data: Dict[str, Any]):
self.timestamp_raw: str = data.get("timestamp", "")
self.timestamp: Optional[datetime] = _parse_timestamp(self.timestamp_raw)
self.type: str = data.get("type", "unknown").lower().strip()
self.actor: str = data.get("actor", "unknown")
self.description: str = data.get("description", "")
self.metadata: Dict[str, Any] = data.get("metadata", {})
def to_dict(self) -> Dict[str, Any]:
result: Dict[str, Any] = {
"timestamp": self.timestamp_raw, "type": self.type,
"actor": self.actor, "description": self.description,
}
if self.metadata:
result["metadata"] = self.metadata
return result
@property
def is_decision_point(self) -> bool:
return self.type in DECISION_EVENT_TYPES
class IncidentPhase:
"""Represents a detected phase of the incident lifecycle."""
def __init__(self, name: str, description: str):
self.name: str = name
self.description: str = description
self.start_time: Optional[datetime] = None
self.end_time: Optional[datetime] = None
self.events: List[IncidentEvent] = []
@property
def duration_minutes(self) -> Optional[float]:
if self.start_time and self.end_time:
return (self.end_time - self.start_time).total_seconds() / 60.0
return None
def to_dict(self) -> Dict[str, Any]:
dur = self.duration_minutes
return {
"name": self.name, "description": self.description,
"start_time": self.start_time.strftime(ISO_FORMAT) if self.start_time else None,
"end_time": self.end_time.strftime(ISO_FORMAT) if self.end_time else None,
"duration_minutes": round(dur, 1) if dur is not None else None,
"event_count": len(self.events),
}
class CommunicationTemplate:
"""A generated communication message for a specific audience."""
def __init__(self, template_type: str, audience: str, subject: str, body: str):
self.template_type = template_type
self.audience = audience
self.subject = subject
self.body = body
def to_dict(self) -> Dict[str, Any]:
return {"template_type": self.template_type, "audience": self.audience,
"subject": self.subject, "body": self.body}
class TimelineGap:
"""Represents a gap in the timeline where no events were logged."""
def __init__(self, start: datetime, end: datetime, duration_minutes: float):
self.start = start
self.end = end
self.duration_minutes = duration_minutes
def to_dict(self) -> Dict[str, Any]:
return {"start": self.start.strftime(ISO_FORMAT),
"end": self.end.strftime(ISO_FORMAT),
"duration_minutes": round(self.duration_minutes, 1)}
class TimelineAnalysis:
"""Holds the complete analysis result for an incident timeline."""
def __init__(self):
self.incident_id: str = ""
self.incident_title: str = ""
self.severity: str = ""
self.status: str = ""
self.commander: str = ""
self.service: str = ""
self.affected_services: List[str] = []
self.declared_at: Optional[datetime] = None
self.resolved_at: Optional[datetime] = None
self.events: List[IncidentEvent] = []
self.phases: List[IncidentPhase] = []
self.gaps: List[TimelineGap] = []
self.decision_points: List[IncidentEvent] = []
self.metrics: Dict[str, Any] = {}
self.communications: List[CommunicationTemplate] = []
self.errors: List[str] = []
# ---------------------------------------------------------------------------
# Timestamp Helpers
# ---------------------------------------------------------------------------
def _parse_timestamp(raw: str) -> Optional[datetime]:
"""Parse an ISO-8601 timestamp string into a datetime object."""
if not raw:
return None
cleaned = raw.replace("Z", "+00:00") if raw.endswith("Z") else raw
try:
return datetime.fromisoformat(cleaned).replace(tzinfo=None)
except (ValueError, AttributeError):
pass
try:
return datetime.strptime(raw, ISO_FORMAT)
except ValueError:
return None
def _fmt_duration(minutes: Optional[float]) -> str:
"""Format a duration in minutes as a human-readable string."""
if minutes is None:
return "N/A"
if minutes < 1:
return f"{minutes * 60:.0f}s"
if minutes < 60:
return f"{minutes:.0f}m"
hours, remaining = int(minutes // 60), int(minutes % 60)
return f"{hours}h" if remaining == 0 else f"{hours}h {remaining}m"
def _fmt_ts(dt: Optional[datetime]) -> str:
"""Format a datetime as HH:MM:SS for display."""
return dt.strftime("%H:%M:%S") if dt else "??:??:??"
def _sev_label(sev: str) -> str:
"""Return the human label for a severity code."""
return SEVERITY_LEVELS.get(sev, {}).get("label", sev)
# ---------------------------------------------------------------------------
# Core Analysis Functions
# ---------------------------------------------------------------------------
def parse_incident_data(data: Dict[str, Any]) -> TimelineAnalysis:
"""Parse raw incident JSON into a TimelineAnalysis with populated fields."""
a = TimelineAnalysis()
inc = data.get("incident", {})
a.incident_id = inc.get("id", "UNKNOWN")
a.incident_title = inc.get("title", "Untitled Incident")
a.severity = inc.get("severity", "UNKNOWN").upper()
a.status = inc.get("status", "unknown").lower()
a.commander = inc.get("commander", "Unassigned")
a.service = inc.get("service", "unknown")
a.affected_services = inc.get("affected_services", [])
a.declared_at = _parse_timestamp(inc.get("declared_at", ""))
a.resolved_at = _parse_timestamp(inc.get("resolved_at", ""))
raw_events = data.get("events", [])
if not raw_events:
a.errors.append("No events found in incident data.")
return a
for raw in raw_events:
event = IncidentEvent(raw)
if event.timestamp is None:
a.errors.append(f"Skipping event with unparseable timestamp: {raw.get('timestamp', '')}")
continue
a.events.append(event)
a.events.sort(key=lambda e: e.timestamp) # type: ignore[arg-type]
return a
def detect_phases(analysis: TimelineAnalysis) -> None:
"""Detect incident lifecycle phases from the ordered event stream."""
if not analysis.events:
return
trigger_map: Dict[str, Dict[str, str]] = {}
for pdef in PHASE_DEFINITIONS:
for ttype in pdef["trigger_types"]:
trigger_map[ttype] = {"name": pdef["name"], "description": pdef["description"]}
phase_by_name: Dict[str, IncidentPhase] = {}
phase_order: List[str] = []
current: Optional[IncidentPhase] = None
for event in analysis.events:
pinfo = trigger_map.get(event.type)
if pinfo and pinfo["name"] not in phase_by_name:
if current is not None:
current.end_time = event.timestamp
phase = IncidentPhase(pinfo["name"], pinfo["description"])
phase.start_time = event.timestamp
phase_by_name[pinfo["name"]] = phase
phase_order.append(pinfo["name"])
current = phase
if current is not None:
current.events.append(event)
if current is not None:
current.end_time = analysis.resolved_at or analysis.events[-1].timestamp
analysis.phases = [phase_by_name[n] for n in phase_order]
def detect_gaps(analysis: TimelineAnalysis) -> None:
"""Identify gaps longer than GAP_THRESHOLD_MINUTES between consecutive events."""
for i in range(len(analysis.events) - 1):
ts_a, ts_b = analysis.events[i].timestamp, analysis.events[i + 1].timestamp
if ts_a is None or ts_b is None:
continue
delta = (ts_b - ts_a).total_seconds() / 60.0
if delta >= GAP_THRESHOLD_MINUTES:
analysis.gaps.append(TimelineGap(start=ts_a, end=ts_b, duration_minutes=delta))
def identify_decision_points(analysis: TimelineAnalysis) -> None:
"""Extract key decision-point events from the timeline."""
analysis.decision_points = [e for e in analysis.events if e.is_decision_point]
def calculate_metrics(analysis: TimelineAnalysis) -> None:
"""Calculate incident response metrics: MTTD, MTTR, phase durations."""
m: Dict[str, Any] = {}
det = [e for e in analysis.events if e.type == "detection"]
first_det = det[0].timestamp if det else None
first_ts = analysis.events[0].timestamp if analysis.events else None
# MTTD: first event to first detection.
if first_ts and first_det:
m["mttd_minutes"] = round((first_det - first_ts).total_seconds() / 60.0, 1)
else:
m["mttd_minutes"] = None
# MTTR: detection to resolution.
if first_det and analysis.resolved_at:
m["mttr_minutes"] = round((analysis.resolved_at - first_det).total_seconds() / 60.0, 1)
else:
m["mttr_minutes"] = None
# Total duration.
if analysis.declared_at and analysis.resolved_at:
m["total_duration_minutes"] = round(
(analysis.resolved_at - analysis.declared_at).total_seconds() / 60.0, 1)
else:
m["total_duration_minutes"] = None
# Phase durations.
m["phase_durations"] = {
p.name: (round(p.duration_minutes, 1) if p.duration_minutes is not None else None)
for p in analysis.phases
}
# Event counts by type.
tc: Dict[str, int] = {}
for e in analysis.events:
tc[e.type] = tc.get(e.type, 0) + 1
m["event_counts_by_type"] = tc
# Gap statistics.
m["gap_count"] = len(analysis.gaps)
if analysis.gaps:
gm = [g.duration_minutes for g in analysis.gaps]
m["longest_gap_minutes"] = round(max(gm), 1)
m["total_gap_minutes"] = round(sum(gm), 1)
else:
m["longest_gap_minutes"] = 0
m["total_gap_minutes"] = 0
m["total_events"] = len(analysis.events)
m["decision_point_count"] = len(analysis.decision_points)
m["phase_count"] = len(analysis.phases)
analysis.metrics = m
# ---------------------------------------------------------------------------
# Communication Template Generation
# ---------------------------------------------------------------------------
def generate_communications(analysis: TimelineAnalysis) -> None:
"""Generate four communication templates based on incident data."""
sev, sl = analysis.severity, _sev_label(analysis.severity)
title, svc = analysis.incident_title, analysis.service
affected = ", ".join(analysis.affected_services) or "none identified"
cmd, iid = analysis.commander, analysis.incident_id
decl = analysis.declared_at.strftime("%Y-%m-%d %H:%M UTC") if analysis.declared_at else "TBD"
resv = analysis.resolved_at.strftime("%Y-%m-%d %H:%M UTC") if analysis.resolved_at else "TBD"
dur = _fmt_duration(analysis.metrics.get("total_duration_minutes"))
resolved = analysis.status == "resolved"
# 1 -- Initial stakeholder notification
analysis.communications.append(CommunicationTemplate(
"initial_notification", "internal", f"[{sev}] Incident Declared: {title}",
f"An incident has been declared for {svc}.\n\n"
f"Incident ID: {iid}\nSeverity: {sev} ({sl})\nCommander: {cmd}\n"
f"Declared at: {decl}\nAffected services: {affected}\n\n"
f"The incident team is actively investigating. Updates will follow.",
))
# 2 -- Status page update
if resolved:
sp_subj = f"[Resolved] {title}"
sp_body = (f"The incident affecting {svc} has been resolved.\n\n"
f"Duration: {dur}\nAll affected services ({affected}) are restored. "
f"A post-incident review will be published within 48 hours.")
else:
sp_subj = f"[Investigating] {title}"
sp_body = (f"We are investigating degraded performance in {svc}. "
f"Affected services: {affected}.\n\n"
f"Our team is working to identify the root cause. Updates every 30 minutes.")
analysis.communications.append(CommunicationTemplate(
"status_page", "external", sp_subj, sp_body))
# 3 -- Executive summary
phase_lines = "\n".join(
f" - {p.name}: {_fmt_duration(p.duration_minutes)}" for p in analysis.phases
) or " No phase data available."
mttd = _fmt_duration(analysis.metrics.get("mttd_minutes"))
mttr = _fmt_duration(analysis.metrics.get("mttr_minutes"))
analysis.communications.append(CommunicationTemplate(
"executive_summary", "executive", f"Executive Summary: {iid} - {title}",
f"Incident: {iid} - {title}\nSeverity: {sev} ({sl})\n"
f"Service: {svc}\nCommander: {cmd}\nStatus: {analysis.status.capitalize()}\n"
f"Declared: {decl}\nResolved: {resv}\nDuration: {dur}\n\n"
f"Key Metrics:\n - MTTD: {mttd}\n - MTTR: {mttr}\n"
f" - Timeline Gaps: {analysis.metrics.get('gap_count', 0)}\n\n"
f"Phase Breakdown:\n{phase_lines}\n\nAffected Services: {affected}",
))
# 4 -- Customer notification
if resolved:
cust_body = (f"We experienced an issue affecting {svc} starting at {decl}.\n\n"
f"The issue was resolved at {resv} (duration: {dur}). "
f"We apologize for any inconvenience and are reviewing to prevent recurrence.")
else:
cust_body = (f"We are experiencing an issue affecting {svc} starting at {decl}.\n\n"
f"Our engineering team is actively working to resolve this. "
f"We will provide updates as the situation develops. We apologize for the inconvenience.")
analysis.communications.append(CommunicationTemplate(
"customer_notification", "external", f"Service Update: {title}", cust_body))
# ---------------------------------------------------------------------------
# Main Analysis Orchestrator
# ---------------------------------------------------------------------------
def build_timeline(data: Dict[str, Any]) -> TimelineAnalysis:
"""Run the full timeline analysis pipeline on raw incident data."""
analysis = parse_incident_data(data)
if analysis.errors and not analysis.events:
return analysis
detect_phases(analysis)
detect_gaps(analysis)
identify_decision_points(analysis)
calculate_metrics(analysis)
generate_communications(analysis)
return analysis
# ---------------------------------------------------------------------------
# Output Formatters
# ---------------------------------------------------------------------------
def format_text_output(analysis: TimelineAnalysis) -> str:
"""Format the analysis as a human-readable text report."""
L: List[str] = []
w = 64
L.append("=" * w)
L.append("INCIDENT TIMELINE REPORT")
L.append("=" * w)
L.append("")
if analysis.errors:
for err in analysis.errors:
L.append(f" WARNING: {err}")
L.append("")
if not analysis.events:
return "\n".join(L)
# Summary
L.append("INCIDENT SUMMARY")
L.append("-" * 32)
L.append(f" ID: {analysis.incident_id}")
L.append(f" Title: {analysis.incident_title}")
L.append(f" Severity: {analysis.severity}")
L.append(f" Status: {analysis.status.capitalize()}")
L.append(f" Commander: {analysis.commander}")
L.append(f" Service: {analysis.service}")
if analysis.affected_services:
L.append(f" Affected: {', '.join(analysis.affected_services)}")
L.append(f" Duration: {_fmt_duration(analysis.metrics.get('total_duration_minutes'))}")
L.append("")
# Key metrics
L.append("KEY METRICS")
L.append("-" * 32)
L.append(f" MTTD (Mean Time to Detect): {_fmt_duration(analysis.metrics.get('mttd_minutes'))}")
L.append(f" MTTR (Mean Time to Resolve): {_fmt_duration(analysis.metrics.get('mttr_minutes'))}")
L.append(f" Total Events: {analysis.metrics.get('total_events', 0)}")
L.append(f" Decision Points: {analysis.metrics.get('decision_point_count', 0)}")
L.append(f" Timeline Gaps (>{GAP_THRESHOLD_MINUTES}m): {analysis.metrics.get('gap_count', 0)}")
L.append("")
# Phases
L.append("INCIDENT PHASES")
L.append("-" * 32)
if analysis.phases:
for p in analysis.phases:
L.append(f" [{_fmt_ts(p.start_time)} - {_fmt_ts(p.end_time)}] {p.name} ({_fmt_duration(p.duration_minutes)})")
L.append(f" {p.description}")
L.append(f" Events: {len(p.events)}")
else:
L.append(" No phases detected.")
L.append("")
# Chronological timeline
L.append("CHRONOLOGICAL TIMELINE")
L.append("-" * 32)
for e in analysis.events:
marker = "*" if e.is_decision_point else " "
L.append(f" {_fmt_ts(e.timestamp)} {marker} [{e.type.upper():13s}] {e.actor}")
L.append(f" {e.description}")
L.append("")
L.append(" (* = key decision point)")
L.append("")
# Gap warnings
if analysis.gaps:
L.append("GAP ANALYSIS")
L.append("-" * 32)
for g in analysis.gaps:
L.append(f" WARNING: {_fmt_duration(g.duration_minutes)} gap between {_fmt_ts(g.start)} and {_fmt_ts(g.end)}")
L.append("")
# Decision points
if analysis.decision_points:
L.append("KEY DECISION POINTS")
L.append("-" * 32)
for dp in analysis.decision_points:
L.append(f" {_fmt_ts(dp.timestamp)} [{dp.type.upper()}] {dp.description}")
L.append("")
# Communications
if analysis.communications:
L.append("GENERATED COMMUNICATIONS")
L.append("-" * 32)
for c in analysis.communications:
L.append(f" Type: {c.template_type}")
L.append(f" Audience: {c.audience}")
L.append(f" Subject: {c.subject}")
L.append(" ---")
for bl in c.body.split("\n"):
L.append(f" {bl}")
L.append("")
L.append("=" * w)
L.append("END OF REPORT")
L.append("=" * w)
return "\n".join(L)
def format_json_output(analysis: TimelineAnalysis) -> Dict[str, Any]:
"""Format the analysis as a structured JSON-serializable dictionary."""
return {
"incident": {
"id": analysis.incident_id, "title": analysis.incident_title,
"severity": analysis.severity, "status": analysis.status,
"commander": analysis.commander, "service": analysis.service,
"affected_services": analysis.affected_services,
"declared_at": analysis.declared_at.strftime(ISO_FORMAT) if analysis.declared_at else None,
"resolved_at": analysis.resolved_at.strftime(ISO_FORMAT) if analysis.resolved_at else None,
},
"timeline": [e.to_dict() for e in analysis.events],
"phases": [p.to_dict() for p in analysis.phases],
"gaps": [g.to_dict() for g in analysis.gaps],
"decision_points": [e.to_dict() for e in analysis.decision_points],
"metrics": analysis.metrics,
"communications": [c.to_dict() for c in analysis.communications],
"errors": analysis.errors if analysis.errors else [],
}
def format_markdown_output(analysis: TimelineAnalysis) -> str:
"""Format the analysis as a professional Markdown report."""
L: List[str] = []
L.append(f"# Incident Timeline Report: {analysis.incident_id}")
L.append("")
if analysis.errors:
L.append("> **Warnings:**")
for err in analysis.errors:
L.append(f"> - {err}")
L.append("")
if not analysis.events:
return "\n".join(L)
# Summary table
L.append("## Incident Summary")
L.append("")
L.append("| Field | Value |")
L.append("|-------|-------|")
L.append(f"| **ID** | {analysis.incident_id} |")
L.append(f"| **Title** | {analysis.incident_title} |")
L.append(f"| **Severity** | {analysis.severity} ({_sev_label(analysis.severity)}) |")
L.append(f"| **Status** | {analysis.status.capitalize()} |")
L.append(f"| **Commander** | {analysis.commander} |")
L.append(f"| **Service** | {analysis.service} |")
if analysis.affected_services:
L.append(f"| **Affected Services** | {', '.join(analysis.affected_services)} |")
L.append(f"| **Duration** | {_fmt_duration(analysis.metrics.get('total_duration_minutes'))} |")
L.append("")
# Key metrics
L.append("## Key Metrics")
L.append("")
L.append(f"- **MTTD (Mean Time to Detect):** {_fmt_duration(analysis.metrics.get('mttd_minutes'))}")
L.append(f"- **MTTR (Mean Time to Resolve):** {_fmt_duration(analysis.metrics.get('mttr_minutes'))}")
L.append(f"- **Total Events:** {analysis.metrics.get('total_events', 0)}")
L.append(f"- **Decision Points:** {analysis.metrics.get('decision_point_count', 0)}")
L.append(f"- **Timeline Gaps (>{GAP_THRESHOLD_MINUTES}m):** {analysis.metrics.get('gap_count', 0)}")
if analysis.metrics.get("longest_gap_minutes", 0) > 0:
L.append(f"- **Longest Gap:** {_fmt_duration(analysis.metrics.get('longest_gap_minutes'))}")
L.append("")
# Phases table
L.append("## Incident Phases")
L.append("")
if analysis.phases:
L.append("| Phase | Start | End | Duration | Events |")
L.append("|-------|-------|-----|----------|--------|")
for p in analysis.phases:
L.append(f"| {p.name} | {_fmt_ts(p.start_time)} | {_fmt_ts(p.end_time)} | {_fmt_duration(p.duration_minutes)} | {len(p.events)} |")
L.append("")
# ASCII bar chart
max_dur = max((p.duration_minutes for p in analysis.phases if p.duration_minutes), default=0)
if max_dur and max_dur > 0:
L.append("### Phase Duration Distribution")
L.append("")
L.append("```")
for p in analysis.phases:
d = p.duration_minutes or 0
bar = "#" * int((d / max_dur) * 40)
L.append(f" {p.name:15s} |{bar} {_fmt_duration(d)}")
L.append("```")
L.append("")
else:
L.append("No phases detected.")
L.append("")
# Chronological timeline
L.append("## Chronological Timeline")
L.append("")
for e in analysis.events:
dm = " **[KEY DECISION]**" if e.is_decision_point else ""
L.append(f"- `{_fmt_ts(e.timestamp)}` **{e.type.upper()}** ({e.actor}){dm}")
L.append(f" - {e.description}")
L.append("")
# Gap analysis
if analysis.gaps:
L.append("## Gap Analysis")
L.append("")
L.append(f"> {len(analysis.gaps)} gap(s) of >{GAP_THRESHOLD_MINUTES} minutes detected. "
f"These may represent blind spots where important activity was not recorded.")
L.append("")
for g in analysis.gaps:
L.append(f"- **{_fmt_duration(g.duration_minutes)}** gap from `{_fmt_ts(g.start)}` to `{_fmt_ts(g.end)}`")
L.append("")
# Decision points
if analysis.decision_points:
L.append("## Key Decision Points")
L.append("")
for dp in analysis.decision_points:
L.append(f"1. `{_fmt_ts(dp.timestamp)}` **{dp.type.upper()}** - {dp.description}")
L.append("")
# Communications
if analysis.communications:
L.append("## Generated Communications")
L.append("")
for c in analysis.communications:
L.append(f"### {c.template_type.replace('_', ' ').title()} ({c.audience})")
L.append("")
L.append(f"**Subject:** {c.subject}")
L.append("")
for bl in c.body.split("\n"):
L.append(bl)
L.append("")
L.append("---")
L.append("")
# Event type breakdown
tc = analysis.metrics.get("event_counts_by_type", {})
if tc:
L.append("## Event Type Breakdown")
L.append("")
L.append("| Type | Count |")
L.append("|------|-------|")
for etype, count in sorted(tc.items(), key=lambda x: -x[1]):
L.append(f"| {etype} | {count} |")
L.append("")
L.append("---")
L.append(f"*Report generated for incident {analysis.incident_id}. All timestamps in UTC.*")
return "\n".join(L)
# ---------------------------------------------------------------------------
# CLI Interface
# ---------------------------------------------------------------------------
def main() -> int:
"""Main CLI entry point."""
parser = argparse.ArgumentParser(
description="Build structured incident timelines with phase detection and communication templates."
)
parser.add_argument(
"data_file", nargs="?", default=None,
help="JSON file with incident data (reads stdin if omitted)",
)
parser.add_argument(
"--format", choices=["text", "json", "markdown"], default="text",
help="Output format (default: text)",
)
args = parser.parse_args()
try:
if args.data_file:
try:
with open(args.data_file, "r") as f:
raw_data = json.load(f)
except FileNotFoundError:
print(f"Error: File '{args.data_file}' not found.", file=sys.stderr)
return 1
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in '{args.data_file}': {e}", file=sys.stderr)
return 1
else:
if sys.stdin.isatty():
print("Error: No input file specified and stdin is a terminal. "
"Provide a file argument or pipe JSON to stdin.", file=sys.stderr)
return 1
try:
raw_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON on stdin: {e}", file=sys.stderr)
return 1
if not isinstance(raw_data, dict):
print("Error: Input must be a JSON object.", file=sys.stderr)
return 1
if "incident" not in raw_data and "events" not in raw_data:
print("Error: Input must contain at least 'incident' or 'events' keys.", file=sys.stderr)
return 1
analysis = build_timeline(raw_data)
if args.format == "json":
print(json.dumps(format_json_output(analysis), indent=2))
elif args.format == "markdown":
print(format_markdown_output(analysis))
else:
print(format_text_output(analysis))
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
FILE:scripts/pir_generator.py
#!/usr/bin/env python3
"""
PIR (Post-Incident Review) Generator
Generates comprehensive Post-Incident Review documents from incident data, timelines,
and actions taken. Applies multiple RCA frameworks including 5 Whys, Fishbone diagram,
and Timeline analysis.
This tool creates structured PIR documents with root cause analysis, lessons learned,
action items, and follow-up recommendations.
Usage:
python pir_generator.py --incident incident.json --timeline timeline.json --output pir.md
python pir_generator.py --incident incident.json --rca-method fishbone --action-items
cat incident.json | python pir_generator.py --format markdown
"""
import argparse
import json
import sys
import re
from datetime import datetime, timezone, timedelta
from typing import Dict, List, Optional, Any, Tuple
from collections import defaultdict, Counter
class PIRGenerator:
"""
Generates comprehensive Post-Incident Review documents with multiple
RCA frameworks, lessons learned, and actionable follow-up items.
"""
def __init__(self):
"""Initialize the PIR generator with templates and frameworks."""
self.rca_frameworks = self._load_rca_frameworks()
self.pir_templates = self._load_pir_templates()
self.severity_guidelines = self._load_severity_guidelines()
self.action_item_types = self._load_action_item_types()
self.lessons_learned_categories = self._load_lessons_learned_categories()
def _load_rca_frameworks(self) -> Dict[str, Dict]:
"""Load root cause analysis framework definitions."""
return {
"five_whys": {
"name": "5 Whys Analysis",
"description": "Iterative questioning technique to explore cause-and-effect relationships",
"steps": [
"State the problem clearly",
"Ask why the problem occurred",
"For each answer, ask why again",
"Continue until root cause is identified",
"Verify the root cause addresses the original problem"
],
"min_iterations": 3,
"max_iterations": 7
},
"fishbone": {
"name": "Fishbone (Ishikawa) Diagram",
"description": "Systematic analysis across multiple categories of potential causes",
"categories": [
{
"name": "People",
"description": "Human factors, training, communication, experience",
"examples": ["Training gaps", "Communication failures", "Skill deficits", "Staffing issues"]
},
{
"name": "Process",
"description": "Procedures, workflows, change management, review processes",
"examples": ["Missing procedures", "Inadequate reviews", "Change management gaps", "Documentation issues"]
},
{
"name": "Technology",
"description": "Systems, tools, architecture, automation",
"examples": ["Architecture limitations", "Tool deficiencies", "Automation gaps", "Infrastructure issues"]
},
{
"name": "Environment",
"description": "External factors, dependencies, infrastructure",
"examples": ["Third-party dependencies", "Network issues", "Hardware failures", "External service outages"]
}
]
},
"timeline": {
"name": "Timeline Analysis",
"description": "Chronological analysis of events to identify decision points and missed opportunities",
"focus_areas": [
"Detection timing and effectiveness",
"Response time and escalation paths",
"Decision points and alternative paths",
"Communication effectiveness",
"Mitigation strategy effectiveness"
]
},
"bow_tie": {
"name": "Bow Tie Analysis",
"description": "Analysis of both preventive and protective measures around an incident",
"components": [
"Hazards (what could go wrong)",
"Top events (what actually went wrong)",
"Threats (what caused it)",
"Consequences (what was the impact)",
"Barriers (what preventive/protective measures exist or could exist)"
]
}
}
def _load_pir_templates(self) -> Dict[str, str]:
"""Load PIR document templates for different severity levels."""
return {
"comprehensive": """# Post-Incident Review: {incident_title}
## Executive Summary
{executive_summary}
## Incident Overview
- **Incident ID:** {incident_id}
- **Date & Time:** {incident_date}
- **Duration:** {duration}
- **Severity:** {severity}
- **Status:** {status}
- **Incident Commander:** {incident_commander}
- **Responders:** {responders}
### Customer Impact
{customer_impact}
### Business Impact
{business_impact}
## Timeline
{timeline_section}
## Root Cause Analysis
{rca_section}
## What Went Well
{what_went_well}
## What Didn't Go Well
{what_went_wrong}
## Lessons Learned
{lessons_learned}
## Action Items
{action_items}
## Follow-up and Prevention
{prevention_measures}
## Appendix
{appendix_section}
---
*Generated on {generation_date} by PIR Generator*
""",
"standard": """# Post-Incident Review: {incident_title}
## Summary
{executive_summary}
## Incident Details
- **Date:** {incident_date}
- **Duration:** {duration}
- **Severity:** {severity}
- **Impact:** {customer_impact}
## Timeline
{timeline_section}
## Root Cause
{rca_section}
## Action Items
{action_items}
## Lessons Learned
{lessons_learned}
---
*Generated on {generation_date}*
""",
"brief": """# Incident Review: {incident_title}
**Date:** {incident_date} | **Duration:** {duration} | **Severity:** {severity}
## What Happened
{executive_summary}
## Root Cause
{rca_section}
## Actions
{action_items}
---
*{generation_date}*
"""
}
def _load_severity_guidelines(self) -> Dict[str, Dict]:
"""Load severity-specific PIR guidelines."""
return {
"sev1": {
"required_sections": ["executive_summary", "timeline", "rca", "action_items", "lessons_learned"],
"required_attendees": ["incident_commander", "technical_leads", "engineering_manager", "product_manager"],
"timeline_requirement": "Complete timeline with 15-minute intervals",
"rca_methods": ["five_whys", "fishbone", "timeline"],
"review_deadline_hours": 24,
"follow_up_weeks": 4
},
"sev2": {
"required_sections": ["summary", "timeline", "rca", "action_items"],
"required_attendees": ["incident_commander", "technical_leads", "team_lead"],
"timeline_requirement": "Key milestone timeline",
"rca_methods": ["five_whys", "timeline"],
"review_deadline_hours": 72,
"follow_up_weeks": 2
},
"sev3": {
"required_sections": ["summary", "rca", "action_items"],
"required_attendees": ["technical_lead", "team_member"],
"timeline_requirement": "Basic timeline",
"rca_methods": ["five_whys"],
"review_deadline_hours": 168, # 1 week
"follow_up_weeks": 1
},
"sev4": {
"required_sections": ["summary", "action_items"],
"required_attendees": ["assigned_engineer"],
"timeline_requirement": "Optional",
"rca_methods": ["brief_analysis"],
"review_deadline_hours": 336, # 2 weeks
"follow_up_weeks": 0
}
}
def _load_action_item_types(self) -> Dict[str, Dict]:
"""Load action item categorization and templates."""
return {
"immediate_fix": {
"priority": "P0",
"timeline": "24-48 hours",
"description": "Critical bugs or security issues that need immediate attention",
"template": "Fix {issue_description} to prevent recurrence of {incident_type}",
"owners": ["engineer", "team_lead"]
},
"process_improvement": {
"priority": "P1",
"timeline": "1-2 weeks",
"description": "Process gaps or communication issues identified",
"template": "Improve {process_area} to address {gap_description}",
"owners": ["team_lead", "process_owner"]
},
"monitoring_alerting": {
"priority": "P1",
"timeline": "1 week",
"description": "Missing monitoring or alerting capabilities",
"template": "Implement {monitoring_type} for {system_component}",
"owners": ["sre", "engineer"]
},
"documentation": {
"priority": "P2",
"timeline": "2-3 weeks",
"description": "Documentation gaps or runbook updates",
"template": "Update {documentation_type} to include {missing_information}",
"owners": ["technical_writer", "engineer"]
},
"training": {
"priority": "P2",
"timeline": "1 month",
"description": "Training needs or knowledge gaps",
"template": "Provide {training_type} training on {topic}",
"owners": ["training_coordinator", "subject_matter_expert"]
},
"architectural": {
"priority": "P1-P3",
"timeline": "1-3 months",
"description": "System design or architecture improvements",
"template": "Redesign {system_component} to improve {quality_attribute}",
"owners": ["architect", "engineering_manager"]
},
"tooling": {
"priority": "P2",
"timeline": "2-4 weeks",
"description": "Tool improvements or new tool requirements",
"template": "Implement {tool_type} to support {use_case}",
"owners": ["devops", "engineer"]
}
}
def _load_lessons_learned_categories(self) -> Dict[str, List[str]]:
"""Load categories for organizing lessons learned."""
return {
"detection_and_monitoring": [
"Monitoring gaps identified",
"Alert fatigue issues",
"Detection timing improvements",
"Observability enhancements"
],
"response_and_escalation": [
"Response time improvements",
"Escalation path optimization",
"Communication effectiveness",
"Resource allocation lessons"
],
"technical_systems": [
"Architecture resilience",
"Failure mode analysis",
"Performance bottlenecks",
"Dependency management"
],
"process_and_procedures": [
"Runbook effectiveness",
"Change management gaps",
"Review process improvements",
"Documentation quality"
],
"team_and_culture": [
"Training needs identified",
"Cross-team collaboration",
"Knowledge sharing gaps",
"Decision-making processes"
]
}
def generate_pir(self, incident_data: Dict[str, Any], timeline_data: Optional[Dict] = None,
rca_method: str = "five_whys", template_type: str = "comprehensive") -> Dict[str, Any]:
"""
Generate a comprehensive PIR document from incident data.
Args:
incident_data: Core incident information
timeline_data: Optional timeline reconstruction data
rca_method: RCA framework to use
template_type: PIR template type (comprehensive, standard, brief)
Returns:
Dictionary containing PIR document and metadata
"""
# Extract incident information
incident_info = self._extract_incident_info(incident_data)
# Generate root cause analysis
rca_results = self._perform_rca(incident_data, timeline_data, rca_method)
# Generate lessons learned
lessons_learned = self._generate_lessons_learned(incident_data, timeline_data, rca_results)
# Generate action items
action_items = self._generate_action_items(incident_data, rca_results, lessons_learned)
# Create timeline section
timeline_section = self._create_timeline_section(timeline_data, incident_info["severity"])
# Generate document sections
sections = self._generate_document_sections(
incident_info, rca_results, lessons_learned, action_items, timeline_section
)
# Build final document
template = self.pir_templates[template_type]
pir_document = template.format(**sections)
# Generate metadata
metadata = self._generate_metadata(incident_info, rca_results, action_items)
return {
"pir_document": pir_document,
"metadata": metadata,
"incident_info": incident_info,
"rca_results": rca_results,
"lessons_learned": lessons_learned,
"action_items": action_items,
"generation_timestamp": datetime.now(timezone.utc).isoformat()
}
def _extract_incident_info(self, incident_data: Dict) -> Dict[str, Any]:
"""Extract and normalize incident information."""
return {
"incident_id": incident_data.get("incident_id", "INC-" + datetime.now().strftime("%Y%m%d-%H%M")),
"title": incident_data.get("title", incident_data.get("description", "Incident")[:50]),
"description": incident_data.get("description", "No description provided"),
"severity": incident_data.get("severity", "unknown").lower(),
"start_time": self._parse_timestamp(incident_data.get("start_time", incident_data.get("timestamp", ""))),
"end_time": self._parse_timestamp(incident_data.get("end_time", "")),
"duration": self._calculate_duration(incident_data),
"affected_services": incident_data.get("affected_services", []),
"customer_impact": incident_data.get("customer_impact", "Unknown impact"),
"business_impact": incident_data.get("business_impact", "Unknown business impact"),
"incident_commander": incident_data.get("incident_commander", "TBD"),
"responders": incident_data.get("responders", []),
"status": incident_data.get("status", "resolved")
}
def _parse_timestamp(self, timestamp_str: str) -> Optional[datetime]:
"""Parse timestamp string to datetime object."""
if not timestamp_str:
return None
formats = [
"%Y-%m-%dT%H:%M:%S.%fZ",
"%Y-%m-%dT%H:%M:%SZ",
"%Y-%m-%d %H:%M:%S",
"%m/%d/%Y %H:%M:%S"
]
for fmt in formats:
try:
dt = datetime.strptime(timestamp_str, fmt)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
except ValueError:
continue
return None
def _calculate_duration(self, incident_data: Dict) -> str:
"""Calculate incident duration in human-readable format."""
start_time = self._parse_timestamp(incident_data.get("start_time", ""))
end_time = self._parse_timestamp(incident_data.get("end_time", ""))
if start_time and end_time:
duration = end_time - start_time
total_minutes = int(duration.total_seconds() / 60)
if total_minutes < 60:
return f"{total_minutes} minutes"
elif total_minutes < 1440: # Less than 24 hours
hours = total_minutes // 60
minutes = total_minutes % 60
return f"{hours}h {minutes}m"
else:
days = total_minutes // 1440
hours = (total_minutes % 1440) // 60
return f"{days}d {hours}h"
return incident_data.get("duration", "Unknown duration")
def _perform_rca(self, incident_data: Dict, timeline_data: Optional[Dict], method: str) -> Dict[str, Any]:
"""Perform root cause analysis using specified method."""
if method == "five_whys":
return self._five_whys_analysis(incident_data, timeline_data)
elif method == "fishbone":
return self._fishbone_analysis(incident_data, timeline_data)
elif method == "timeline":
return self._timeline_analysis(incident_data, timeline_data)
elif method == "bow_tie":
return self._bow_tie_analysis(incident_data, timeline_data)
else:
return self._five_whys_analysis(incident_data, timeline_data) # Default
def _five_whys_analysis(self, incident_data: Dict, timeline_data: Optional[Dict]) -> Dict[str, Any]:
"""Perform 5 Whys root cause analysis."""
problem_statement = incident_data.get("description", "Incident occurred")
# Generate why questions based on incident data
whys = []
current_issue = problem_statement
# Generate systematic why questions
why_patterns = [
f"Why did {current_issue}?",
"Why wasn't this detected earlier?",
"Why didn't existing safeguards prevent this?",
"Why wasn't there a backup mechanism?",
"Why wasn't this scenario anticipated?"
]
# Try to infer answers from incident data
potential_answers = self._infer_why_answers(incident_data, timeline_data)
for i, why_question in enumerate(why_patterns):
answer = potential_answers[i] if i < len(potential_answers) else "Further investigation needed"
whys.append({
"question": why_question,
"answer": answer,
"evidence": self._find_supporting_evidence(answer, incident_data, timeline_data)
})
# Identify root causes from the analysis
root_causes = self._extract_root_causes(whys)
return {
"method": "five_whys",
"problem_statement": problem_statement,
"why_analysis": whys,
"root_causes": root_causes,
"confidence": self._calculate_rca_confidence(whys, incident_data)
}
def _fishbone_analysis(self, incident_data: Dict, timeline_data: Optional[Dict]) -> Dict[str, Any]:
"""Perform Fishbone (Ishikawa) diagram analysis."""
problem_statement = incident_data.get("description", "Incident occurred")
# Analyze each category
categories = {}
for category_info in self.rca_frameworks["fishbone"]["categories"]:
category_name = category_info["name"]
contributing_factors = self._identify_category_factors(
category_name, incident_data, timeline_data
)
categories[category_name] = {
"description": category_info["description"],
"factors": contributing_factors,
"examples": category_info["examples"]
}
# Identify primary contributing factors
primary_factors = self._identify_primary_factors(categories)
# Generate root cause hypothesis
root_causes = self._synthesize_fishbone_root_causes(categories, primary_factors)
return {
"method": "fishbone",
"problem_statement": problem_statement,
"categories": categories,
"primary_factors": primary_factors,
"root_causes": root_causes,
"confidence": self._calculate_rca_confidence(categories, incident_data)
}
def _timeline_analysis(self, incident_data: Dict, timeline_data: Optional[Dict]) -> Dict[str, Any]:
"""Perform timeline-based root cause analysis."""
if not timeline_data:
return {"method": "timeline", "error": "No timeline data provided"}
# Extract key decision points
decision_points = self._extract_decision_points(timeline_data)
# Identify missed opportunities
missed_opportunities = self._identify_missed_opportunities(timeline_data)
# Analyze response effectiveness
response_analysis = self._analyze_response_effectiveness(timeline_data)
# Generate timeline-based root causes
root_causes = self._extract_timeline_root_causes(
decision_points, missed_opportunities, response_analysis
)
return {
"method": "timeline",
"decision_points": decision_points,
"missed_opportunities": missed_opportunities,
"response_analysis": response_analysis,
"root_causes": root_causes,
"confidence": self._calculate_rca_confidence(timeline_data, incident_data)
}
def _bow_tie_analysis(self, incident_data: Dict, timeline_data: Optional[Dict]) -> Dict[str, Any]:
"""Perform Bow Tie analysis."""
# Identify the top event (what went wrong)
top_event = incident_data.get("description", "Service failure")
# Identify threats (what caused it)
threats = self._identify_threats(incident_data, timeline_data)
# Identify consequences (impact)
consequences = self._identify_consequences(incident_data)
# Identify existing barriers
existing_barriers = self._identify_existing_barriers(incident_data, timeline_data)
# Recommend additional barriers
recommended_barriers = self._recommend_additional_barriers(threats, consequences)
return {
"method": "bow_tie",
"top_event": top_event,
"threats": threats,
"consequences": consequences,
"existing_barriers": existing_barriers,
"recommended_barriers": recommended_barriers,
"confidence": self._calculate_rca_confidence(threats, incident_data)
}
def _infer_why_answers(self, incident_data: Dict, timeline_data: Optional[Dict]) -> List[str]:
"""Infer potential answers to why questions from available data."""
answers = []
# Look for clues in incident description
description = incident_data.get("description", "").lower()
# Common patterns and their inferred answers
if "database" in description and ("timeout" in description or "slow" in description):
answers.append("Database connection pool was exhausted")
answers.append("Connection pool configuration was insufficient for peak load")
answers.append("Load testing didn't include realistic database scenarios")
elif "deployment" in description or "release" in description:
answers.append("New deployment introduced a regression")
answers.append("Code review process missed the issue")
answers.append("Testing environment didn't match production")
elif "network" in description or "connectivity" in description:
answers.append("Network infrastructure had unexpected load")
answers.append("Network monitoring wasn't comprehensive enough")
answers.append("Redundancy mechanisms failed simultaneously")
else:
# Generic answers based on common root causes
answers.extend([
"System couldn't handle the load/request volume",
"Monitoring didn't detect the issue early enough",
"Error handling mechanisms were insufficient",
"Dependencies failed without proper circuit breakers",
"System lacked sufficient redundancy/resilience"
])
return answers[:5] # Return up to 5 answers
def _find_supporting_evidence(self, answer: str, incident_data: Dict, timeline_data: Optional[Dict]) -> List[str]:
"""Find supporting evidence for RCA answers."""
evidence = []
# Look for supporting information in incident data
if timeline_data and "timeline" in timeline_data:
events = timeline_data["timeline"].get("events", [])
for event in events:
event_message = event.get("message", "").lower()
if any(keyword in event_message for keyword in answer.lower().split()):
evidence.append(f"Timeline event: {event['message']}")
# Check incident metadata for supporting info
metadata = incident_data.get("metadata", {})
for key, value in metadata.items():
if isinstance(value, str) and any(keyword in value.lower() for keyword in answer.lower().split()):
evidence.append(f"Incident metadata: {key} = {value}")
return evidence[:3] # Return top 3 pieces of evidence
def _extract_root_causes(self, whys: List[Dict]) -> List[Dict]:
"""Extract root causes from 5 Whys analysis."""
root_causes = []
# The deepest "why" answers are typically closest to root causes
if len(whys) >= 3:
for i, why in enumerate(whys[-2:]): # Look at last 2 whys
if "further investigation needed" not in why["answer"].lower():
root_causes.append({
"cause": why["answer"],
"category": self._categorize_root_cause(why["answer"]),
"evidence": why["evidence"],
"confidence": "high" if len(why["evidence"]) > 1 else "medium"
})
return root_causes
def _categorize_root_cause(self, cause: str) -> str:
"""Categorize a root cause into standard categories."""
cause_lower = cause.lower()
if any(keyword in cause_lower for keyword in ["process", "procedure", "review", "change management"]):
return "Process"
elif any(keyword in cause_lower for keyword in ["training", "knowledge", "skill", "experience"]):
return "People"
elif any(keyword in cause_lower for keyword in ["system", "architecture", "code", "configuration"]):
return "Technology"
elif any(keyword in cause_lower for keyword in ["network", "infrastructure", "dependency", "third-party"]):
return "Environment"
else:
return "Unknown"
def _identify_category_factors(self, category: str, incident_data: Dict, timeline_data: Optional[Dict]) -> List[Dict]:
"""Identify contributing factors for a Fishbone category."""
factors = []
description = incident_data.get("description", "").lower()
if category == "People":
if "misconfigured" in description or "human error" in description:
factors.append({"factor": "Configuration error", "likelihood": "high"})
if timeline_data and self._has_delayed_response(timeline_data):
factors.append({"factor": "Delayed incident response", "likelihood": "medium"})
elif category == "Process":
if "deployment" in description:
factors.append({"factor": "Insufficient deployment validation", "likelihood": "high"})
if "code review" in incident_data.get("context", "").lower():
factors.append({"factor": "Code review process gaps", "likelihood": "medium"})
elif category == "Technology":
if "database" in description:
factors.append({"factor": "Database performance limitations", "likelihood": "high"})
if "timeout" in description or "latency" in description:
factors.append({"factor": "System performance bottlenecks", "likelihood": "high"})
elif category == "Environment":
if "network" in description:
factors.append({"factor": "Network infrastructure issues", "likelihood": "medium"})
if "third-party" in description or "external" in description:
factors.append({"factor": "External service dependencies", "likelihood": "medium"})
return factors
def _identify_primary_factors(self, categories: Dict) -> List[Dict]:
"""Identify primary contributing factors across all categories."""
primary_factors = []
for category_name, category_data in categories.items():
high_likelihood_factors = [
f for f in category_data["factors"]
if f.get("likelihood") == "high"
]
primary_factors.extend([
{**factor, "category": category_name}
for factor in high_likelihood_factors
])
return primary_factors
def _synthesize_fishbone_root_causes(self, categories: Dict, primary_factors: List[Dict]) -> List[Dict]:
"""Synthesize root causes from Fishbone analysis."""
root_causes = []
# Group primary factors by category
category_factors = defaultdict(list)
for factor in primary_factors:
category_factors[factor["category"]].append(factor)
# Create root causes from categories with multiple factors
for category, factors in category_factors.items():
if len(factors) > 1:
root_causes.append({
"cause": f"Multiple {category.lower()} issues contributed to the incident",
"category": category,
"contributing_factors": [f["factor"] for f in factors],
"confidence": "high"
})
elif len(factors) == 1:
root_causes.append({
"cause": factors[0]["factor"],
"category": category,
"confidence": "medium"
})
return root_causes
def _has_delayed_response(self, timeline_data: Dict) -> bool:
"""Check if timeline shows delayed response patterns."""
if not timeline_data or "gap_analysis" not in timeline_data:
return False
gaps = timeline_data["gap_analysis"].get("gaps", [])
return any(gap.get("type") == "phase_transition" for gap in gaps)
def _extract_decision_points(self, timeline_data: Dict) -> List[Dict]:
"""Extract key decision points from timeline."""
decision_points = []
if "timeline" in timeline_data and "phases" in timeline_data["timeline"]:
phases = timeline_data["timeline"]["phases"]
for i, phase in enumerate(phases):
if phase["name"] in ["escalation", "mitigation"]:
decision_points.append({
"timestamp": phase["start_time"],
"decision": f"Initiated {phase['name']} phase",
"phase": phase["name"],
"duration": phase["duration_minutes"]
})
return decision_points
def _identify_missed_opportunities(self, timeline_data: Dict) -> List[Dict]:
"""Identify missed opportunities from gap analysis."""
missed_opportunities = []
if "gap_analysis" in timeline_data:
gaps = timeline_data["gap_analysis"].get("gaps", [])
for gap in gaps:
if gap.get("severity") == "critical":
missed_opportunities.append({
"opportunity": f"Earlier {gap['type'].replace('_', ' ')}",
"gap_minutes": gap["gap_minutes"],
"potential_impact": "Could have reduced incident duration"
})
return missed_opportunities
def _analyze_response_effectiveness(self, timeline_data: Dict) -> Dict[str, Any]:
"""Analyze the effectiveness of incident response."""
effectiveness = {
"overall_rating": "unknown",
"strengths": [],
"weaknesses": [],
"metrics": {}
}
if "metrics" in timeline_data:
metrics = timeline_data["metrics"]
duration_metrics = metrics.get("duration_metrics", {})
# Analyze response times
time_to_mitigation = duration_metrics.get("time_to_mitigation_minutes", 0)
time_to_resolution = duration_metrics.get("time_to_resolution_minutes", 0)
if time_to_mitigation <= 30:
effectiveness["strengths"].append("Quick mitigation response")
else:
effectiveness["weaknesses"].append("Slow mitigation response")
if time_to_resolution <= 120:
effectiveness["strengths"].append("Fast resolution")
else:
effectiveness["weaknesses"].append("Extended resolution time")
effectiveness["metrics"] = {
"time_to_mitigation": time_to_mitigation,
"time_to_resolution": time_to_resolution
}
# Overall rating based on strengths vs weaknesses
if len(effectiveness["strengths"]) > len(effectiveness["weaknesses"]):
effectiveness["overall_rating"] = "effective"
elif len(effectiveness["weaknesses"]) > len(effectiveness["strengths"]):
effectiveness["overall_rating"] = "needs_improvement"
else:
effectiveness["overall_rating"] = "mixed"
return effectiveness
def _extract_timeline_root_causes(self, decision_points: List, missed_opportunities: List,
response_analysis: Dict) -> List[Dict]:
"""Extract root causes from timeline analysis."""
root_causes = []
# Root causes from missed opportunities
for opportunity in missed_opportunities:
if opportunity["gap_minutes"] > 60: # Significant gaps
root_causes.append({
"cause": f"Delayed response: {opportunity['opportunity']}",
"category": "Process",
"evidence": f"{opportunity['gap_minutes']} minute gap identified",
"confidence": "high"
})
# Root causes from response effectiveness
for weakness in response_analysis.get("weaknesses", []):
root_causes.append({
"cause": weakness,
"category": "Process",
"evidence": "Timeline analysis",
"confidence": "medium"
})
return root_causes
def _identify_threats(self, incident_data: Dict, timeline_data: Optional[Dict]) -> List[Dict]:
"""Identify threats for Bow Tie analysis."""
threats = []
description = incident_data.get("description", "").lower()
if "deployment" in description:
threats.append({"threat": "Defective code deployment", "likelihood": "medium"})
if "load" in description or "traffic" in description:
threats.append({"threat": "Unexpected load increase", "likelihood": "high"})
if "database" in description:
threats.append({"threat": "Database performance degradation", "likelihood": "medium"})
return threats
def _identify_consequences(self, incident_data: Dict) -> List[Dict]:
"""Identify consequences for Bow Tie analysis."""
consequences = []
customer_impact = incident_data.get("customer_impact", "").lower()
business_impact = incident_data.get("business_impact", "").lower()
if "all users" in customer_impact or "complete outage" in customer_impact:
consequences.append({"consequence": "Complete service unavailability", "severity": "critical"})
if "revenue" in business_impact:
consequences.append({"consequence": "Revenue loss", "severity": "high"})
return consequences
def _identify_existing_barriers(self, incident_data: Dict, timeline_data: Optional[Dict]) -> List[Dict]:
"""Identify existing preventive/protective barriers."""
barriers = []
# Look for evidence of existing controls
if timeline_data and "timeline" in timeline_data:
events = timeline_data["timeline"].get("events", [])
for event in events:
message = event.get("message", "").lower()
if "alert" in message or "monitoring" in message:
barriers.append({
"barrier": "Monitoring and alerting system",
"type": "detective",
"effectiveness": "partial"
})
elif "rollback" in message:
barriers.append({
"barrier": "Rollback capability",
"type": "corrective",
"effectiveness": "effective"
})
return barriers
def _recommend_additional_barriers(self, threats: List[Dict], consequences: List[Dict]) -> List[Dict]:
"""Recommend additional barriers based on threats and consequences."""
recommendations = []
for threat in threats:
if "deployment" in threat["threat"].lower():
recommendations.append({
"barrier": "Enhanced pre-deployment testing",
"type": "preventive",
"justification": "Prevent defective deployments reaching production"
})
elif "load" in threat["threat"].lower():
recommendations.append({
"barrier": "Auto-scaling and load shedding",
"type": "preventive",
"justification": "Handle unexpected load increases automatically"
})
return recommendations
def _calculate_rca_confidence(self, analysis_data: Any, incident_data: Dict) -> str:
"""Calculate confidence level for RCA results."""
# Simple heuristic based on available data
confidence_score = 0
# More detailed incident data increases confidence
if incident_data.get("description") and len(incident_data["description"]) > 50:
confidence_score += 1
if incident_data.get("timeline") or incident_data.get("events"):
confidence_score += 2
if incident_data.get("logs") or incident_data.get("monitoring_data"):
confidence_score += 2
# Analysis data completeness
if isinstance(analysis_data, list) and len(analysis_data) > 3:
confidence_score += 1
elif isinstance(analysis_data, dict) and len(analysis_data) > 5:
confidence_score += 1
if confidence_score >= 4:
return "high"
elif confidence_score >= 2:
return "medium"
else:
return "low"
def _generate_lessons_learned(self, incident_data: Dict, timeline_data: Optional[Dict],
rca_results: Dict) -> Dict[str, List[str]]:
"""Generate categorized lessons learned."""
lessons = defaultdict(list)
# Lessons from RCA
root_causes = rca_results.get("root_causes", [])
for root_cause in root_causes:
category = root_cause.get("category", "technical_systems").lower()
category_key = self._map_to_lessons_category(category)
lesson = f"Identified: {root_cause['cause']}"
lessons[category_key].append(lesson)
# Lessons from timeline analysis
if timeline_data and "gap_analysis" in timeline_data:
gaps = timeline_data["gap_analysis"].get("gaps", [])
for gap in gaps:
if gap.get("severity") == "critical":
lessons["response_and_escalation"].append(
f"Response time gap: {gap['type'].replace('_', ' ')} took {gap['gap_minutes']} minutes"
)
# Generic lessons based on incident characteristics
severity = incident_data.get("severity", "").lower()
if severity in ["sev1", "critical"]:
lessons["detection_and_monitoring"].append(
"Critical incidents require immediate detection and alerting"
)
return dict(lessons)
def _map_to_lessons_category(self, category: str) -> str:
"""Map RCA category to lessons learned category."""
mapping = {
"people": "team_and_culture",
"process": "process_and_procedures",
"technology": "technical_systems",
"environment": "technical_systems",
"unknown": "process_and_procedures"
}
return mapping.get(category, "technical_systems")
def _generate_action_items(self, incident_data: Dict, rca_results: Dict,
lessons_learned: Dict) -> List[Dict]:
"""Generate actionable follow-up items."""
action_items = []
# Actions from root causes
root_causes = rca_results.get("root_causes", [])
for root_cause in root_causes:
action_type = self._determine_action_type(root_cause)
action_template = self.action_item_types[action_type]
action_items.append({
"title": f"Address: {root_cause['cause'][:50]}...",
"description": root_cause["cause"],
"type": action_type,
"priority": action_template["priority"],
"timeline": action_template["timeline"],
"owner": "TBD",
"success_criteria": f"Prevent recurrence of {root_cause['cause'][:30]}...",
"related_root_cause": root_cause
})
# Actions from lessons learned
for category, lessons in lessons_learned.items():
if len(lessons) > 1: # Multiple lessons in same category indicate systematic issue
action_items.append({
"title": f"Improve {category.replace('_', ' ')}",
"description": f"Address multiple issues identified in {category}",
"type": "process_improvement",
"priority": "P1",
"timeline": "2-3 weeks",
"owner": "TBD",
"success_criteria": f"Comprehensive review and improvement of {category}"
})
# Standard actions based on severity
severity = incident_data.get("severity", "").lower()
if severity in ["sev1", "critical"]:
action_items.append({
"title": "Conduct comprehensive post-incident review",
"description": "Schedule PIR meeting with all stakeholders",
"type": "process_improvement",
"priority": "P0",
"timeline": "24-48 hours",
"owner": incident_data.get("incident_commander", "TBD"),
"success_criteria": "PIR completed and documented"
})
return action_items
def _determine_action_type(self, root_cause: Dict) -> str:
"""Determine action item type based on root cause."""
cause_text = root_cause.get("cause", "").lower()
category = root_cause.get("category", "").lower()
if any(keyword in cause_text for keyword in ["bug", "error", "failure", "crash"]):
return "immediate_fix"
elif any(keyword in cause_text for keyword in ["monitor", "alert", "detect"]):
return "monitoring_alerting"
elif any(keyword in cause_text for keyword in ["process", "procedure", "review"]):
return "process_improvement"
elif any(keyword in cause_text for keyword in ["document", "runbook", "knowledge"]):
return "documentation"
elif any(keyword in cause_text for keyword in ["training", "skill", "knowledge"]):
return "training"
elif any(keyword in cause_text for keyword in ["architecture", "design", "system"]):
return "architectural"
else:
return "process_improvement" # Default
def _create_timeline_section(self, timeline_data: Optional[Dict], severity: str) -> str:
"""Create timeline section for PIR document."""
if not timeline_data:
return "No detailed timeline available."
timeline_content = []
if "timeline" in timeline_data and "phases" in timeline_data["timeline"]:
timeline_content.append("### Phase Timeline")
timeline_content.append("")
phases = timeline_data["timeline"]["phases"]
for phase in phases:
timeline_content.append(f"**{phase['name'].title()} Phase**")
timeline_content.append(f"- Start: {phase['start_time']}")
timeline_content.append(f"- Duration: {phase['duration_minutes']} minutes")
timeline_content.append(f"- Events: {phase['event_count']}")
timeline_content.append("")
if "metrics" in timeline_data:
metrics = timeline_data["metrics"]
duration_metrics = metrics.get("duration_metrics", {})
timeline_content.append("### Key Metrics")
timeline_content.append("")
timeline_content.append(f"- Total Duration: {duration_metrics.get('total_duration_minutes', 'N/A')} minutes")
timeline_content.append(f"- Time to Mitigation: {duration_metrics.get('time_to_mitigation_minutes', 'N/A')} minutes")
timeline_content.append(f"- Time to Resolution: {duration_metrics.get('time_to_resolution_minutes', 'N/A')} minutes")
timeline_content.append("")
return "\n".join(timeline_content)
def _generate_document_sections(self, incident_info: Dict, rca_results: Dict,
lessons_learned: Dict, action_items: List[Dict],
timeline_section: str) -> Dict[str, str]:
"""Generate all document sections for PIR template."""
sections = {}
# Basic information
sections["incident_title"] = incident_info["title"]
sections["incident_id"] = incident_info["incident_id"]
sections["incident_date"] = incident_info["start_time"].strftime("%Y-%m-%d %H:%M:%S UTC") if incident_info["start_time"] else "Unknown"
sections["duration"] = incident_info["duration"]
sections["severity"] = incident_info["severity"].upper()
sections["status"] = incident_info["status"].title()
sections["incident_commander"] = incident_info["incident_commander"]
sections["responders"] = ", ".join(incident_info["responders"]) if incident_info["responders"] else "TBD"
sections["generation_date"] = datetime.now().strftime("%Y-%m-%d")
# Impact sections
sections["customer_impact"] = incident_info["customer_impact"]
sections["business_impact"] = incident_info["business_impact"]
# Executive summary
sections["executive_summary"] = self._create_executive_summary(incident_info, rca_results)
# Timeline
sections["timeline_section"] = timeline_section
# RCA section
sections["rca_section"] = self._create_rca_section(rca_results)
# What went well/wrong
sections["what_went_well"] = self._create_what_went_well_section(incident_info, rca_results)
sections["what_went_wrong"] = self._create_what_went_wrong_section(rca_results, lessons_learned)
# Lessons learned
sections["lessons_learned"] = self._create_lessons_learned_section(lessons_learned)
# Action items
sections["action_items"] = self._create_action_items_section(action_items)
# Prevention and appendix
sections["prevention_measures"] = self._create_prevention_section(rca_results, action_items)
sections["appendix_section"] = self._create_appendix_section(incident_info)
return sections
def _create_executive_summary(self, incident_info: Dict, rca_results: Dict) -> str:
"""Create executive summary section."""
summary_parts = []
# Incident description
summary_parts.append(f"On {incident_info['start_time'].strftime('%B %d, %Y') if incident_info['start_time'] else 'an unknown date'}, we experienced a {incident_info['severity']} incident affecting {incident_info.get('affected_services', ['our services'])}.")
# Duration and impact
summary_parts.append(f"The incident lasted {incident_info['duration']} and had the following impact: {incident_info['customer_impact']}")
# Root cause summary
root_causes = rca_results.get("root_causes", [])
if root_causes:
primary_cause = root_causes[0]["cause"]
summary_parts.append(f"Root cause analysis identified the primary issue as: {primary_cause}")
# Resolution
summary_parts.append(f"The incident has been {incident_info['status']} and we have identified specific actions to prevent recurrence.")
return " ".join(summary_parts)
def _create_rca_section(self, rca_results: Dict) -> str:
"""Create RCA section content."""
rca_content = []
method = rca_results.get("method", "unknown")
rca_content.append(f"### Analysis Method: {self.rca_frameworks.get(method, {}).get('name', method)}")
rca_content.append("")
if method == "five_whys" and "why_analysis" in rca_results:
rca_content.append("#### Why Analysis")
rca_content.append("")
for i, why in enumerate(rca_results["why_analysis"], 1):
rca_content.append(f"**Why {i}:** {why['question']}")
rca_content.append(f"**Answer:** {why['answer']}")
if why["evidence"]:
rca_content.append(f"**Evidence:** {', '.join(why['evidence'])}")
rca_content.append("")
elif method == "fishbone" and "categories" in rca_results:
rca_content.append("#### Contributing Factor Analysis")
rca_content.append("")
for category, data in rca_results["categories"].items():
if data["factors"]:
rca_content.append(f"**{category}:**")
for factor in data["factors"]:
rca_content.append(f"- {factor['factor']} (likelihood: {factor.get('likelihood', 'unknown')})")
rca_content.append("")
# Root causes summary
root_causes = rca_results.get("root_causes", [])
if root_causes:
rca_content.append("#### Identified Root Causes")
rca_content.append("")
for i, cause in enumerate(root_causes, 1):
rca_content.append(f"{i}. **{cause['cause']}**")
rca_content.append(f" - Category: {cause.get('category', 'Unknown')}")
rca_content.append(f" - Confidence: {cause.get('confidence', 'Unknown')}")
if cause.get("evidence"):
rca_content.append(f" - Evidence: {cause['evidence']}")
rca_content.append("")
return "\n".join(rca_content)
def _create_what_went_well_section(self, incident_info: Dict, rca_results: Dict) -> str:
"""Create what went well section."""
positives = []
# Generic positive aspects
if incident_info["status"] == "resolved":
positives.append("The incident was successfully resolved")
if incident_info["incident_commander"] != "TBD":
positives.append("Incident command was established")
if len(incident_info.get("responders", [])) > 1:
positives.append("Multiple team members collaborated on resolution")
# Analysis-specific positives
if rca_results.get("confidence") == "high":
positives.append("Root cause analysis provided clear insights")
if not positives:
positives.append("Incident response process was followed")
return "\n".join([f"- {positive}" for positive in positives])
def _create_what_went_wrong_section(self, rca_results: Dict, lessons_learned: Dict) -> str:
"""Create what went wrong section."""
issues = []
# Issues from RCA
root_causes = rca_results.get("root_causes", [])
for cause in root_causes[:3]: # Show top 3
issues.append(cause["cause"])
# Issues from lessons learned
for category, lessons in lessons_learned.items():
if lessons:
issues.append(f"{category.replace('_', ' ').title()}: {lessons[0]}")
if not issues:
issues.append("Analysis in progress")
return "\n".join([f"- {issue}" for issue in issues])
def _create_lessons_learned_section(self, lessons_learned: Dict) -> str:
"""Create lessons learned section."""
content = []
for category, lessons in lessons_learned.items():
if lessons:
content.append(f"### {category.replace('_', ' ').title()}")
content.append("")
for lesson in lessons:
content.append(f"- {lesson}")
content.append("")
if not content:
content.append("Lessons learned to be documented following detailed analysis.")
return "\n".join(content)
def _create_action_items_section(self, action_items: List[Dict]) -> str:
"""Create action items section."""
if not action_items:
return "Action items to be defined."
content = []
# Group by priority
priority_groups = defaultdict(list)
for item in action_items:
priority_groups[item.get("priority", "P3")].append(item)
for priority in ["P0", "P1", "P2", "P3"]:
items = priority_groups.get(priority, [])
if items:
content.append(f"### {priority} - {self._get_priority_description(priority)}")
content.append("")
for item in items:
content.append(f"**{item['title']}**")
content.append(f"- Owner: {item.get('owner', 'TBD')}")
content.append(f"- Timeline: {item.get('timeline', 'TBD')}")
content.append(f"- Success Criteria: {item.get('success_criteria', 'TBD')}")
content.append("")
return "\n".join(content)
def _get_priority_description(self, priority: str) -> str:
"""Get human-readable priority description."""
descriptions = {
"P0": "Critical - Immediate Action Required",
"P1": "High Priority - Complete Within 1-2 Weeks",
"P2": "Medium Priority - Complete Within 1 Month",
"P3": "Low Priority - Complete When Capacity Allows"
}
return descriptions.get(priority, "Unknown Priority")
def _create_prevention_section(self, rca_results: Dict, action_items: List[Dict]) -> str:
"""Create prevention and follow-up section."""
content = []
content.append("### Prevention Measures")
content.append("")
content.append("Based on the root cause analysis, the following preventive measures have been identified:")
content.append("")
# Extract prevention-focused action items
prevention_items = [item for item in action_items if "prevent" in item.get("description", "").lower()]
if prevention_items:
for item in prevention_items:
content.append(f"- {item['title']}: {item.get('description', '')}")
else:
content.append("- Implement comprehensive testing for similar scenarios")
content.append("- Improve monitoring and alerting coverage")
content.append("- Enhance error handling and resilience patterns")
content.append("")
content.append("### Follow-up Schedule")
content.append("")
content.append("- 1 week: Review action item progress")
content.append("- 1 month: Evaluate effectiveness of implemented changes")
content.append("- 3 months: Conduct follow-up assessment and update preventive measures")
return "\n".join(content)
def _create_appendix_section(self, incident_info: Dict) -> str:
"""Create appendix section."""
content = []
content.append("### Additional Information")
content.append("")
content.append(f"- Incident ID: {incident_info['incident_id']}")
content.append(f"- Severity Classification: {incident_info['severity']}")
if incident_info.get("affected_services"):
content.append(f"- Affected Services: {', '.join(incident_info['affected_services'])}")
content.append("")
content.append("### References")
content.append("")
content.append("- Incident tracking ticket: [Link TBD]")
content.append("- Monitoring dashboards: [Link TBD]")
content.append("- Communication thread: [Link TBD]")
return "\n".join(content)
def _generate_metadata(self, incident_info: Dict, rca_results: Dict, action_items: List[Dict]) -> Dict[str, Any]:
"""Generate PIR metadata for tracking and analysis."""
return {
"pir_id": f"PIR-{incident_info['incident_id']}",
"incident_severity": incident_info["severity"],
"rca_method": rca_results.get("method", "unknown"),
"rca_confidence": rca_results.get("confidence", "unknown"),
"total_action_items": len(action_items),
"critical_action_items": len([item for item in action_items if item.get("priority") == "P0"]),
"estimated_prevention_timeline": self._estimate_prevention_timeline(action_items),
"categories_affected": list(set(item.get("type", "unknown") for item in action_items)),
"review_completeness": self._assess_review_completeness(incident_info, rca_results, action_items)
}
def _estimate_prevention_timeline(self, action_items: List[Dict]) -> str:
"""Estimate timeline for implementing all prevention measures."""
if not action_items:
return "unknown"
# Find the longest timeline among action items
max_weeks = 0
for item in action_items:
timeline = item.get("timeline", "")
if "week" in timeline:
try:
weeks = int(re.findall(r'\d+', timeline)[0])
max_weeks = max(max_weeks, weeks)
except (IndexError, ValueError):
pass
elif "month" in timeline:
try:
months = int(re.findall(r'\d+', timeline)[0])
max_weeks = max(max_weeks, months * 4)
except (IndexError, ValueError):
pass
if max_weeks == 0:
return "1-2 weeks"
elif max_weeks <= 4:
return f"{max_weeks} weeks"
else:
return f"{max_weeks // 4} months"
def _assess_review_completeness(self, incident_info: Dict, rca_results: Dict, action_items: List[Dict]) -> float:
"""Assess completeness of the PIR (0-1 score)."""
score = 0.0
# Basic information completeness
if incident_info.get("description"):
score += 0.1
if incident_info.get("start_time"):
score += 0.1
if incident_info.get("customer_impact"):
score += 0.1
# RCA completeness
if rca_results.get("root_causes"):
score += 0.2
if rca_results.get("confidence") in ["medium", "high"]:
score += 0.1
# Action items completeness
if action_items:
score += 0.2
if any(item.get("owner") and item["owner"] != "TBD" for item in action_items):
score += 0.1
# Additional factors
if incident_info.get("incident_commander") != "TBD":
score += 0.1
if len(action_items) >= 3: # Multiple action items show thorough analysis
score += 0.1
return min(score, 1.0)
def format_json_output(result: Dict) -> str:
"""Format result as pretty JSON."""
return json.dumps(result, indent=2, ensure_ascii=False)
def format_markdown_output(result: Dict) -> str:
"""Format result as Markdown PIR document."""
return result.get("pir_document", "Error: No PIR document generated")
def format_text_output(result: Dict) -> str:
"""Format result as human-readable summary."""
if "error" in result:
return f"Error: {result['error']}"
metadata = result.get("metadata", {})
incident_info = result.get("incident_info", {})
rca_results = result.get("rca_results", {})
action_items = result.get("action_items", [])
output = []
output.append("=" * 60)
output.append("POST-INCIDENT REVIEW SUMMARY")
output.append("=" * 60)
output.append("")
# Basic info
output.append("INCIDENT INFORMATION:")
output.append(f" PIR ID: {metadata.get('pir_id', 'Unknown')}")
output.append(f" Severity: {incident_info.get('severity', 'Unknown').upper()}")
output.append(f" Duration: {incident_info.get('duration', 'Unknown')}")
output.append(f" Status: {incident_info.get('status', 'Unknown').title()}")
output.append("")
# RCA summary
output.append("ROOT CAUSE ANALYSIS:")
output.append(f" Method: {rca_results.get('method', 'Unknown')}")
output.append(f" Confidence: {rca_results.get('confidence', 'Unknown').title()}")
root_causes = rca_results.get("root_causes", [])
if root_causes:
output.append(f" Root Causes Identified: {len(root_causes)}")
for i, cause in enumerate(root_causes[:3], 1):
output.append(f" {i}. {cause.get('cause', 'Unknown')[:60]}...")
output.append("")
# Action items summary
output.append("ACTION ITEMS:")
output.append(f" Total Actions: {len(action_items)}")
output.append(f" Critical (P0): {metadata.get('critical_action_items', 0)}")
output.append(f" Prevention Timeline: {metadata.get('estimated_prevention_timeline', 'Unknown')}")
if action_items:
output.append(" Top Actions:")
for item in action_items[:3]:
output.append(f" - {item.get('title', 'Unknown')[:50]}...")
output.append("")
# Completeness
completeness = metadata.get("review_completeness", 0) * 100
output.append(f"REVIEW COMPLETENESS: {completeness:.0f}%")
output.append("")
output.append("=" * 60)
return "\n".join(output)
def main():
"""Main function with argument parsing and execution."""
parser = argparse.ArgumentParser(
description="Generate Post-Incident Review documents with RCA and action items",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python pir_generator.py --incident incident.json --output pir.md
python pir_generator.py --incident incident.json --rca-method fishbone
cat incident.json | python pir_generator.py --format markdown
Incident JSON format:
{
"incident_id": "INC-2024-001",
"title": "Database performance degradation",
"description": "Users experiencing slow response times",
"severity": "sev2",
"start_time": "2024-01-01T12:00:00Z",
"end_time": "2024-01-01T14:30:00Z",
"customer_impact": "50% of users affected by slow page loads",
"business_impact": "Moderate user experience degradation",
"incident_commander": "Alice Smith",
"responders": ["Bob Jones", "Carol Johnson"]
}
"""
)
parser.add_argument(
"--incident", "-i",
help="Incident data file (JSON) or '-' for stdin"
)
parser.add_argument(
"--timeline", "-t",
help="Timeline reconstruction file (JSON)"
)
parser.add_argument(
"--output", "-o",
help="Output file path (default: stdout)"
)
parser.add_argument(
"--format", "-f",
choices=["json", "markdown", "text"],
default="markdown",
help="Output format (default: markdown)"
)
parser.add_argument(
"--rca-method",
choices=["five_whys", "fishbone", "timeline", "bow_tie"],
default="five_whys",
help="Root cause analysis method (default: five_whys)"
)
parser.add_argument(
"--template-type",
choices=["comprehensive", "standard", "brief"],
default="comprehensive",
help="PIR template type (default: comprehensive)"
)
parser.add_argument(
"--action-items",
action="store_true",
help="Generate detailed action items"
)
args = parser.parse_args()
generator = PIRGenerator()
try:
# Read incident data
if args.incident == "-" or (not args.incident and not sys.stdin.isatty()):
# Read from stdin
input_text = sys.stdin.read().strip()
if not input_text:
parser.error("No incident data provided")
incident_data = json.loads(input_text)
elif args.incident:
# Read from file
with open(args.incident, 'r') as f:
incident_data = json.load(f)
else:
parser.error("No incident data specified. Use --incident or pipe data to stdin.")
# Read timeline data if provided
timeline_data = None
if args.timeline:
with open(args.timeline, 'r') as f:
timeline_data = json.load(f)
# Validate incident data
if not isinstance(incident_data, dict):
parser.error("Incident data must be a JSON object")
if not incident_data.get("description") and not incident_data.get("title"):
parser.error("Incident data must contain 'description' or 'title'")
# Generate PIR
result = generator.generate_pir(
incident_data=incident_data,
timeline_data=timeline_data,
rca_method=args.rca_method,
template_type=args.template_type
)
# Format output
if args.format == "json":
output = format_json_output(result)
elif args.format == "markdown":
output = format_markdown_output(result)
else:
output = format_text_output(result)
# Write output
if args.output:
with open(args.output, 'w') as f:
f.write(output)
f.write('\n')
else:
print(output)
except FileNotFoundError as e:
print(f"Error: File not found - {e}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON - {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
FILE:scripts/postmortem_generator.py
#!/usr/bin/env python3
"""
Postmortem Generator - Generate structured postmortem reports with 5-Whys analysis.
Produces comprehensive incident postmortem documents from structured JSON input,
including root cause analysis, contributing factor classification, action item
validation, MTTD/MTTR metrics, and customer impact summaries.
Usage:
python postmortem_generator.py incident_data.json
python postmortem_generator.py incident_data.json --format markdown
python postmortem_generator.py incident_data.json --format json
cat incident_data.json | python postmortem_generator.py
Input:
JSON object with keys: incident, timeline, resolution, action_items, participants.
See SKILL.md for the full input schema.
"""
import argparse
import json
import sys
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple
# ---------- Constants and Configuration ----------
VERSION = "1.0.0"
SEVERITY_ORDER = {"SEV0": 0, "SEV1": 1, "SEV2": 2, "SEV3": 3, "SEV4": 4}
FACTOR_CATEGORIES = ("process", "tooling", "human", "environment", "external")
ACTION_TYPES = ("detection", "prevention", "mitigation", "process")
PRIORITY_ORDER = {"P0": 0, "P1": 1, "P2": 2, "P3": 3, "P4": 4}
POSTMORTEM_TARGET_HOURS = 72
# Industry benchmarks for incident response (minutes, except postmortem)
BENCHMARKS = {
"SEV0": {"mttd": 5, "mttr": 60, "mitigate": 30, "declare": 5},
"SEV1": {"mttd": 10, "mttr": 120, "mitigate": 60, "declare": 10},
"SEV2": {"mttd": 30, "mttr": 480, "mitigate": 120, "declare": 30},
"SEV3": {"mttd": 60, "mttr": 1440, "mitigate": 240, "declare": 60},
"SEV4": {"mttd": 120, "mttr": 2880, "mitigate": 480, "declare": 120},
}
CAT_TO_ACTION = {"process": "process", "tooling": "detection", "human": "prevention",
"environment": "mitigation", "external": "prevention"}
CAT_WEIGHT = {"process": 1.0, "tooling": 0.9, "human": 0.8, "environment": 0.7, "external": 0.6}
# Keywords used to classify contributing factors into categories
FACTOR_KEYWORDS = {
"process": ["process", "procedure", "workflow", "review", "approval", "checklist",
"runbook", "documentation", "policy", "standard", "protocol", "canary",
"deployment", "rollback", "change management"],
"tooling": ["tool", "monitor", "alert", "threshold", "automation", "test", "pipeline",
"ci/cd", "observability", "dashboard", "logging", "infrastructure",
"configuration", "config"],
"human": ["training", "knowledge", "experience", "communication", "handoff", "fatigue",
"oversight", "mistake", "error", "misunderstand", "assumption", "awareness"],
"environment": ["load", "traffic", "scale", "capacity", "resource", "network", "hardware",
"region", "latency", "timeout", "connection", "performance", "spike"],
"external": ["vendor", "third-party", "upstream", "downstream", "provider", "api",
"dependency", "partner", "dns", "cdn", "certificate"],
}
# 5-Whys templates per category (each list is 5 why->answer steps)
WHY_TEMPLATES = {
"process": [
"Why did this process gap exist? -> The existing process did not account for this scenario.",
"Why was the scenario not accounted for? -> It was not identified during the last process review.",
"Why was the process review incomplete? -> Reviews focus on known failure modes, not emerging risks.",
"Why are emerging risks not surfaced? -> No systematic mechanism to capture lessons from near-misses.",
"Why is there no near-miss capture mechanism? -> Incident learning is ad-hoc rather than systematic."],
"tooling": [
"Why did the tooling fail to catch this? -> The relevant metric was not monitored or the threshold was misconfigured.",
"Why was the threshold misconfigured? -> It was set during initial deployment and never revisited.",
"Why was it never revisited? -> There is no scheduled review of monitoring configurations.",
"Why is there no scheduled review? -> Monitoring ownership is diffuse across teams.",
"Why is ownership diffuse? -> No clear operational runbook assigns monitoring review responsibilities."],
"human": [
"Why did the human factor contribute? -> The individual lacked context needed to prevent the issue.",
"Why was context lacking? -> Knowledge was siloed and not documented accessibly.",
"Why was knowledge siloed? -> No structured onboarding or knowledge-sharing process for this area.",
"Why is there no knowledge-sharing process? -> Team capacity has been focused on feature delivery.",
"Why is capacity skewed toward features? -> Operational excellence is not weighted equally in planning."],
"environment": [
"Why did the environment cause this failure? -> System capacity was insufficient for the load pattern.",
"Why was capacity insufficient? -> Load projections did not account for this traffic pattern.",
"Why were projections inaccurate? -> Load testing does not replicate production-scale variability.",
"Why doesn't load testing replicate production? -> Test environments lack realistic traffic generators.",
"Why are traffic generators missing? -> Investment in production-like test infrastructure was deferred."],
"external": [
"Why did the external factor cause an incident? -> The system had a hard dependency with no fallback.",
"Why was there no fallback? -> The integration was assumed to be highly available.",
"Why was high availability assumed? -> SLA review of the external dependency was not performed.",
"Why was SLA review skipped? -> No standard checklist for evaluating third-party dependencies.",
"Why is there no evaluation checklist? -> Vendor management practices are informal and undocumented."],
}
THEME_RECS = {
"process": ["Establish a quarterly process review cadence covering change management and deployment procedures.",
"Implement a near-miss tracking system to surface latent risks before they become incidents.",
"Create pre-deployment checklists that require sign-off from the service owner."],
"tooling": ["Schedule quarterly reviews of alerting thresholds and monitoring coverage.",
"Assign explicit monitoring ownership per service in operational runbooks.",
"Invest in synthetic monitoring and canary analysis for critical paths."],
"human": ["Build structured onboarding that covers incident-prone areas and past postmortems.",
"Implement blameless knowledge-sharing sessions after each incident.",
"Balance operational excellence work alongside feature delivery in sprint planning."],
"environment": ["Conduct periodic capacity planning reviews using production traffic replays.",
"Invest in production-like load-testing infrastructure with realistic traffic profiles.",
"Implement auto-scaling policies with validated upper-bound thresholds."],
"external": ["Perform formal SLA reviews for all third-party dependencies annually.",
"Implement circuit breakers and fallbacks for external service integrations.",
"Maintain a dependency registry with risk ratings and contingency plans."],
}
MISSING_ACTION_TEMPLATES = {
"process": "Create or update runbook/checklist to prevent recurrence of this process gap",
"detection": "Add monitoring and alerting to detect this class of issue earlier",
"mitigation": "Implement auto-scaling or circuit-breaker to reduce blast radius",
"prevention": "Add automated safeguards (canary deploy, load test gate) to prevent recurrence",
}
# ---------- Data Model Classes ----------
class IncidentData:
"""Parsed incident metadata."""
def __init__(self, data: Dict[str, Any]) -> None:
self.id: str = data.get("id", "UNKNOWN")
self.title: str = data.get("title", "Untitled Incident")
self.severity: str = data.get("severity", "SEV3").upper()
self.commander: str = data.get("commander", "Unassigned")
self.service: str = data.get("service", "unknown-service")
self.affected_services: List[str] = data.get("affected_services", [])
def to_dict(self) -> Dict[str, Any]:
return {"id": self.id, "title": self.title, "severity": self.severity,
"commander": self.commander, "service": self.service,
"affected_services": self.affected_services}
class TimelineMetrics:
"""MTTD, MTTR, and other timing metrics computed from raw timestamps."""
def __init__(self, timeline: Dict[str, str], severity: str) -> None:
self.severity = severity
self.issue_started = self._parse(timeline.get("issue_started"))
self.detected_at = self._parse(timeline.get("detected_at"))
self.declared_at = self._parse(timeline.get("declared_at"))
self.mitigated_at = self._parse(timeline.get("mitigated_at"))
self.resolved_at = self._parse(timeline.get("resolved_at"))
self.postmortem_at = self._parse(timeline.get("postmortem_at"))
@staticmethod
def _parse(ts: Optional[str]) -> Optional[datetime]:
if ts is None:
return None
for fmt in ("%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%dT%H:%M:%S"):
try:
dt = datetime.strptime(ts, fmt)
return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
except ValueError:
continue
return None
def _delta_min(self, start: Optional[datetime], end: Optional[datetime]) -> Optional[float]:
if start is None or end is None:
return None
return round((end - start).total_seconds() / 60.0, 1)
@property
def mttd(self) -> Optional[float]:
return self._delta_min(self.issue_started, self.detected_at)
@property
def mttr(self) -> Optional[float]:
return self._delta_min(self.detected_at, self.resolved_at)
@property
def time_to_mitigate(self) -> Optional[float]:
return self._delta_min(self.detected_at, self.mitigated_at)
@property
def time_to_declare(self) -> Optional[float]:
return self._delta_min(self.detected_at, self.declared_at)
@property
def postmortem_timeliness_hours(self) -> Optional[float]:
m = self._delta_min(self.resolved_at, self.postmortem_at)
return round(m / 60.0, 1) if m is not None else None
@property
def postmortem_on_time(self) -> Optional[bool]:
h = self.postmortem_timeliness_hours
return h <= POSTMORTEM_TARGET_HOURS if h is not None else None
def benchmark_comparison(self) -> Dict[str, Dict[str, Any]]:
bench = BENCHMARKS.get(self.severity, BENCHMARKS["SEV3"])
results: Dict[str, Dict[str, Any]] = {}
for name, actual, target in [("mttd", self.mttd, bench["mttd"]),
("mttr", self.mttr, bench["mttr"]),
("time_to_mitigate", self.time_to_mitigate, bench["mitigate"]),
("time_to_declare", self.time_to_declare, bench["declare"])]:
if actual is not None:
results[name] = {"actual_minutes": actual, "benchmark_minutes": target,
"met_benchmark": actual <= target,
"delta_minutes": round(actual - target, 1)}
h = self.postmortem_timeliness_hours
if h is not None:
results["postmortem_timeliness"] = {
"actual_hours": h, "target_hours": POSTMORTEM_TARGET_HOURS,
"met_target": self.postmortem_on_time, "delta_hours": round(h - POSTMORTEM_TARGET_HOURS, 1)}
return results
def to_dict(self) -> Dict[str, Any]:
return {"mttd_minutes": self.mttd, "mttr_minutes": self.mttr,
"time_to_mitigate_minutes": self.time_to_mitigate,
"time_to_declare_minutes": self.time_to_declare,
"postmortem_timeliness_hours": self.postmortem_timeliness_hours,
"postmortem_on_time": self.postmortem_on_time,
"benchmarks": self.benchmark_comparison()}
class ContributingFactor:
"""A classified contributing factor with weight and action-type mapping."""
def __init__(self, description: str, index: int) -> None:
self.description = description
self.index = index
self.category = self._classify()
self.weight = round(max(1.0 - index * 0.15, 0.3) * CAT_WEIGHT.get(self.category, 0.8), 2)
self.mapped_action_type = CAT_TO_ACTION.get(self.category, "process")
def _classify(self) -> str:
lower = self.description.lower()
scores = {cat: sum(1 for kw in kws if kw in lower) for cat, kws in FACTOR_KEYWORDS.items()}
best = max(scores, key=lambda k: scores[k])
return best if scores[best] > 0 else "process"
def to_dict(self) -> Dict[str, Any]:
return {"description": self.description, "category": self.category,
"weight": self.weight, "mapped_action_type": self.mapped_action_type}
class FiveWhysAnalysis:
"""Structured 5-Whys chain for a contributing factor."""
def __init__(self, factor: ContributingFactor) -> None:
self.factor = factor
self.systemic_theme: str = factor.category
self.chain: List[str] = [f"Why? {factor.description}"] + \
WHY_TEMPLATES.get(factor.category, WHY_TEMPLATES["process"])
def to_dict(self) -> Dict[str, Any]:
return {"factor": self.factor.description, "category": self.factor.category,
"chain": self.chain, "systemic_theme": self.systemic_theme}
class ActionItem:
"""Parsed and validated action item."""
def __init__(self, data: Dict[str, Any]) -> None:
self.title: str = data.get("title", "")
self.owner: str = data.get("owner", "")
self.priority: str = data.get("priority", "P3")
self.deadline: str = data.get("deadline", "")
self.type: str = data.get("type", "process")
self.status: str = data.get("status", "open")
self.validation_issues: List[str] = []
self.quality_score: int = 0
self._validate()
def _validate(self) -> None:
self.validation_issues = []
if not self.title:
self.validation_issues.append("Missing title")
if not self.owner:
self.validation_issues.append("Missing owner")
if not self.deadline:
self.validation_issues.append("Missing deadline")
if self.priority not in PRIORITY_ORDER:
self.validation_issues.append(f"Invalid priority: {self.priority}")
if self.type not in ACTION_TYPES:
self.validation_issues.append(f"Invalid type: {self.type}")
self.quality_score = self._score_quality()
def _score_quality(self) -> int:
"""Score 0-100: specific, measurable, achievable."""
s = 0
if len(self.title) > 10: s += 20
if self.owner: s += 20
if self.deadline: s += 20
if self.priority in PRIORITY_ORDER: s += 10
if self.type in ACTION_TYPES: s += 10
if any(kw in self.title.lower() for kw in ["%", "threshold", "within", "before",
"after", "less than", "greater than"]):
s += 10
if len(self.title.split()) >= 5: s += 10
return min(s, 100)
@property
def is_valid(self) -> bool:
return len(self.validation_issues) == 0
@property
def is_past_deadline(self) -> bool:
if not self.deadline or self.status != "open":
return False
try:
dl = datetime.strptime(self.deadline, "%Y-%m-%d").replace(tzinfo=timezone.utc)
return datetime.now(timezone.utc) > dl
except ValueError:
return False
def to_dict(self) -> Dict[str, Any]:
return {"title": self.title, "owner": self.owner, "priority": self.priority,
"deadline": self.deadline, "type": self.type, "status": self.status,
"is_valid": self.is_valid, "validation_issues": self.validation_issues,
"quality_score": self.quality_score, "is_past_deadline": self.is_past_deadline}
class PostmortemReport:
"""Complete postmortem document assembled from all analysis components."""
def __init__(self, raw: Dict[str, Any]) -> None:
self.raw = raw
self.incident = IncidentData(raw.get("incident", {}))
self.timeline = TimelineMetrics(raw.get("timeline", {}), self.incident.severity)
self.resolution: Dict[str, Any] = raw.get("resolution", {})
self.participants: List[Dict[str, str]] = raw.get("participants", [])
# Derived analysis
self.contributing_factors = [ContributingFactor(f, i)
for i, f in enumerate(self.resolution.get("contributing_factors", []))]
self.five_whys = [FiveWhysAnalysis(f) for f in self.contributing_factors]
self.action_items = [ActionItem(a) for a in raw.get("action_items", [])]
self.factor_distribution = self._compute_factor_distribution()
self.coverage_gaps = self._find_coverage_gaps()
self.suggested_actions = self._suggest_missing_actions()
self.theme_recommendations = self._build_theme_recommendations()
def _compute_factor_distribution(self) -> Dict[str, float]:
dist: Dict[str, float] = {c: 0.0 for c in FACTOR_CATEGORIES}
total = sum(f.weight for f in self.contributing_factors) or 1.0
for f in self.contributing_factors:
dist[f.category] += f.weight
return {k: round(v / total * 100, 1) for k, v in dist.items()}
def _find_coverage_gaps(self) -> List[str]:
factor_cats = {f.category for f in self.contributing_factors}
action_types = {a.type for a in self.action_items}
gaps = []
for cat in factor_cats:
expected = CAT_TO_ACTION.get(cat)
if expected and expected not in action_types:
gaps.append(f"No '{expected}' action item to address '{cat}' contributing factor")
return gaps
def _suggest_missing_actions(self) -> List[Dict[str, str]]:
factor_cats = {f.category for f in self.contributing_factors}
action_types = {a.type for a in self.action_items}
suggestions = []
for cat in factor_cats:
expected = CAT_TO_ACTION.get(cat)
if expected and expected not in action_types:
suggestions.append({
"type": expected,
"suggestion": MISSING_ACTION_TEMPLATES.get(expected, "Add an action item for this gap"),
"reason": f"No action item addresses the '{cat}' contributing factor"})
return suggestions
def _build_theme_recommendations(self) -> Dict[str, List[str]]:
seen: Dict[str, List[str]] = {}
for a in self.five_whys:
if a.systemic_theme not in seen:
seen[a.systemic_theme] = THEME_RECS.get(a.systemic_theme, [])
return seen
def customer_impact_summary(self) -> Dict[str, Any]:
impact = self.resolution.get("customer_impact", {})
affected = impact.get("affected_users", 0)
failed_tx = impact.get("failed_transactions", 0)
revenue = impact.get("revenue_impact_usd", 0)
data_loss = impact.get("data_loss", False)
comm_required = affected > 1000 or data_loss or revenue > 10000
sev = "high" if (affected > 10000 or revenue > 50000) else (
"medium" if (affected > 1000 or revenue > 5000) else "low")
return {"affected_users": affected, "failed_transactions": failed_tx,
"revenue_impact_usd": revenue, "data_loss": data_loss,
"data_integrity": "compromised" if data_loss else "intact",
"customer_communication_required": comm_required, "impact_severity": sev}
def executive_summary(self) -> str:
mttr = self.timeline.mttr
ci = self.customer_impact_summary()
mttr_str = f"{mttr:.0f} minutes" if mttr is not None else "unknown duration"
parts = [
f"On {self._fmt_date(self.timeline.issue_started)}, a {self.incident.severity} "
f"incident (\"{self.incident.title}\") impacted the {self.incident.service} service.",
f"The root cause was identified as: {self.resolution.get('root_cause', 'Unknown root cause')}.",
f"The incident was resolved in {mttr_str}, affecting approximately "
f"{ci['affected_users']:,} users with an estimated revenue impact of ,.2f.",
"Data loss was confirmed; affected customers must be notified." if ci["data_loss"]
else "No data loss occurred during this incident."]
return " ".join(parts)
@staticmethod
def _fmt_date(dt: Optional[datetime]) -> str:
return dt.strftime("%Y-%m-%d at %H:%M UTC") if dt else "an unknown date"
def overdue_p1_items(self) -> List[Dict[str, str]]:
return [{"title": a.title, "owner": a.owner, "deadline": a.deadline}
for a in self.action_items if a.priority in ("P0", "P1") and a.is_past_deadline]
def to_dict(self) -> Dict[str, Any]:
return {
"version": VERSION, "incident": self.incident.to_dict(),
"executive_summary": self.executive_summary(),
"timeline_metrics": self.timeline.to_dict(),
"customer_impact": self.customer_impact_summary(),
"root_cause": self.resolution.get("root_cause", ""),
"contributing_factors": [f.to_dict() for f in self.contributing_factors],
"factor_distribution": self.factor_distribution,
"five_whys_analysis": [a.to_dict() for a in self.five_whys],
"theme_recommendations": self.theme_recommendations,
"mitigation_steps": self.resolution.get("mitigation_steps", []),
"permanent_fix": self.resolution.get("permanent_fix", ""),
"action_items": [a.to_dict() for a in self.action_items],
"action_item_coverage_gaps": self.coverage_gaps,
"suggested_actions": self.suggested_actions,
"overdue_p1_items": self.overdue_p1_items(),
"participants": self.participants}
# ---------- Core Analysis Helpers ----------
def _bar(pct: float, width: int = 30) -> str:
"""Render a text-based horizontal bar chart segment."""
filled = int(round(pct / 100 * width))
return "[" + "#" * filled + "." * (width - filled) + "]"
def _generate_lessons(report: PostmortemReport) -> List[str]:
"""Derive lessons learned from the analysis."""
lessons: List[str] = []
bench = BENCHMARKS.get(report.incident.severity, BENCHMARKS["SEV3"])
mttd = report.timeline.mttd
if mttd is not None and mttd > bench["mttd"]:
lessons.append(
f"Detection took {mttd:.0f} minutes, exceeding the {bench['mttd']}-minute "
f"benchmark for {report.incident.severity}. Invest in earlier detection mechanisms.")
dist = report.factor_distribution
dominant = max(dist, key=lambda k: dist[k])
if dist[dominant] >= 50:
lessons.append(
f"The '{dominant}' category accounts for {dist[dominant]:.0f}% of contributing factors. "
f"Targeted improvements in this area will yield the highest return.")
if report.coverage_gaps:
lessons.append(
f"There are {len(report.coverage_gaps)} action item coverage gap(s). "
"Ensure every contributing factor category has a corresponding remediation action.")
avg_q = (sum(a.quality_score for a in report.action_items) / len(report.action_items)
if report.action_items else 0)
if avg_q < 70:
lessons.append(
f"Average action item quality score is {avg_q:.0f}/100. "
"Make action items more specific with measurable targets and clear ownership.")
if report.timeline.postmortem_on_time is False:
h = report.timeline.postmortem_timeliness_hours
lessons.append(
f"Postmortem was held {h:.0f} hours after resolution, exceeding the "
f"{POSTMORTEM_TARGET_HOURS}-hour target. Schedule postmortems sooner to capture context.")
if not lessons:
lessons.append("This incident was handled within benchmarks. Continue reinforcing "
"current practices and share this postmortem for organizational learning.")
return lessons
# ---------- Output Formatters ----------
def format_text(report: PostmortemReport) -> str:
"""Format the postmortem as plain text."""
L: List[str] = []
W = 72
def h1(title: str) -> None:
L.append(""); L.append("=" * W); L.append(f" {title}"); L.append("=" * W)
def h2(title: str) -> None:
L.append(""); L.append(f"--- {title} ---")
inc = report.incident
h1(f"POSTMORTEM: {inc.title}")
L.append(f" ID: {inc.id} | Severity: {inc.severity} | Service: {inc.service}")
L.append(f" Commander: {inc.commander}")
if inc.affected_services:
L.append(f" Affected services: {', '.join(inc.affected_services)}")
# Executive Summary
h1("EXECUTIVE SUMMARY")
L.append("")
for sentence in report.executive_summary().split(". "):
s = sentence.strip()
if s and not s.endswith("."): s += "."
if s: L.append(f" {s}")
# Timeline Metrics
h1("TIMELINE METRICS")
tm = report.timeline
L.append("")
for label, val, unit in [("MTTD (Time to Detect)", tm.mttd, "min"),
("MTTR (Time to Resolve)", tm.mttr, "min"),
("Time to Mitigate", tm.time_to_mitigate, "min"),
("Time to Declare", tm.time_to_declare, "min"),
("Postmortem Timeliness", tm.postmortem_timeliness_hours, "hrs")]:
L.append(f" {label:<30s} {f'{val:.1f} {unit}' if val is not None else 'N/A'}")
h2("Benchmark Comparison")
for name, d in tm.benchmark_comparison().items():
if "actual_minutes" in d:
st = "PASS" if d["met_benchmark"] else "FAIL"
L.append(f" {name:<25s} actual={d['actual_minutes']}min benchmark={d['benchmark_minutes']}min [{st}]")
elif "actual_hours" in d:
st = "PASS" if d["met_target"] else "FAIL"
L.append(f" {name:<25s} actual={d['actual_hours']}hrs target={d['target_hours']}hrs [{st}]")
# Customer Impact
h1("CUSTOMER IMPACT")
ci = report.customer_impact_summary()
L.append("")
L.append(f" Affected users: {ci['affected_users']:,}")
L.append(f" Failed transactions: {ci['failed_transactions']:,}")
L.append(f" Revenue impact: ,.2f")
L.append(f" Data integrity: {ci['data_integrity']}")
L.append(f" Impact severity: {ci['impact_severity']}")
L.append(f" Comms required: {'Yes' if ci['customer_communication_required'] else 'No'}")
# Root Cause
h1("ROOT CAUSE ANALYSIS")
L.append("")
L.append(f" {report.resolution.get('root_cause', 'Unknown')}")
h2("Contributing Factors")
for f in report.contributing_factors:
L.append(f" [{f.category.upper():<12s} w={f.weight:.2f}] {f.description}")
h2("Factor Distribution")
for cat, pct in sorted(report.factor_distribution.items(), key=lambda x: -x[1]):
if pct > 0:
L.append(f" {cat:<14s} {pct:5.1f}% {_bar(pct)}")
# 5-Whys
h1("5-WHYS ANALYSIS")
for analysis in report.five_whys:
L.append("")
L.append(f" Factor: {analysis.factor.description}")
L.append(f" Theme: {analysis.systemic_theme}")
for i, step in enumerate(analysis.chain):
L.append(f" {i}. {step}")
h2("Theme-Based Recommendations")
for theme, recs in report.theme_recommendations.items():
L.append(f" [{theme.upper()}]")
for rec in recs:
L.append(f" - {rec}")
# Mitigation & Fix
h1("MITIGATION AND RESOLUTION")
h2("Mitigation Steps Taken")
for step in report.resolution.get("mitigation_steps", []):
L.append(f" - {step}")
h2("Permanent Fix")
L.append(f" {report.resolution.get('permanent_fix', 'TBD')}")
# Action Items
h1("ACTION ITEMS")
L.append("")
hdr = f" {'Priority':<10s} {'Type':<14s} {'Owner':<25s} {'Deadline':<12s} {'Quality':<8s} Title"
L.append(hdr)
L.append(" " + "-" * (len(hdr) - 2))
for a in sorted(report.action_items, key=lambda x: PRIORITY_ORDER.get(x.priority, 99)):
flag = " *OVERDUE*" if a.is_past_deadline else ""
L.append(f" {a.priority:<10s} {a.type:<14s} {a.owner:<25s} {a.deadline:<12s} "
f"{a.quality_score:<8d} {a.title}{flag}")
if report.coverage_gaps:
h2("Coverage Gaps")
for gap in report.coverage_gaps:
L.append(f" WARNING: {gap}")
if report.suggested_actions:
h2("Suggested Additional Actions")
for s in report.suggested_actions:
L.append(f" [{s['type'].upper()}] {s['suggestion']}")
L.append(f" Reason: {s['reason']}")
overdue = report.overdue_p1_items()
if overdue:
h2("Overdue P0/P1 Items")
for item in overdue:
L.append(f" OVERDUE: {item['title']} (owner: {item['owner']}, deadline: {item['deadline']})")
# Participants
h1("PARTICIPANTS")
L.append("")
for p in report.participants:
L.append(f" {p.get('name', 'Unknown'):<25s} {p.get('role', '')}")
# Lessons Learned
h1("LESSONS LEARNED")
L.append("")
for i, lesson in enumerate(_generate_lessons(report), 1):
L.append(f" {i}. {lesson}")
L.append("")
L.append("=" * W)
L.append(f" Generated by postmortem_generator v{VERSION}")
L.append("=" * W)
L.append("")
return "\n".join(L)
def format_json(report: PostmortemReport) -> str:
"""Format the postmortem as JSON."""
data = report.to_dict()
data["lessons_learned"] = _generate_lessons(report)
return json.dumps(data, indent=2, default=str)
def format_markdown(report: PostmortemReport) -> str:
"""Format the postmortem as a Markdown document."""
L: List[str] = []
inc = report.incident
L.append(f"# Postmortem: {inc.title}")
L.append("")
L.append("| Field | Value |")
L.append("|-------|-------|")
L.append(f"| **ID** | {inc.id} |")
L.append(f"| **Severity** | {inc.severity} |")
L.append(f"| **Service** | {inc.service} |")
L.append(f"| **Commander** | {inc.commander} |")
if inc.affected_services:
L.append(f"| **Affected Services** | {', '.join(inc.affected_services)} |")
L.append("")
# Executive Summary
L.append("## Executive Summary\n")
L.append(report.executive_summary())
L.append("")
# Timeline Metrics
L.append("## Timeline Metrics\n")
L.append("| Metric | Value | Benchmark | Status |")
L.append("|--------|-------|-----------|--------|")
labels = {"mttd": "MTTD (Time to Detect)", "mttr": "MTTR (Time to Resolve)",
"time_to_mitigate": "Time to Mitigate", "time_to_declare": "Time to Declare",
"postmortem_timeliness": "Postmortem Timeliness"}
for key, label in labels.items():
b = report.timeline.benchmark_comparison().get(key)
if b and "actual_minutes" in b:
st = "PASS" if b["met_benchmark"] else "FAIL"
L.append(f"| {label} | {b['actual_minutes']} min | {b['benchmark_minutes']} min | {st} |")
elif b and "actual_hours" in b:
st = "PASS" if b["met_target"] else "FAIL"
L.append(f"| {label} | {b['actual_hours']} hrs | {b['target_hours']} hrs | {st} |")
L.append("")
# Customer Impact
L.append("## Customer Impact\n")
ci = report.customer_impact_summary()
L.append(f"- **Affected users:** {ci['affected_users']:,}")
L.append(f"- **Failed transactions:** {ci['failed_transactions']:,}")
L.append(f"- **Revenue impact:** ,.2f")
L.append(f"- **Data integrity:** {ci['data_integrity']}")
L.append(f"- **Impact severity:** {ci['impact_severity']}")
L.append(f"- **Customer communication required:** {'Yes' if ci['customer_communication_required'] else 'No'}")
L.append("")
# Root Cause Analysis
L.append("## Root Cause Analysis\n")
L.append(f"**Root cause:** {report.resolution.get('root_cause', 'Unknown')}")
L.append("")
L.append("### Contributing Factors\n")
L.append("| # | Category | Weight | Description |")
L.append("|---|----------|--------|-------------|")
for i, f in enumerate(report.contributing_factors, 1):
L.append(f"| {i} | {f.category} | {f.weight:.2f} | {f.description} |")
L.append("")
L.append("### Factor Distribution\n")
L.append("```")
for cat, pct in sorted(report.factor_distribution.items(), key=lambda x: -x[1]):
if pct > 0:
L.append(f" {cat:<14s} {pct:5.1f}% {_bar(pct, 25)}")
L.append("```")
L.append("")
# 5-Whys
L.append("## 5-Whys Analysis\n")
for analysis in report.five_whys:
L.append(f"### Factor: {analysis.factor.description}")
L.append(f"**Systemic theme:** {analysis.systemic_theme}\n")
for i, step in enumerate(analysis.chain):
L.append(f"{i}. {step}")
L.append("")
L.append("### Theme-Based Recommendations\n")
for theme, recs in report.theme_recommendations.items():
L.append(f"**{theme.capitalize()}:**")
for rec in recs:
L.append(f"- {rec}")
L.append("")
# Mitigation
L.append("## Mitigation and Resolution\n")
L.append("### Mitigation Steps Taken\n")
for step in report.resolution.get("mitigation_steps", []):
L.append(f"- {step}")
L.append("")
L.append("### Permanent Fix\n")
L.append(report.resolution.get("permanent_fix", "TBD"))
L.append("")
# Action Items
L.append("## Action Items\n")
L.append("| Priority | Type | Owner | Deadline | Quality | Title |")
L.append("|----------|------|-------|----------|---------|-------|")
for a in sorted(report.action_items, key=lambda x: PRIORITY_ORDER.get(x.priority, 99)):
flag = " **OVERDUE**" if a.is_past_deadline else ""
L.append(f"| {a.priority} | {a.type} | {a.owner} | {a.deadline} | {a.quality_score}/100 | {a.title}{flag} |")
L.append("")
if report.coverage_gaps:
L.append("### Coverage Gaps\n")
for gap in report.coverage_gaps:
L.append(f"> **WARNING:** {gap}")
L.append("")
if report.suggested_actions:
L.append("### Suggested Additional Actions\n")
for s in report.suggested_actions:
L.append(f"- **[{s['type'].upper()}]** {s['suggestion']}")
L.append(f" - _Reason: {s['reason']}_")
L.append("")
overdue = report.overdue_p1_items()
if overdue:
L.append("### Overdue P0/P1 Items\n")
for item in overdue:
L.append(f"- **{item['title']}** (owner: {item['owner']}, deadline: {item['deadline']})")
L.append("")
# Participants
L.append("## Participants\n")
L.append("| Name | Role |")
L.append("|------|------|")
for p in report.participants:
L.append(f"| {p.get('name', 'Unknown')} | {p.get('role', '')} |")
L.append("")
# Lessons Learned
L.append("## Lessons Learned\n")
for i, lesson in enumerate(_generate_lessons(report), 1):
L.append(f"{i}. {lesson}")
L.append("")
L.append("---")
L.append(f"_Generated by postmortem_generator v{VERSION}_")
L.append("")
return "\n".join(L)
# ---------- Input Loading ----------
def load_input(filepath: Optional[str]) -> Dict[str, Any]:
"""Load incident data from a file path or stdin."""
if filepath:
try:
with open(filepath, "r", encoding="utf-8") as fh:
return json.load(fh)
except FileNotFoundError:
print(f"Error: File not found: {filepath}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as exc:
print(f"Error: Invalid JSON in {filepath}: {exc}", file=sys.stderr)
sys.exit(1)
else:
if sys.stdin.isatty():
print("Error: No input file specified and no data on stdin.", file=sys.stderr)
print("Usage: postmortem_generator.py [data_file] or pipe JSON via stdin.", file=sys.stderr)
sys.exit(1)
try:
return json.load(sys.stdin)
except json.JSONDecodeError as exc:
print(f"Error: Invalid JSON on stdin: {exc}", file=sys.stderr)
sys.exit(1)
def validate_input(data: Dict[str, Any]) -> List[str]:
"""Return a list of validation warnings (non-fatal)."""
warnings: List[str] = []
for key in ("incident", "timeline", "resolution", "action_items"):
if key not in data:
warnings.append(f"Missing '{key}' section")
for ts in ("issue_started", "detected_at", "mitigated_at", "resolved_at"):
if ts not in data.get("timeline", {}):
warnings.append(f"Missing timeline field: {ts}")
res = data.get("resolution", {})
if "root_cause" not in res:
warnings.append("Missing 'root_cause' in resolution")
if not res.get("contributing_factors"):
warnings.append("No contributing factors provided")
return warnings
# ---------- CLI Entry Point ----------
def main() -> None:
"""CLI entry point for postmortem generation."""
parser = argparse.ArgumentParser(
description="Generate structured postmortem reports with 5-Whys analysis.",
epilog="Reads JSON from a file or stdin. Outputs text, JSON, or markdown.")
parser.add_argument("data_file", nargs="?", default=None,
help="JSON file with incident + resolution data (reads stdin if omitted)")
parser.add_argument("--format", choices=["text", "json", "markdown"], default="text",
dest="output_format", help="Output format (default: text)")
args = parser.parse_args()
data = load_input(args.data_file)
warnings = validate_input(data)
for w in warnings:
print(f"Warning: {w}", file=sys.stderr)
report = PostmortemReport(data)
formatters = {"text": format_text, "json": format_json, "markdown": format_markdown}
print(formatters[args.output_format](report))
if __name__ == "__main__":
main()
FILE:scripts/severity_classifier.py
#!/usr/bin/env python3
"""
Severity Classifier - Classify incident severity and generate escalation paths.
Analyses incident data across multiple dimensions (revenue impact, user scope,
data/security risk, service criticality, blast radius) to produce a weighted
severity score and map it to SEV1-SEV4. Generates escalation paths, on-call
routing, SLA impact assessments, and immediate action plans.
Table of Contents:
SeverityLevel - Enum-like severity definitions (SEV1-SEV4)
ImpactAssessment - Parsed impact data from incident input
SeverityScore - Multi-dimensional weighted scoring result
EscalationPath - Generated escalation routing and timelines
ActionPlan - Recommended immediate actions per severity
SLAImpact - SLA breach risk and error-budget assessment
parse_incident_data() - Validate and normalise raw JSON input
compute_dimension_scores() - Score each weighted dimension
classify_severity() - Map composite score to SEV1-SEV4
build_escalation_path() - Generate escalation routing
build_action_plan() - Generate immediate action checklist
assess_sla_impact() - SLA breach risk assessment
format_text() - Human-readable text output
format_json() - Machine-readable JSON output
format_markdown() - Markdown report output
main() - CLI entry point
Usage:
python severity_classifier.py incident.json
python severity_classifier.py incident.json --format json
python severity_classifier.py incident.json --format markdown
cat incident.json | python severity_classifier.py --format text
echo '{"incident":{...}}' | python severity_classifier.py
"""
import argparse
import json
import sys
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple
# ---------- Severity Level Definitions ----------------------------------------
class SeverityLevel:
"""Enum-like container for SEV1 through SEV4 definitions."""
SEV1 = "SEV1"
SEV2 = "SEV2"
SEV3 = "SEV3"
SEV4 = "SEV4"
DEFINITIONS: Dict[str, Dict[str, Any]] = {
"SEV1": {
"label": "Critical",
"description": (
"Complete service outage, confirmed data loss or corruption, "
"active security breach, or more than 50% of users affected."
),
"score_threshold": 0.75,
"response_time_minutes": 5,
"update_cadence_minutes": 15,
"executive_notify": True,
"war_room": True,
},
"SEV2": {
"label": "Major",
"description": (
"Significant service degradation, more than 25% of users "
"affected, no viable workaround, or high revenue impact."
),
"score_threshold": 0.50,
"response_time_minutes": 15,
"update_cadence_minutes": 30,
"executive_notify": False,
"war_room": True,
},
"SEV3": {
"label": "Moderate",
"description": (
"Partial degradation with workaround available, fewer than "
"25% of users affected, limited blast radius."
),
"score_threshold": 0.25,
"response_time_minutes": 30,
"update_cadence_minutes": 60,
"executive_notify": False,
"war_room": False,
},
"SEV4": {
"label": "Minor",
"description": (
"Cosmetic issue, low impact, minimal user effect, "
"informational or non-urgent."
),
"score_threshold": 0.0,
"response_time_minutes": 120,
"update_cadence_minutes": 240,
"executive_notify": False,
"war_room": False,
},
}
@classmethod
def from_score(cls, score: float) -> str:
"""Return the severity level string for a given composite score."""
for level in [cls.SEV1, cls.SEV2, cls.SEV3]:
if score >= cls.DEFINITIONS[level]["score_threshold"]:
return level
return cls.SEV4
@classmethod
def get_definition(cls, level: str) -> Dict[str, Any]:
return cls.DEFINITIONS.get(level, cls.DEFINITIONS[cls.SEV4])
# ---------- Configuration Constants -------------------------------------------
DIMENSION_WEIGHTS: Dict[str, float] = {
"revenue_impact": 0.25,
"user_impact_scope": 0.25,
"data_security_risk": 0.20,
"service_criticality": 0.15,
"blast_radius": 0.15,
}
REVENUE_IMPACT_SCORES: Dict[str, float] = {
"critical": 1.0,
"high": 0.8,
"medium": 0.5,
"low": 0.2,
"none": 0.0,
}
DEGRADATION_SCORES: Dict[str, float] = {
"complete": 1.0,
"major": 0.75,
"partial": 0.50,
"minor": 0.25,
"none": 0.0,
}
ERROR_RATE_THRESHOLDS: List[Tuple[float, float]] = [
(50.0, 1.0),
(25.0, 0.8),
(10.0, 0.6),
(5.0, 0.4),
(1.0, 0.2),
]
LATENCY_P99_THRESHOLDS_MS: List[Tuple[float, float]] = [
(10000, 1.0),
(5000, 0.8),
(2000, 0.6),
(1000, 0.4),
(500, 0.2),
]
SLA_TIERS: Dict[str, Dict[str, Any]] = {
"SEV1": {
"target_resolution_hours": 1,
"target_response_minutes": 5,
"sla_percentage": 99.95,
"monthly_error_budget_minutes": 21.6,
},
"SEV2": {
"target_resolution_hours": 4,
"target_response_minutes": 15,
"sla_percentage": 99.9,
"monthly_error_budget_minutes": 43.2,
},
"SEV3": {
"target_resolution_hours": 24,
"target_response_minutes": 60,
"sla_percentage": 99.5,
"monthly_error_budget_minutes": 216.0,
},
"SEV4": {
"target_resolution_hours": 72,
"target_response_minutes": 480,
"sla_percentage": 99.0,
"monthly_error_budget_minutes": 432.0,
},
}
ESCALATION_TEMPLATES: Dict[str, Dict[str, Any]] = {
"SEV1": {
"initial_notify": ["on-call-primary", "on-call-secondary", "engineering-manager"],
"escalate_after_minutes": 15,
"escalate_to": ["vp-engineering", "cto"],
"bridge_required": True,
"status_page_update": True,
"customer_comms": True,
},
"SEV2": {
"initial_notify": ["on-call-primary", "on-call-secondary"],
"escalate_after_minutes": 30,
"escalate_to": ["engineering-manager"],
"bridge_required": True,
"status_page_update": True,
"customer_comms": False,
},
"SEV3": {
"initial_notify": ["on-call-primary"],
"escalate_after_minutes": 120,
"escalate_to": ["on-call-secondary"],
"bridge_required": False,
"status_page_update": False,
"customer_comms": False,
},
"SEV4": {
"initial_notify": ["on-call-primary"],
"escalate_after_minutes": 480,
"escalate_to": [],
"bridge_required": False,
"status_page_update": False,
"customer_comms": False,
},
}
# ---------- Data Model Classes ------------------------------------------------
@dataclass
class ImpactAssessment:
"""Parsed and normalised impact data from incident input."""
revenue_impact: str = "none"
affected_users_percentage: float = 0.0
affected_regions: List[str] = field(default_factory=list)
data_integrity_risk: bool = False
security_breach: bool = False
customer_facing: bool = False
degradation_type: str = "none"
workaround_available: bool = True
@dataclass
class SeverityScore:
"""Multi-dimensional scoring result with per-dimension breakdown."""
composite_score: float = 0.0
severity_level: str = SeverityLevel.SEV4
dimensions: Dict[str, float] = field(default_factory=dict)
weighted_dimensions: Dict[str, float] = field(default_factory=dict)
contributing_factors: List[str] = field(default_factory=list)
auto_escalate_reasons: List[str] = field(default_factory=list)
@dataclass
class EscalationPath:
"""Generated escalation routing and notification schedule."""
severity_level: str = SeverityLevel.SEV4
immediate_notify: List[str] = field(default_factory=list)
escalation_chain: List[Dict[str, Any]] = field(default_factory=list)
cross_team_notify: List[str] = field(default_factory=list)
war_room_required: bool = False
bridge_link: str = ""
status_page_update: bool = False
customer_comms_required: bool = False
suggested_smes: List[str] = field(default_factory=list)
@dataclass
class ActionPlan:
"""Recommended immediate actions checklist for the incident."""
severity_level: str = SeverityLevel.SEV4
immediate_actions: List[str] = field(default_factory=list)
diagnostic_steps: List[str] = field(default_factory=list)
communication_actions: List[str] = field(default_factory=list)
rollback_assessment: Dict[str, Any] = field(default_factory=dict)
@dataclass
class SLAImpact:
"""SLA breach risk and error-budget assessment."""
severity_level: str = SeverityLevel.SEV4
sla_tier: Dict[str, Any] = field(default_factory=dict)
breach_risk: str = "low"
error_budget_impact_minutes: float = 0.0
remaining_budget_percentage: float = 100.0
estimated_time_to_breach_minutes: float = 0.0
recommendations: List[str] = field(default_factory=list)
# ---------- Input Parsing -----------------------------------------------------
def parse_incident_data(raw: Dict[str, Any]) -> Tuple[Dict, ImpactAssessment, Dict, Dict]:
"""
Validate and normalise raw JSON input into typed structures.
Returns:
(incident_info, impact_assessment, signals, context)
"""
incident = raw.get("incident", {})
if not incident:
raise ValueError("Input must contain an 'incident' key with title and description.")
impact_raw = raw.get("impact", {})
impact = ImpactAssessment(
revenue_impact=impact_raw.get("revenue_impact", "none"),
affected_users_percentage=float(impact_raw.get("affected_users_percentage", 0)),
affected_regions=impact_raw.get("affected_regions", []),
data_integrity_risk=bool(impact_raw.get("data_integrity_risk", False)),
security_breach=bool(impact_raw.get("security_breach", False)),
customer_facing=bool(impact_raw.get("customer_facing", False)),
degradation_type=impact_raw.get("degradation_type", "none"),
workaround_available=bool(impact_raw.get("workaround_available", True)),
)
signals = raw.get("signals", {})
context = raw.get("context", {})
return incident, impact, signals, context
# ---------- Core Scoring Engine -----------------------------------------------
def _score_revenue_impact(impact: ImpactAssessment) -> Tuple[float, List[str]]:
"""Score the revenue impact dimension (0.0 - 1.0)."""
factors: List[str] = []
score = REVENUE_IMPACT_SCORES.get(impact.revenue_impact, 0.0)
if impact.customer_facing and score >= 0.5:
score = min(1.0, score + 0.1)
factors.append("Customer-facing service with revenue exposure")
if not impact.workaround_available and score >= 0.5:
score = min(1.0, score + 0.1)
factors.append("No workaround available, prolonging revenue impact")
if score >= 0.8:
factors.append(f"Revenue impact rated '{impact.revenue_impact}'")
return score, factors
def _score_user_impact(impact: ImpactAssessment, signals: Dict) -> Tuple[float, List[str]]:
"""Score the user impact scope dimension (0.0 - 1.0)."""
factors: List[str] = []
pct = impact.affected_users_percentage
if pct >= 75:
score = 1.0
elif pct >= 50:
score = 0.85
elif pct >= 25:
score = 0.65
elif pct >= 10:
score = 0.45
elif pct >= 1:
score = 0.25
else:
score = 0.1
if pct > 0:
factors.append(f"{pct}% of users affected")
customer_reports = signals.get("customer_reports", 0)
if customer_reports > 20:
score = min(1.0, score + 0.15)
factors.append(f"{customer_reports} customer reports received")
elif customer_reports > 5:
score = min(1.0, score + 0.08)
factors.append(f"{customer_reports} customer reports received")
degradation_boost = DEGRADATION_SCORES.get(impact.degradation_type, 0.0) * 0.15
score = min(1.0, score + degradation_boost)
if impact.degradation_type in ("complete", "major"):
factors.append(f"Degradation type: {impact.degradation_type}")
return score, factors
def _score_data_security(impact: ImpactAssessment) -> Tuple[float, List[str]]:
"""Score the data/security risk dimension (0.0 - 1.0)."""
factors: List[str] = []
score = 0.0
if impact.security_breach:
score = 1.0
factors.append("Active security breach confirmed")
elif impact.data_integrity_risk:
score = 0.8
factors.append("Data integrity at risk")
if impact.customer_facing and impact.data_integrity_risk:
score = min(1.0, score + 0.1)
factors.append("Customer data potentially affected")
return score, factors
def _score_service_criticality(signals: Dict, context: Dict) -> Tuple[float, List[str]]:
"""Score service criticality based on signals and dependency graph."""
factors: List[str] = []
score = 0.0
dependent_services = signals.get("dependent_services", [])
dep_count = len(dependent_services)
if dep_count >= 5:
score = 1.0
factors.append(f"{dep_count} dependent services (critical hub)")
elif dep_count >= 3:
score = 0.75
factors.append(f"{dep_count} dependent services")
elif dep_count >= 1:
score = 0.5
factors.append(f"{dep_count} dependent service(s)")
else:
score = 0.2
affected_endpoints = signals.get("affected_endpoints", [])
if len(affected_endpoints) >= 5:
score = min(1.0, score + 0.15)
factors.append(f"{len(affected_endpoints)} endpoints affected")
elif len(affected_endpoints) >= 2:
score = min(1.0, score + 0.08)
factors.append(f"{len(affected_endpoints)} endpoints affected")
return score, factors
def _score_blast_radius(
impact: ImpactAssessment, signals: Dict
) -> Tuple[float, List[str]]:
"""Score blast radius from region spread, alert volume, and error rate."""
factors: List[str] = []
score = 0.0
region_count = len(impact.affected_regions)
if region_count >= 3:
score = 0.9
factors.append(f"Spanning {region_count} regions")
elif region_count == 2:
score = 0.6
factors.append(f"Spanning {region_count} regions")
elif region_count == 1:
score = 0.3
error_rate = signals.get("error_rate_percentage", 0.0)
for threshold, rate_score in ERROR_RATE_THRESHOLDS:
if error_rate >= threshold:
score = max(score, rate_score)
factors.append(f"Error rate at {error_rate}%")
break
latency = signals.get("latency_p99_ms", 0)
for threshold, lat_score in LATENCY_P99_THRESHOLDS_MS:
if latency >= threshold:
score = max(score, lat_score)
factors.append(f"P99 latency at {latency}ms")
break
alert_count = signals.get("alert_count", 0)
if alert_count >= 20:
score = min(1.0, score + 0.15)
factors.append(f"{alert_count} alerts firing")
elif alert_count >= 10:
score = min(1.0, score + 0.08)
factors.append(f"{alert_count} alerts firing")
return score, factors
def compute_dimension_scores(
impact: ImpactAssessment, signals: Dict, context: Dict
) -> SeverityScore:
"""Score each weighted dimension and produce a composite severity score."""
dimensions: Dict[str, float] = {}
weighted: Dict[str, float] = {}
all_factors: List[str] = []
auto_escalate: List[str] = []
# -- Revenue impact --
rev_score, rev_factors = _score_revenue_impact(impact)
dimensions["revenue_impact"] = round(rev_score, 3)
weighted["revenue_impact"] = round(rev_score * DIMENSION_WEIGHTS["revenue_impact"], 3)
all_factors.extend(rev_factors)
# -- User impact scope --
user_score, user_factors = _score_user_impact(impact, signals)
dimensions["user_impact_scope"] = round(user_score, 3)
weighted["user_impact_scope"] = round(user_score * DIMENSION_WEIGHTS["user_impact_scope"], 3)
all_factors.extend(user_factors)
# -- Data / security risk --
sec_score, sec_factors = _score_data_security(impact)
dimensions["data_security_risk"] = round(sec_score, 3)
weighted["data_security_risk"] = round(sec_score * DIMENSION_WEIGHTS["data_security_risk"], 3)
all_factors.extend(sec_factors)
# -- Service criticality --
svc_score, svc_factors = _score_service_criticality(signals, context)
dimensions["service_criticality"] = round(svc_score, 3)
weighted["service_criticality"] = round(svc_score * DIMENSION_WEIGHTS["service_criticality"], 3)
all_factors.extend(svc_factors)
# -- Blast radius --
blast_score, blast_factors = _score_blast_radius(impact, signals)
dimensions["blast_radius"] = round(blast_score, 3)
weighted["blast_radius"] = round(blast_score * DIMENSION_WEIGHTS["blast_radius"], 3)
all_factors.extend(blast_factors)
composite = sum(weighted.values())
# -- Auto-escalation overrides --
if impact.security_breach:
composite = max(composite, 0.85)
auto_escalate.append("Security breach triggers automatic SEV1 escalation")
if impact.data_integrity_risk and impact.customer_facing:
composite = max(composite, 0.76)
auto_escalate.append("Customer-facing data integrity risk triggers SEV1 floor")
if impact.affected_users_percentage >= 50 and impact.degradation_type == "complete":
composite = max(composite, 0.80)
auto_escalate.append("Complete outage affecting 50%+ users triggers SEV1 floor")
composite = min(1.0, round(composite, 3))
severity_level = SeverityLevel.from_score(composite)
return SeverityScore(
composite_score=composite,
severity_level=severity_level,
dimensions=dimensions,
weighted_dimensions=weighted,
contributing_factors=all_factors,
auto_escalate_reasons=auto_escalate,
)
# ---------- Classification Wrapper --------------------------------------------
def classify_severity(
incident: Dict, impact: ImpactAssessment, signals: Dict, context: Dict
) -> SeverityScore:
"""
Top-level classification: compute scores and return the final
SeverityScore including the resolved severity level.
"""
return compute_dimension_scores(impact, signals, context)
# ---------- Escalation Path Builder -------------------------------------------
def build_escalation_path(
severity_score: SeverityScore,
signals: Dict,
context: Dict,
) -> EscalationPath:
"""Generate the escalation routing based on severity and context."""
level = severity_score.severity_level
template = ESCALATION_TEMPLATES.get(level, ESCALATION_TEMPLATES["SEV4"])
on_call = context.get("on_call", {})
primary = on_call.get("primary", "on-call-primary@company.com")
secondary = on_call.get("secondary", "on-call-secondary@company.com")
immediate: List[str] = []
for role in template["initial_notify"]:
if role == "on-call-primary":
immediate.append(primary)
elif role == "on-call-secondary":
immediate.append(secondary)
else:
immediate.append(role)
chain: List[Dict[str, Any]] = []
if template["escalate_to"]:
chain.append({
"trigger_after_minutes": template["escalate_after_minutes"],
"notify": template["escalate_to"],
"reason": f"No resolution within {template['escalate_after_minutes']} minutes",
})
sev_def = SeverityLevel.get_definition(level)
if sev_def.get("executive_notify"):
chain.append({
"trigger_after_minutes": 15,
"notify": ["vp-engineering", "cto"],
"reason": "SEV1 executive notification policy",
})
cross_team: List[str] = []
dependent_services = signals.get("dependent_services", [])
for svc in dependent_services:
cross_team.append(f"{svc}-team")
suggested_smes: List[str] = []
affected_endpoints = signals.get("affected_endpoints", [])
if affected_endpoints:
suggested_smes.append(f"API owner for: {', '.join(affected_endpoints[:3])}")
if dependent_services:
suggested_smes.append(f"Service owners: {', '.join(dependent_services[:3])}")
ongoing = context.get("ongoing_incidents", [])
if ongoing:
suggested_smes.append("Incident coordinator (multiple active incidents)")
bridge_link = ""
if template["bridge_required"]:
bridge_link = f"https://bridge.company.com/incident-{level.lower()}"
return EscalationPath(
severity_level=level,
immediate_notify=immediate,
escalation_chain=chain,
cross_team_notify=cross_team,
war_room_required=template["bridge_required"],
bridge_link=bridge_link,
status_page_update=template["status_page_update"],
customer_comms_required=template.get("customer_comms", False),
suggested_smes=suggested_smes,
)
# ---------- Action Plan Builder -----------------------------------------------
def build_action_plan(
severity_score: SeverityScore,
incident: Dict,
impact: ImpactAssessment,
signals: Dict,
context: Dict,
) -> ActionPlan:
"""Generate the immediate action plan for the classified incident."""
level = severity_score.severity_level
sev_def = SeverityLevel.get_definition(level)
# -- Immediate actions --
immediate: List[str] = [
f"Acknowledge incident within {sev_def['response_time_minutes']} minutes",
"Join the war room / bridge call" if sev_def["war_room"] else "Open incident channel",
f"Post status update every {sev_def['update_cadence_minutes']} minutes",
]
if level in (SeverityLevel.SEV1, SeverityLevel.SEV2):
immediate.append("Page secondary on-call if primary unresponsive within 5 minutes")
immediate.append("Begin impact quantification for executive update")
if impact.security_breach:
immediate.insert(0, "CRITICAL: Initiate security incident response playbook")
immediate.append("Engage security team immediately")
immediate.append("Preserve forensic evidence -- do not restart services yet")
if impact.data_integrity_risk:
immediate.append("Halt writes to affected data stores if safe to do so")
immediate.append("Begin data integrity verification")
# -- Diagnostic steps --
diagnostics: List[str] = [
"Check service dashboards and recent metric trends",
"Review application logs for error spikes",
"Verify upstream and downstream dependency health",
]
error_rate = signals.get("error_rate_percentage", 0)
if error_rate > 10:
diagnostics.append(f"Investigate error rate spike ({error_rate}%)")
latency = signals.get("latency_p99_ms", 0)
if latency > 2000:
diagnostics.append(f"Investigate latency degradation (P99 = {latency}ms)")
affected_endpoints = signals.get("affected_endpoints", [])
if affected_endpoints:
diagnostics.append(
f"Trace requests to affected endpoints: {', '.join(affected_endpoints[:5])}"
)
dependent_services = signals.get("dependent_services", [])
if dependent_services:
diagnostics.append(
f"Check health of dependent services: {', '.join(dependent_services)}"
)
# -- Communication actions --
comms: List[str] = []
if sev_def.get("executive_notify"):
comms.append("Draft executive summary within 15 minutes")
if level in (SeverityLevel.SEV1, SeverityLevel.SEV2):
comms.append("Post initial status page update")
comms.append("Notify customer success team for proactive outreach")
comms.append(f"Schedule post-incident review within 48 hours")
# -- Rollback assessment --
recent_deploys = context.get("recent_deployments", [])
rollback: Dict[str, Any] = {"recent_deployment_detected": False, "recommendation": ""}
if recent_deploys:
latest = recent_deploys[0]
rollback["recent_deployment_detected"] = True
rollback["service"] = latest.get("service", "unknown")
rollback["version"] = latest.get("version", "unknown")
rollback["deployed_at"] = latest.get("deployed_at", "unknown")
detected_at = incident.get("detected_at", "")
deploy_time = latest.get("deployed_at", "")
if detected_at and deploy_time:
try:
det = datetime.fromisoformat(detected_at.replace("Z", "+00:00"))
dep = datetime.fromisoformat(deploy_time.replace("Z", "+00:00"))
delta_minutes = (det - dep).total_seconds() / 60
rollback["minutes_since_deploy"] = round(delta_minutes, 1)
if 0 < delta_minutes < 120:
rollback["recommendation"] = (
f"STRONG: Deployment of {latest.get('service')} v{latest.get('version')} "
f"occurred {round(delta_minutes)} minutes before detection. "
"Consider immediate rollback."
)
else:
rollback["recommendation"] = (
"Recent deployment is outside the typical correlation window. "
"Investigate other root causes first."
)
except (ValueError, TypeError):
rollback["recommendation"] = (
"Unable to parse timestamps. Manually assess deployment correlation."
)
else:
rollback["recommendation"] = (
"No recent deployments detected. Focus on infrastructure and dependency investigation."
)
return ActionPlan(
severity_level=level,
immediate_actions=immediate,
diagnostic_steps=diagnostics,
communication_actions=comms,
rollback_assessment=rollback,
)
# ---------- SLA Impact Assessment ---------------------------------------------
def assess_sla_impact(
severity_score: SeverityScore,
impact: ImpactAssessment,
signals: Dict,
) -> SLAImpact:
"""Calculate SLA breach risk and error-budget consumption."""
level = severity_score.severity_level
tier = SLA_TIERS.get(level, SLA_TIERS["SEV4"])
# Estimate ongoing burn rate (minutes of budget consumed per real minute)
user_pct = impact.affected_users_percentage / 100.0
degradation_factor = DEGRADATION_SCORES.get(impact.degradation_type, 0.25)
burn_rate = user_pct * degradation_factor
if burn_rate <= 0:
burn_rate = 0.01 # minimum if incident is open
monthly_budget = tier["monthly_error_budget_minutes"]
# Assume 30% of budget already consumed this month for conservative estimate
assumed_consumed_pct = 30.0
remaining_budget = monthly_budget * (1 - assumed_consumed_pct / 100.0)
if burn_rate > 0:
time_to_breach = remaining_budget / burn_rate
else:
time_to_breach = float("inf")
# Classify breach risk
if time_to_breach <= 30:
breach_risk = "critical"
elif time_to_breach <= 120:
breach_risk = "high"
elif time_to_breach <= 480:
breach_risk = "medium"
else:
breach_risk = "low"
budget_impact_per_hour = burn_rate * 60
error_budget_impact = round(budget_impact_per_hour, 2)
remaining_pct = round(
max(0.0, (remaining_budget / monthly_budget) * 100.0), 1
)
recommendations: List[str] = []
if breach_risk == "critical":
recommendations.append(
"SLA breach imminent. Prioritize resolution above all other work."
)
recommendations.append(
"Prepare customer communication about potential SLA credit."
)
elif breach_risk == "high":
recommendations.append(
"SLA breach likely within hours. Escalate to ensure rapid resolution."
)
elif breach_risk == "medium":
recommendations.append(
"Monitor error budget consumption. Resolve before end of business."
)
else:
recommendations.append(
"SLA impact is contained. Continue standard incident response."
)
recommendations.append(
f"Current burn rate: {round(burn_rate * 100, 1)}% of error budget per minute"
)
recommendations.append(
f"Estimated time to SLA breach: {round(time_to_breach, 0)} minutes "
f"({round(time_to_breach / 60, 1)} hours)"
)
return SLAImpact(
severity_level=level,
sla_tier=tier,
breach_risk=breach_risk,
error_budget_impact_minutes=error_budget_impact,
remaining_budget_percentage=remaining_pct,
estimated_time_to_breach_minutes=round(time_to_breach, 1),
recommendations=recommendations,
)
# ---------- Output Formatters -------------------------------------------------
def _header_line(char: str, width: int = 72) -> str:
return char * width
def format_text(
incident: Dict,
severity_score: SeverityScore,
escalation: EscalationPath,
action_plan: ActionPlan,
sla_impact: SLAImpact,
) -> str:
"""Render a human-readable text report."""
lines: List[str] = []
w = 72
lines.append(_header_line("=", w))
lines.append("INCIDENT SEVERITY CLASSIFICATION REPORT")
lines.append(_header_line("=", w))
lines.append("")
# -- Incident Summary --
lines.append(f"Title: {incident.get('title', 'N/A')}")
lines.append(f"Service: {incident.get('service', 'N/A')}")
lines.append(f"Detected: {incident.get('detected_at', 'N/A')}")
lines.append(f"Reporter: {incident.get('reporter', 'N/A')}")
lines.append("")
# -- Severity --
sev_def = SeverityLevel.get_definition(severity_score.severity_level)
lines.append(_header_line("-", w))
lines.append(f"SEVERITY: {severity_score.severity_level} ({sev_def['label']})")
lines.append(f"Composite Score: {severity_score.composite_score:.3f}")
lines.append(_header_line("-", w))
lines.append(f" {sev_def['description']}")
lines.append("")
# -- Dimension Breakdown --
lines.append("Dimension Scores:")
for dim, raw in severity_score.dimensions.items():
wt = severity_score.weighted_dimensions.get(dim, 0)
weight_cfg = DIMENSION_WEIGHTS.get(dim, 0)
label = dim.replace("_", " ").title()
lines.append(f" {label:<25s} raw={raw:.3f} weight={weight_cfg:.2f} weighted={wt:.3f}")
lines.append("")
if severity_score.contributing_factors:
lines.append("Contributing Factors:")
for f in severity_score.contributing_factors:
lines.append(f" - {f}")
lines.append("")
if severity_score.auto_escalate_reasons:
lines.append("Auto-Escalation Overrides:")
for r in severity_score.auto_escalate_reasons:
lines.append(f" * {r}")
lines.append("")
# -- Escalation Path --
lines.append(_header_line("-", w))
lines.append("ESCALATION PATH")
lines.append(_header_line("-", w))
lines.append(f"Immediate Notify: {', '.join(escalation.immediate_notify)}")
if escalation.war_room_required:
lines.append(f"War Room: Required ({escalation.bridge_link})")
else:
lines.append("War Room: Not required")
lines.append(f"Status Page: {'Update required' if escalation.status_page_update else 'No update needed'}")
lines.append(f"Customer Comms: {'Required' if escalation.customer_comms_required else 'Not required'}")
lines.append("")
if escalation.escalation_chain:
lines.append("Escalation Chain:")
for step in escalation.escalation_chain:
lines.append(
f" After {step['trigger_after_minutes']}min -> "
f"Notify: {', '.join(step['notify'])} ({step['reason']})"
)
lines.append("")
if escalation.cross_team_notify:
lines.append(f"Cross-Team Notify: {', '.join(escalation.cross_team_notify)}")
if escalation.suggested_smes:
lines.append("Suggested SMEs:")
for sme in escalation.suggested_smes:
lines.append(f" - {sme}")
lines.append("")
# -- Action Plan --
lines.append(_header_line("-", w))
lines.append("ACTION PLAN")
lines.append(_header_line("-", w))
lines.append("Immediate Actions:")
for i, action in enumerate(action_plan.immediate_actions, 1):
lines.append(f" {i}. {action}")
lines.append("")
lines.append("Diagnostic Steps:")
for i, step in enumerate(action_plan.diagnostic_steps, 1):
lines.append(f" {i}. {step}")
lines.append("")
lines.append("Communication Actions:")
for i, action in enumerate(action_plan.communication_actions, 1):
lines.append(f" {i}. {action}")
lines.append("")
rb = action_plan.rollback_assessment
lines.append("Rollback Assessment:")
if rb.get("recent_deployment_detected"):
lines.append(f" Recent Deploy: {rb.get('service', '?')} v{rb.get('version', '?')}")
lines.append(f" Deployed At: {rb.get('deployed_at', '?')}")
if "minutes_since_deploy" in rb:
lines.append(f" Minutes Before Detection: {rb['minutes_since_deploy']}")
lines.append(f" Recommendation: {rb.get('recommendation', 'N/A')}")
lines.append("")
# -- SLA Impact --
lines.append(_header_line("-", w))
lines.append("SLA IMPACT ASSESSMENT")
lines.append(_header_line("-", w))
lines.append(f"Breach Risk: {sla_impact.breach_risk.upper()}")
lines.append(f"Error Budget Impact: {sla_impact.error_budget_impact_minutes} min/hr")
lines.append(f"Remaining Budget: {sla_impact.remaining_budget_percentage}%")
lines.append(f"Est. Time to Breach: {sla_impact.estimated_time_to_breach_minutes} min")
tier = sla_impact.sla_tier
lines.append(f"Target Resolution: {tier.get('target_resolution_hours', '?')} hours")
lines.append(f"Target Response: {tier.get('target_response_minutes', '?')} minutes")
lines.append("")
if sla_impact.recommendations:
lines.append("SLA Recommendations:")
for rec in sla_impact.recommendations:
lines.append(f" - {rec}")
lines.append("")
lines.append(_header_line("=", w))
return "\n".join(lines)
def format_json(
incident: Dict,
severity_score: SeverityScore,
escalation: EscalationPath,
action_plan: ActionPlan,
sla_impact: SLAImpact,
) -> str:
"""Render a machine-readable JSON report."""
report = {
"classification_timestamp": datetime.now(timezone.utc).isoformat(),
"incident": incident,
"severity": asdict(severity_score),
"severity_definition": SeverityLevel.get_definition(severity_score.severity_level),
"escalation": asdict(escalation),
"action_plan": asdict(action_plan),
"sla_impact": asdict(sla_impact),
}
return json.dumps(report, indent=2, default=str)
def format_markdown(
incident: Dict,
severity_score: SeverityScore,
escalation: EscalationPath,
action_plan: ActionPlan,
sla_impact: SLAImpact,
) -> str:
"""Render a Markdown report suitable for incident tickets or wikis."""
lines: List[str] = []
sev_def = SeverityLevel.get_definition(severity_score.severity_level)
lines.append(f"# Incident Severity Classification: {severity_score.severity_level}")
lines.append("")
lines.append(f"**Classified:** {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}")
lines.append("")
lines.append("## Incident Summary")
lines.append("")
lines.append(f"| Field | Value |")
lines.append(f"|-------|-------|")
lines.append(f"| Title | {incident.get('title', 'N/A')} |")
lines.append(f"| Service | {incident.get('service', 'N/A')} |")
lines.append(f"| Detected | {incident.get('detected_at', 'N/A')} |")
lines.append(f"| Reporter | {incident.get('reporter', 'N/A')} |")
lines.append("")
lines.append("## Severity Classification")
lines.append("")
lines.append(
f"> **{severity_score.severity_level} -- {sev_def['label']}** "
f"(Score: {severity_score.composite_score:.3f})"
)
lines.append(f">")
lines.append(f"> {sev_def['description']}")
lines.append("")
lines.append("### Dimension Scores")
lines.append("")
lines.append("| Dimension | Raw | Weight | Weighted |")
lines.append("|-----------|-----|--------|----------|")
for dim, raw in severity_score.dimensions.items():
wt = severity_score.weighted_dimensions.get(dim, 0)
weight_cfg = DIMENSION_WEIGHTS.get(dim, 0)
label = dim.replace("_", " ").title()
lines.append(f"| {label} | {raw:.3f} | {weight_cfg:.2f} | {wt:.3f} |")
lines.append("")
if severity_score.contributing_factors:
lines.append("### Contributing Factors")
lines.append("")
for f in severity_score.contributing_factors:
lines.append(f"- {f}")
lines.append("")
if severity_score.auto_escalate_reasons:
lines.append("### Auto-Escalation Overrides")
lines.append("")
for r in severity_score.auto_escalate_reasons:
lines.append(f"- **{r}**")
lines.append("")
lines.append("## Escalation Path")
lines.append("")
lines.append(f"**Immediate Notify:** {', '.join(escalation.immediate_notify)}")
lines.append("")
if escalation.war_room_required:
lines.append(f"**War Room:** [Join Bridge]({escalation.bridge_link})")
else:
lines.append("**War Room:** Not required")
lines.append("")
if escalation.escalation_chain:
lines.append("### Escalation Chain")
lines.append("")
for step in escalation.escalation_chain:
lines.append(
f"- **After {step['trigger_after_minutes']} min:** "
f"Notify {', '.join(step['notify'])} -- {step['reason']}"
)
lines.append("")
if escalation.cross_team_notify:
lines.append(f"**Cross-Team:** {', '.join(escalation.cross_team_notify)}")
lines.append("")
if escalation.suggested_smes:
lines.append("### Suggested SMEs")
lines.append("")
for sme in escalation.suggested_smes:
lines.append(f"- {sme}")
lines.append("")
lines.append("## Action Plan")
lines.append("")
lines.append("### Immediate Actions")
lines.append("")
for i, action in enumerate(action_plan.immediate_actions, 1):
lines.append(f"{i}. {action}")
lines.append("")
lines.append("### Diagnostic Steps")
lines.append("")
for i, step in enumerate(action_plan.diagnostic_steps, 1):
lines.append(f"{i}. {step}")
lines.append("")
lines.append("### Communication")
lines.append("")
for i, action in enumerate(action_plan.communication_actions, 1):
lines.append(f"{i}. {action}")
lines.append("")
rb = action_plan.rollback_assessment
lines.append("### Rollback Assessment")
lines.append("")
if rb.get("recent_deployment_detected"):
lines.append(
f"| Deploy | {rb.get('service', '?')} v{rb.get('version', '?')} |"
)
lines.append(f"|--------|------|")
lines.append(f"| Deployed At | {rb.get('deployed_at', '?')} |")
if "minutes_since_deploy" in rb:
lines.append(f"| Minutes Before Detection | {rb['minutes_since_deploy']} |")
lines.append("")
lines.append(f"**Recommendation:** {rb.get('recommendation', 'N/A')}")
lines.append("")
lines.append("## SLA Impact")
lines.append("")
tier = sla_impact.sla_tier
lines.append(f"| Metric | Value |")
lines.append(f"|--------|-------|")
lines.append(f"| Breach Risk | **{sla_impact.breach_risk.upper()}** |")
lines.append(f"| Error Budget Impact | {sla_impact.error_budget_impact_minutes} min/hr |")
lines.append(f"| Remaining Budget | {sla_impact.remaining_budget_percentage}% |")
lines.append(f"| Est. Time to Breach | {sla_impact.estimated_time_to_breach_minutes} min |")
lines.append(f"| Target Resolution | {tier.get('target_resolution_hours', '?')} hours |")
lines.append(f"| Target Response | {tier.get('target_response_minutes', '?')} minutes |")
lines.append("")
if sla_impact.recommendations:
lines.append("### SLA Recommendations")
lines.append("")
for rec in sla_impact.recommendations:
lines.append(f"- {rec}")
lines.append("")
lines.append("---")
lines.append("*Generated by severity_classifier.py*")
return "\n".join(lines)
# ---------- CLI Entry Point ---------------------------------------------------
def main() -> None:
"""Parse arguments, read input, classify, and emit output."""
parser = argparse.ArgumentParser(
description="Classify incident severity and generate escalation paths.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""\
examples:
%(prog)s incident.json
%(prog)s incident.json --format json
%(prog)s incident.json --format markdown
cat incident.json | %(prog)s
cat incident.json | %(prog)s --format json
""",
)
parser.add_argument(
"data_file",
nargs="?",
default=None,
help="JSON file with incident data (reads stdin if omitted)",
)
parser.add_argument(
"--format",
choices=["text", "json", "markdown"],
default="text",
dest="output_format",
help="Output format (default: text)",
)
args = parser.parse_args()
# -- Read input --
try:
if args.data_file:
with open(args.data_file, "r", encoding="utf-8") as fh:
raw_data = json.load(fh)
else:
if sys.stdin.isatty():
parser.error("No input file provided and stdin is a terminal. Pipe JSON or pass a file.")
raw_data = json.load(sys.stdin)
except json.JSONDecodeError as exc:
print(f"Error: invalid JSON input -- {exc}", file=sys.stderr)
sys.exit(1)
except FileNotFoundError:
print(f"Error: file not found -- {args.data_file}", file=sys.stderr)
sys.exit(1)
except IOError as exc:
print(f"Error: could not read input -- {exc}", file=sys.stderr)
sys.exit(1)
# -- Parse and validate --
try:
incident, impact, signals, context = parse_incident_data(raw_data)
except ValueError as exc:
print(f"Error: {exc}", file=sys.stderr)
sys.exit(1)
# -- Classify --
severity_score = classify_severity(incident, impact, signals, context)
# -- Build outputs --
escalation = build_escalation_path(severity_score, signals, context)
action_plan = build_action_plan(severity_score, incident, impact, signals, context)
sla_impact = assess_sla_impact(severity_score, impact, signals)
# -- Format and print --
if args.output_format == "json":
output = format_json(incident, severity_score, escalation, action_plan, sla_impact)
elif args.output_format == "markdown":
output = format_markdown(incident, severity_score, escalation, action_plan, sla_impact)
else:
output = format_text(incident, severity_score, escalation, action_plan, sla_impact)
print(output)
# -- Exit code reflects severity --
if severity_score.severity_level == SeverityLevel.SEV1:
sys.exit(2)
elif severity_score.severity_level == SeverityLevel.SEV2:
sys.exit(1)
else:
sys.exit(0)
if __name__ == "__main__":
main()
FILE:scripts/timeline_reconstructor.py
#!/usr/bin/env python3
"""
Timeline Reconstructor
Reconstructs incident timelines from timestamped events (logs, alerts, Slack messages).
Identifies incident phases, calculates durations, and performs gap analysis.
This tool processes chronological event data and creates a coherent narrative
of how an incident progressed from detection through resolution.
Usage:
python timeline_reconstructor.py --input events.json --output timeline.md
python timeline_reconstructor.py --input events.json --detect-phases --gap-analysis
cat events.json | python timeline_reconstructor.py --format text
"""
import argparse
import json
import sys
import re
from datetime import datetime, timezone, timedelta
from typing import Dict, List, Optional, Any, Tuple
from collections import defaultdict, namedtuple
# Event data structure
Event = namedtuple('Event', ['timestamp', 'source', 'type', 'message', 'severity', 'actor', 'metadata'])
# Phase data structure
Phase = namedtuple('Phase', ['name', 'start_time', 'end_time', 'duration', 'events', 'description'])
class TimelineReconstructor:
"""
Reconstructs incident timelines from disparate event sources.
Identifies phases, calculates metrics, and performs gap analysis.
"""
def __init__(self):
"""Initialize the reconstructor with phase detection rules and templates."""
self.phase_patterns = self._load_phase_patterns()
self.event_types = self._load_event_types()
self.severity_mapping = self._load_severity_mapping()
self.gap_thresholds = self._load_gap_thresholds()
def _load_phase_patterns(self) -> Dict[str, Dict]:
"""Load patterns for identifying incident phases."""
return {
"detection": {
"keywords": [
"alert", "alarm", "triggered", "fired", "detected", "noticed",
"monitoring", "threshold exceeded", "anomaly", "spike",
"error rate", "latency increase", "timeout", "failure"
],
"event_types": ["alert", "monitoring", "notification"],
"priority": 1,
"description": "Initial detection of the incident through monitoring or observation"
},
"triage": {
"keywords": [
"investigating", "triaging", "assessing", "evaluating",
"checking", "looking into", "analyzing", "reviewing",
"diagnosis", "troubleshooting", "examining"
],
"event_types": ["investigation", "communication", "action"],
"priority": 2,
"description": "Assessment and initial investigation of the incident"
},
"escalation": {
"keywords": [
"escalating", "paging", "calling in", "requesting help",
"engaging", "involving", "notifying", "alerting team",
"incident commander", "war room", "all hands"
],
"event_types": ["escalation", "communication", "notification"],
"priority": 3,
"description": "Escalation to additional resources or higher severity response"
},
"mitigation": {
"keywords": [
"fixing", "patching", "deploying", "rolling back", "restarting",
"scaling", "rerouting", "bypassing", "workaround",
"implementing fix", "applying solution", "remediation"
],
"event_types": ["deployment", "action", "fix"],
"priority": 4,
"description": "Active mitigation efforts to resolve the incident"
},
"resolution": {
"keywords": [
"resolved", "fixed", "restored", "recovered", "back online",
"working", "normal", "stable", "healthy", "operational",
"incident closed", "service restored"
],
"event_types": ["resolution", "confirmation"],
"priority": 5,
"description": "Confirmation that the incident has been resolved"
},
"review": {
"keywords": [
"post-mortem", "retrospective", "review", "lessons learned",
"pir", "post-incident", "analysis", "follow-up",
"action items", "improvements"
],
"event_types": ["review", "documentation"],
"priority": 6,
"description": "Post-incident review and documentation activities"
}
}
def _load_event_types(self) -> Dict[str, Dict]:
"""Load event type classification rules."""
return {
"alert": {
"sources": ["monitoring", "nagios", "datadog", "newrelic", "prometheus"],
"indicators": ["alert", "alarm", "threshold", "metric"],
"severity_boost": 2
},
"log": {
"sources": ["application", "server", "container", "system"],
"indicators": ["error", "exception", "warn", "fail"],
"severity_boost": 1
},
"communication": {
"sources": ["slack", "teams", "email", "chat"],
"indicators": ["message", "notification", "update"],
"severity_boost": 0
},
"deployment": {
"sources": ["ci/cd", "jenkins", "github", "gitlab", "deploy"],
"indicators": ["deploy", "release", "build", "merge"],
"severity_boost": 3
},
"action": {
"sources": ["manual", "script", "automation", "operator"],
"indicators": ["executed", "ran", "performed", "applied"],
"severity_boost": 2
},
"escalation": {
"sources": ["pagerduty", "opsgenie", "oncall", "escalation"],
"indicators": ["paged", "escalated", "notified", "assigned"],
"severity_boost": 3
}
}
def _load_severity_mapping(self) -> Dict[str, int]:
"""Load severity level mappings."""
return {
"critical": 5, "crit": 5, "sev1": 5, "p1": 5,
"high": 4, "major": 4, "sev2": 4, "p2": 4,
"medium": 3, "moderate": 3, "sev3": 3, "p3": 3,
"low": 2, "minor": 2, "sev4": 2, "p4": 2,
"info": 1, "informational": 1, "debug": 1,
"unknown": 0
}
def _load_gap_thresholds(self) -> Dict[str, int]:
"""Load gap analysis thresholds in minutes."""
return {
"detection_to_triage": 15, # Should start investigating within 15 min
"triage_to_mitigation": 30, # Should start mitigation within 30 min
"mitigation_to_resolution": 120, # Should resolve within 2 hours
"communication_gap": 30, # Should communicate every 30 min
"action_gap": 60, # Should take actions every hour
"phase_transition": 45 # Should transition phases within 45 min
}
def reconstruct_timeline(self, events_data: List[Dict]) -> Dict[str, Any]:
"""
Main reconstruction method that processes events and builds timeline.
Args:
events_data: List of event dictionaries
Returns:
Dictionary with timeline analysis and metrics
"""
# Parse and normalize events
events = self._parse_events(events_data)
if not events:
return {"error": "No valid events found"}
# Sort events chronologically
events.sort(key=lambda e: e.timestamp)
# Detect phases
phases = self._detect_phases(events)
# Calculate metrics
metrics = self._calculate_metrics(events, phases)
# Perform gap analysis
gap_analysis = self._analyze_gaps(events, phases)
# Generate timeline narrative
narrative = self._generate_narrative(events, phases)
# Create summary statistics
summary = self._generate_summary(events, phases, metrics)
return {
"timeline": {
"total_events": len(events),
"time_range": {
"start": events[0].timestamp.isoformat(),
"end": events[-1].timestamp.isoformat(),
"duration_minutes": int((events[-1].timestamp - events[0].timestamp).total_seconds() / 60)
},
"phases": [self._phase_to_dict(phase) for phase in phases],
"events": [self._event_to_dict(event) for event in events]
},
"metrics": metrics,
"gap_analysis": gap_analysis,
"narrative": narrative,
"summary": summary,
"reconstruction_timestamp": datetime.now(timezone.utc).isoformat()
}
def _parse_events(self, events_data: List[Dict]) -> List[Event]:
"""Parse raw event data into normalized Event objects."""
events = []
for event_dict in events_data:
try:
# Parse timestamp
timestamp_str = event_dict.get("timestamp", event_dict.get("time", ""))
if not timestamp_str:
continue
timestamp = self._parse_timestamp(timestamp_str)
if not timestamp:
continue
# Extract other fields
source = event_dict.get("source", "unknown")
event_type = self._classify_event_type(event_dict)
message = event_dict.get("message", event_dict.get("description", ""))
severity = self._parse_severity(event_dict.get("severity", event_dict.get("level", "unknown")))
actor = event_dict.get("actor", event_dict.get("user", "system"))
# Extract metadata
metadata = {k: v for k, v in event_dict.items()
if k not in ["timestamp", "time", "source", "type", "message", "severity", "actor"]}
event = Event(
timestamp=timestamp,
source=source,
type=event_type,
message=message,
severity=severity,
actor=actor,
metadata=metadata
)
events.append(event)
except Exception as e:
# Skip invalid events but log them
continue
return events
def _parse_timestamp(self, timestamp_str: str) -> Optional[datetime]:
"""Parse various timestamp formats."""
# Common timestamp formats
formats = [
"%Y-%m-%dT%H:%M:%S.%fZ", # ISO with microseconds
"%Y-%m-%dT%H:%M:%SZ", # ISO without microseconds
"%Y-%m-%d %H:%M:%S", # Standard format
"%m/%d/%Y %H:%M:%S", # US format
"%d/%m/%Y %H:%M:%S", # EU format
"%Y-%m-%d %H:%M:%S.%f", # With microseconds
"%Y%m%d_%H%M%S", # Compact format
]
for fmt in formats:
try:
dt = datetime.strptime(timestamp_str, fmt)
# Ensure timezone awareness
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
except ValueError:
continue
# Try parsing as Unix timestamp
try:
timestamp_float = float(timestamp_str)
return datetime.fromtimestamp(timestamp_float, tz=timezone.utc)
except ValueError:
pass
return None
def _classify_event_type(self, event_dict: Dict) -> str:
"""Classify event type based on source and content."""
source = event_dict.get("source", "").lower()
message = event_dict.get("message", "").lower()
event_type = event_dict.get("type", "").lower()
# Check explicit type first
if event_type in self.event_types:
return event_type
# Classify based on source and content
for type_name, type_info in self.event_types.items():
# Check source patterns
if any(src in source for src in type_info["sources"]):
return type_name
# Check message indicators
if any(indicator in message for indicator in type_info["indicators"]):
return type_name
return "unknown"
def _parse_severity(self, severity_str: str) -> int:
"""Parse severity string to numeric value."""
severity_clean = str(severity_str).lower().strip()
return self.severity_mapping.get(severity_clean, 0)
def _detect_phases(self, events: List[Event]) -> List[Phase]:
"""Detect incident phases based on event patterns."""
phases = []
current_phase = None
phase_events = []
for event in events:
detected_phase = self._identify_phase(event)
if detected_phase != current_phase:
# End current phase if exists
if current_phase and phase_events:
phase_obj = Phase(
name=current_phase,
start_time=phase_events[0].timestamp,
end_time=phase_events[-1].timestamp,
duration=(phase_events[-1].timestamp - phase_events[0].timestamp).total_seconds() / 60,
events=phase_events.copy(),
description=self.phase_patterns[current_phase]["description"]
)
phases.append(phase_obj)
# Start new phase
current_phase = detected_phase
phase_events = [event]
else:
phase_events.append(event)
# Add final phase
if current_phase and phase_events:
phase_obj = Phase(
name=current_phase,
start_time=phase_events[0].timestamp,
end_time=phase_events[-1].timestamp,
duration=(phase_events[-1].timestamp - phase_events[0].timestamp).total_seconds() / 60,
events=phase_events,
description=self.phase_patterns[current_phase]["description"]
)
phases.append(phase_obj)
return self._merge_adjacent_phases(phases)
def _identify_phase(self, event: Event) -> str:
"""Identify which phase an event belongs to."""
message_lower = event.message.lower()
# Score each phase based on keywords and event type
phase_scores = {}
for phase_name, pattern_info in self.phase_patterns.items():
score = 0
# Keyword matching
for keyword in pattern_info["keywords"]:
if keyword in message_lower:
score += 2
# Event type matching
if event.type in pattern_info["event_types"]:
score += 3
# Severity boost for certain phases
if phase_name == "escalation" and event.severity >= 4:
score += 2
phase_scores[phase_name] = score
# Return highest scoring phase, default to triage
if phase_scores and max(phase_scores.values()) > 0:
return max(phase_scores, key=phase_scores.get)
return "triage" # Default phase
def _merge_adjacent_phases(self, phases: List[Phase]) -> List[Phase]:
"""Merge adjacent phases of the same type."""
if not phases:
return phases
merged = []
current_phase = phases[0]
for next_phase in phases[1:]:
if (next_phase.name == current_phase.name and
(next_phase.start_time - current_phase.end_time).total_seconds() < 300): # 5 min gap
# Merge phases
merged_events = current_phase.events + next_phase.events
current_phase = Phase(
name=current_phase.name,
start_time=current_phase.start_time,
end_time=next_phase.end_time,
duration=(next_phase.end_time - current_phase.start_time).total_seconds() / 60,
events=merged_events,
description=current_phase.description
)
else:
merged.append(current_phase)
current_phase = next_phase
merged.append(current_phase)
return merged
def _calculate_metrics(self, events: List[Event], phases: List[Phase]) -> Dict[str, Any]:
"""Calculate timeline metrics and KPIs."""
if not events or not phases:
return {}
start_time = events[0].timestamp
end_time = events[-1].timestamp
total_duration = (end_time - start_time).total_seconds() / 60
# Phase timing metrics
phase_durations = {phase.name: phase.duration for phase in phases}
# Detection metrics
detection_time = 0
if phases and phases[0].name == "detection":
detection_time = phases[0].duration
# Time to mitigation
mitigation_start = None
for phase in phases:
if phase.name == "mitigation":
mitigation_start = (phase.start_time - start_time).total_seconds() / 60
break
# Time to resolution
resolution_time = None
for phase in phases:
if phase.name == "resolution":
resolution_time = (phase.start_time - start_time).total_seconds() / 60
break
# Communication frequency
comm_events = [e for e in events if e.type == "communication"]
comm_frequency = len(comm_events) / (total_duration / 60) if total_duration > 0 else 0
# Action frequency
action_events = [e for e in events if e.type == "action"]
action_frequency = len(action_events) / (total_duration / 60) if total_duration > 0 else 0
# Event source distribution
source_counts = defaultdict(int)
for event in events:
source_counts[event.source] += 1
return {
"duration_metrics": {
"total_duration_minutes": round(total_duration, 1),
"detection_duration_minutes": round(detection_time, 1),
"time_to_mitigation_minutes": round(mitigation_start or 0, 1),
"time_to_resolution_minutes": round(resolution_time or 0, 1),
"phase_durations": {k: round(v, 1) for k, v in phase_durations.items()}
},
"activity_metrics": {
"total_events": len(events),
"events_per_hour": round((len(events) / (total_duration / 60)) if total_duration > 0 else 0, 1),
"communication_frequency": round(comm_frequency, 1),
"action_frequency": round(action_frequency, 1),
"unique_sources": len(source_counts),
"unique_actors": len(set(e.actor for e in events))
},
"phase_metrics": {
"total_phases": len(phases),
"phase_sequence": [p.name for p in phases],
"longest_phase": max(phases, key=lambda p: p.duration).name if phases else None,
"shortest_phase": min(phases, key=lambda p: p.duration).name if phases else None
},
"source_distribution": dict(source_counts)
}
def _analyze_gaps(self, events: List[Event], phases: List[Phase]) -> Dict[str, Any]:
"""Perform gap analysis to identify potential issues."""
gaps = []
warnings = []
# Check phase transition timing
for i in range(len(phases) - 1):
current_phase = phases[i]
next_phase = phases[i + 1]
transition_gap = (next_phase.start_time - current_phase.end_time).total_seconds() / 60
threshold_key = f"{current_phase.name}_to_{next_phase.name}"
threshold = self.gap_thresholds.get(threshold_key, self.gap_thresholds["phase_transition"])
if transition_gap > threshold:
gaps.append({
"type": "phase_transition",
"from_phase": current_phase.name,
"to_phase": next_phase.name,
"gap_minutes": round(transition_gap, 1),
"threshold_minutes": threshold,
"severity": "warning" if transition_gap < threshold * 2 else "critical"
})
# Check communication gaps
comm_events = [e for e in events if e.type == "communication"]
for i in range(len(comm_events) - 1):
gap_minutes = (comm_events[i+1].timestamp - comm_events[i].timestamp).total_seconds() / 60
if gap_minutes > self.gap_thresholds["communication_gap"]:
gaps.append({
"type": "communication_gap",
"gap_minutes": round(gap_minutes, 1),
"threshold_minutes": self.gap_thresholds["communication_gap"],
"severity": "warning" if gap_minutes < self.gap_thresholds["communication_gap"] * 2 else "critical"
})
# Check for missing phases
expected_phases = ["detection", "triage", "mitigation", "resolution"]
actual_phases = [p.name for p in phases]
missing_phases = [p for p in expected_phases if p not in actual_phases]
for missing_phase in missing_phases:
warnings.append({
"type": "missing_phase",
"phase": missing_phase,
"message": f"Expected phase '{missing_phase}' not detected in timeline"
})
# Check for unusually long phases
for phase in phases:
if phase.duration > 180: # 3 hours
warnings.append({
"type": "long_phase",
"phase": phase.name,
"duration_minutes": round(phase.duration, 1),
"message": f"Phase '{phase.name}' lasted {phase.duration:.0f} minutes, which is unusually long"
})
return {
"gaps": gaps,
"warnings": warnings,
"gap_summary": {
"total_gaps": len(gaps),
"critical_gaps": len([g for g in gaps if g.get("severity") == "critical"]),
"warning_gaps": len([g for g in gaps if g.get("severity") == "warning"]),
"missing_phases": len(missing_phases)
}
}
def _generate_narrative(self, events: List[Event], phases: List[Phase]) -> Dict[str, Any]:
"""Generate human-readable incident narrative."""
if not events or not phases:
return {"error": "Insufficient data for narrative generation"}
# Create phase-based narrative
phase_narratives = []
for phase in phases:
key_events = self._extract_key_events(phase.events)
narrative_text = self._create_phase_narrative(phase, key_events)
phase_narratives.append({
"phase": phase.name,
"start_time": phase.start_time.isoformat(),
"duration_minutes": round(phase.duration, 1),
"narrative": narrative_text,
"key_events": len(key_events),
"total_events": len(phase.events)
})
# Create overall summary
start_time = events[0].timestamp
end_time = events[-1].timestamp
total_duration = (end_time - start_time).total_seconds() / 60
summary = f"""Incident Timeline Summary:
The incident began at {start_time.strftime('%Y-%m-%d %H:%M:%S UTC')} and concluded at {end_time.strftime('%Y-%m-%d %H:%M:%S UTC')}, lasting approximately {total_duration:.0f} minutes.
The incident progressed through {len(phases)} distinct phases: {', '.join(p.name for p in phases)}.
Key milestones:"""
for phase in phases:
summary += f"\n- {phase.name.title()}: {phase.start_time.strftime('%H:%M')} ({phase.duration:.0f} min)"
return {
"summary": summary,
"phase_narratives": phase_narratives,
"timeline_type": self._classify_timeline_pattern(phases),
"complexity_score": self._calculate_complexity_score(events, phases)
}
def _extract_key_events(self, events: List[Event]) -> List[Event]:
"""Extract the most important events from a phase."""
# Sort by severity and timestamp
sorted_events = sorted(events, key=lambda e: (e.severity, e.timestamp), reverse=True)
# Take top events, but ensure chronological representation
key_events = []
# Always include first and last events
if events:
key_events.append(events[0])
if len(events) > 1:
key_events.append(events[-1])
# Add high-severity events
high_severity_events = [e for e in events if e.severity >= 4]
key_events.extend(high_severity_events[:3])
# Remove duplicates while preserving order
seen = set()
unique_events = []
for event in key_events:
event_key = (event.timestamp, event.message)
if event_key not in seen:
seen.add(event_key)
unique_events.append(event)
return sorted(unique_events, key=lambda e: e.timestamp)
def _create_phase_narrative(self, phase: Phase, key_events: List[Event]) -> str:
"""Create narrative text for a phase."""
phase_templates = {
"detection": "The incident was first detected when {first_event}. {additional_details}",
"triage": "Initial investigation began with {first_event}. The team {investigation_actions}",
"escalation": "The incident was escalated when {escalation_trigger}. {escalation_actions}",
"mitigation": "Mitigation efforts started with {first_action}. {mitigation_steps}",
"resolution": "The incident was resolved when {resolution_event}. {confirmation_steps}",
"review": "Post-incident review activities included {review_activities}"
}
template = phase_templates.get(phase.name, "During the {phase_name} phase, {activities}")
if not key_events:
return f"The {phase.name} phase lasted {phase.duration:.0f} minutes with {len(phase.events)} events."
first_event = key_events[0].message
# Customize based on phase
if phase.name == "detection":
return template.format(
first_event=first_event,
additional_details=f"This phase lasted {phase.duration:.0f} minutes with {len(phase.events)} total events."
)
elif phase.name == "triage":
actions = [e.message for e in key_events if "investigating" in e.message.lower() or "checking" in e.message.lower()]
investigation_text = "performed various diagnostic activities" if not actions else f"focused on {actions[0]}"
return template.format(
first_event=first_event,
investigation_actions=investigation_text
)
else:
return f"During the {phase.name} phase ({phase.duration:.0f} minutes), key activities included: {first_event}"
def _classify_timeline_pattern(self, phases: List[Phase]) -> str:
"""Classify the overall timeline pattern."""
phase_names = [p.name for p in phases]
if "escalation" in phase_names and phases[0].name == "detection":
return "standard_escalation"
elif len(phases) <= 3:
return "simple_resolution"
elif "review" in phase_names:
return "comprehensive_response"
else:
return "complex_incident"
def _calculate_complexity_score(self, events: List[Event], phases: List[Phase]) -> float:
"""Calculate incident complexity score (0-10)."""
score = 0.0
# Phase count contributes to complexity
score += min(len(phases) * 1.5, 6.0)
# Event count contributes to complexity
score += min(len(events) / 20, 2.0)
# Duration contributes to complexity
if events:
duration_hours = (events[-1].timestamp - events[0].timestamp).total_seconds() / 3600
score += min(duration_hours / 2, 2.0)
return min(score, 10.0)
def _generate_summary(self, events: List[Event], phases: List[Phase], metrics: Dict) -> Dict[str, Any]:
"""Generate comprehensive incident summary."""
if not events:
return {}
# Key statistics
start_time = events[0].timestamp
end_time = events[-1].timestamp
duration_minutes = metrics.get("duration_metrics", {}).get("total_duration_minutes", 0)
# Phase analysis
phase_analysis = {}
for phase in phases:
phase_analysis[phase.name] = {
"duration_minutes": round(phase.duration, 1),
"event_count": len(phase.events),
"start_time": phase.start_time.isoformat(),
"end_time": phase.end_time.isoformat()
}
# Actor involvement
actors = defaultdict(int)
for event in events:
actors[event.actor] += 1
return {
"incident_overview": {
"start_time": start_time.isoformat(),
"end_time": end_time.isoformat(),
"total_duration_minutes": round(duration_minutes, 1),
"total_events": len(events),
"phases_detected": len(phases)
},
"phase_analysis": phase_analysis,
"key_participants": dict(actors),
"event_sources": dict(defaultdict(int, {e.source: 1 for e in events})),
"complexity_indicators": {
"unique_sources": len(set(e.source for e in events)),
"unique_actors": len(set(e.actor for e in events)),
"high_severity_events": len([e for e in events if e.severity >= 4]),
"phase_transitions": len(phases) - 1 if phases else 0
}
}
def _event_to_dict(self, event: Event) -> Dict:
"""Convert Event namedtuple to dictionary."""
return {
"timestamp": event.timestamp.isoformat(),
"source": event.source,
"type": event.type,
"message": event.message,
"severity": event.severity,
"actor": event.actor,
"metadata": event.metadata
}
def _phase_to_dict(self, phase: Phase) -> Dict:
"""Convert Phase namedtuple to dictionary."""
return {
"name": phase.name,
"start_time": phase.start_time.isoformat(),
"end_time": phase.end_time.isoformat(),
"duration_minutes": round(phase.duration, 1),
"event_count": len(phase.events),
"description": phase.description
}
def format_json_output(result: Dict) -> str:
"""Format result as pretty JSON."""
return json.dumps(result, indent=2, ensure_ascii=False)
def format_text_output(result: Dict) -> str:
"""Format result as human-readable text."""
if "error" in result:
return f"Error: {result['error']}"
timeline = result["timeline"]
metrics = result["metrics"]
narrative = result["narrative"]
output = []
output.append("=" * 80)
output.append("INCIDENT TIMELINE RECONSTRUCTION")
output.append("=" * 80)
output.append("")
# Overview
time_range = timeline["time_range"]
output.append("OVERVIEW:")
output.append(f" Time Range: {time_range['start']} to {time_range['end']}")
output.append(f" Total Duration: {time_range['duration_minutes']} minutes")
output.append(f" Total Events: {timeline['total_events']}")
output.append(f" Phases Detected: {len(timeline['phases'])}")
output.append("")
# Phase summary
output.append("PHASES:")
for phase in timeline["phases"]:
output.append(f" {phase['name'].upper()}:")
output.append(f" Start: {phase['start_time']}")
output.append(f" Duration: {phase['duration_minutes']} minutes")
output.append(f" Events: {phase['event_count']}")
output.append(f" Description: {phase['description']}")
output.append("")
# Key metrics
if "duration_metrics" in metrics:
duration_metrics = metrics["duration_metrics"]
output.append("KEY METRICS:")
output.append(f" Time to Mitigation: {duration_metrics.get('time_to_mitigation_minutes', 'N/A')} minutes")
output.append(f" Time to Resolution: {duration_metrics.get('time_to_resolution_minutes', 'N/A')} minutes")
if "activity_metrics" in metrics:
activity = metrics["activity_metrics"]
output.append(f" Events per Hour: {activity.get('events_per_hour', 'N/A')}")
output.append(f" Unique Sources: {activity.get('unique_sources', 'N/A')}")
output.append("")
# Narrative
if "summary" in narrative:
output.append("INCIDENT NARRATIVE:")
output.append(narrative["summary"])
output.append("")
# Gap analysis
if "gap_analysis" in result and result["gap_analysis"]["gaps"]:
output.append("GAP ANALYSIS:")
for gap in result["gap_analysis"]["gaps"][:5]: # Show first 5 gaps
output.append(f" {gap['type'].replace('_', ' ').title()}: {gap['gap_minutes']} min gap (threshold: {gap['threshold_minutes']} min)")
output.append("")
output.append("=" * 80)
return "\n".join(output)
def format_markdown_output(result: Dict) -> str:
"""Format result as Markdown timeline."""
if "error" in result:
return f"# Error\n\n{result['error']}"
timeline = result["timeline"]
narrative = result.get("narrative", {})
output = []
output.append("# Incident Timeline")
output.append("")
# Overview
time_range = timeline["time_range"]
output.append("## Overview")
output.append("")
output.append(f"- **Duration:** {time_range['duration_minutes']} minutes")
output.append(f"- **Start Time:** {time_range['start']}")
output.append(f"- **End Time:** {time_range['end']}")
output.append(f"- **Total Events:** {timeline['total_events']}")
output.append("")
# Narrative summary
if "summary" in narrative:
output.append("## Summary")
output.append("")
output.append(narrative["summary"])
output.append("")
# Phase timeline
output.append("## Phase Timeline")
output.append("")
for phase in timeline["phases"]:
output.append(f"### {phase['name'].title()} Phase")
output.append("")
output.append(f"**Duration:** {phase['duration_minutes']} minutes ")
output.append(f"**Start:** {phase['start_time']} ")
output.append(f"**Events:** {phase['event_count']} ")
output.append("")
output.append(phase["description"])
output.append("")
# Detailed timeline
output.append("## Detailed Event Timeline")
output.append("")
for event in timeline["events"]:
timestamp = datetime.fromisoformat(event["timestamp"].replace('Z', '+00:00'))
output.append(f"**{timestamp.strftime('%H:%M:%S')}** [{event['source']}] {event['message']}")
output.append("")
return "\n".join(output)
def main():
"""Main function with argument parsing and execution."""
parser = argparse.ArgumentParser(
description="Reconstruct incident timeline from timestamped events",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python timeline_reconstructor.py --input events.json --output timeline.md
python timeline_reconstructor.py --input events.json --detect-phases --gap-analysis
cat events.json | python timeline_reconstructor.py --format text
Input JSON format:
[
{
"timestamp": "2024-01-01T12:00:00Z",
"source": "monitoring",
"type": "alert",
"message": "High error rate detected",
"severity": "critical",
"actor": "system"
}
]
"""
)
parser.add_argument(
"--input", "-i",
help="Input file path (JSON format) or '-' for stdin"
)
parser.add_argument(
"--output", "-o",
help="Output file path (default: stdout)"
)
parser.add_argument(
"--format", "-f",
choices=["json", "text", "markdown"],
default="json",
help="Output format (default: json)"
)
parser.add_argument(
"--detect-phases",
action="store_true",
help="Enable advanced phase detection"
)
parser.add_argument(
"--gap-analysis",
action="store_true",
help="Perform gap analysis on timeline"
)
parser.add_argument(
"--min-events",
type=int,
default=1,
help="Minimum number of events required (default: 1)"
)
args = parser.parse_args()
reconstructor = TimelineReconstructor()
try:
# Read input
if args.input == "-" or (not args.input and not sys.stdin.isatty()):
# Read from stdin
input_text = sys.stdin.read().strip()
if not input_text:
parser.error("No input provided")
events_data = json.loads(input_text)
elif args.input:
# Read from file
with open(args.input, 'r') as f:
events_data = json.load(f)
else:
parser.error("No input specified. Use --input or pipe data to stdin.")
# Validate input
if not isinstance(events_data, list):
parser.error("Input must be a JSON array of events")
if len(events_data) < args.min_events:
parser.error(f"Minimum {args.min_events} events required")
# Reconstruct timeline
result = reconstructor.reconstruct_timeline(events_data)
# Format output
if args.format == "json":
output = format_json_output(result)
elif args.format == "markdown":
output = format_markdown_output(result)
else:
output = format_text_output(result)
# Write output
if args.output:
with open(args.output, 'w') as f:
f.write(output)
f.write('\n')
else:
print(output)
except FileNotFoundError as e:
print(f"Error: File not found - {e}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON - {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()Phân loại, phân tầng, xác định đường leo thang và thu thập chứng cứ pháp y cho sự cố bảo mật theo SEV1-SEV4 và NIST SP 800-61.
---
name: "incident-response"
description: "Use when a security incident has been detected or declared and needs classification, triage, escalation path determination, and forensic evidence collection. Covers SEV1-SEV4 classification, false positive filtering, incident taxonomy, and NIST SP 800-61 lifecycle."
---
# Incident Response
Incident response skill for the full lifecycle from initial triage through forensic collection, severity declaration, and escalation routing. This is NOT threat hunting (see threat-detection) or post-incident compliance mapping (see governance/compliance-mapping) — this is about classifying, triaging, and managing declared security incidents.
---
## Table of Contents
- [Overview](#overview)
- [Incident Triage Tool](#incident-triage-tool)
- [Incident Classification](#incident-classification)
- [Severity Framework](#severity-framework)
- [False Positive Filtering](#false-positive-filtering)
- [Forensic Evidence Collection](#forensic-evidence-collection)
- [Escalation Paths](#escalation-paths)
- [Regulatory Notification Obligations](#regulatory-notification-obligations)
- [Workflows](#workflows)
- [Anti-Patterns](#anti-patterns)
- [Cross-References](#cross-references)
---
## Overview
### What This Skill Does
This skill provides the methodology and tooling for **incident triage and response** — classifying security events into typed incidents, scoring severity, filtering false positives, determining escalation paths, and initiating forensic evidence collection under chain-of-custody controls.
### Distinction from Other Security Skills
| Skill | Focus | Approach |
|-------|-------|----------|
| **incident-response** (this) | Active incidents | Reactive — classify, escalate, collect evidence |
| threat-detection | Pre-incident hunting | Proactive — find threats before alerts fire |
| cloud-security | Cloud posture assessment | Preventive — IAM, S3, network misconfiguration |
| red-team | Offensive simulation | Offensive — test detection and response capability |
### Prerequisites
A security event must be ingested before triage. Events can come from SIEM alerts, EDR detections, threat intel feeds, or user reports. The triage tool accepts JSON event payloads; see the input schema below.
---
## Incident Triage Tool
The `incident_triage.py` tool classifies events, checks false positives, scores severity, determines escalation, and performs forensic pre-analysis.
```bash
# Classify an event from JSON file
python3 scripts/incident_triage.py --input event.json --classify --json
# Classify with false positive filtering enabled
python3 scripts/incident_triage.py --input event.json --classify --false-positive-check --json
# Force a severity level for tabletop exercises
python3 scripts/incident_triage.py --input event.json --severity sev1 --json
# Read event from stdin
echo '{"event_type": "ransomware", "host": "prod-db-01", "raw_payload": {}}' | \
python3 scripts/incident_triage.py --classify --false-positive-check --json
```
### Input Event Schema
```json
{
"event_type": "ransomware",
"host": "prod-db-01",
"user": "svc_backup",
"source_ip": "10.1.2.3",
"timestamp": "2024-01-15T14:32:00Z",
"raw_payload": {}
}
```
### Exit Codes
| Code | Meaning | Required Response |
|------|---------|-------------------|
| 0 | SEV3/SEV4 or clean | Standard ticket-based handling |
| 1 | SEV2 — elevated | 1-hour bridge call, async coordination |
| 2 | SEV1 — critical | Immediate 15-minute war room, all-hands |
---
## Incident Classification
Security events are classified into 14 incident types. Classification drives default severity, MITRE technique mapping, and response SLA.
### Incident Taxonomy
| Incident Type | Default Severity | MITRE Technique | Response SLA |
|--------------|-----------------|-----------------|--------------|
| ransomware | SEV1 | T1486 | 15 minutes |
| data_exfiltration | SEV1 | T1048 | 15 minutes |
| apt_intrusion | SEV1 | T1566 | 15 minutes |
| supply_chain_compromise | SEV1 | T1195 | 15 minutes |
| domain_controller_breach | SEV1 | T1078.002 | 15 minutes |
| credential_compromise | SEV2 | T1110 | 1 hour |
| lateral_movement | SEV2 | T1021 | 1 hour |
| malware_infection | SEV2 | T1204 | 1 hour |
| insider_threat | SEV2 | T1078 | 1 hour |
| cloud_account_compromise | SEV2 | T1078.004 | 1 hour |
| unauthorized_access | SEV3 | T1190 | 4 hours |
| policy_violation | SEV3 | N/A | 4 hours |
| phishing_attempt | SEV4 | T1566.001 | 24 hours |
| security_alert | SEV4 | N/A | 24 hours |
### SEV Escalation Triggers
Any of the following automatically re-declare a higher severity:
| Trigger | New Severity |
|---------|-------------|
| Ransomware note found | SEV1 |
| Active exfiltration confirmed | SEV1 |
| CloudTrail or SIEM disabled | SEV1 |
| Domain controller access confirmed | SEV1 |
| Second system compromised | SEV1 |
| Exfiltration volume exceeds 1 GB | SEV2 minimum |
| C-suite account accessed | SEV2 minimum |
---
## Severity Framework
### SEV Level Matrix
| Level | Name | Criteria | Skills Invoked | Escalation Path |
|-------|------|----------|---------------|-----------------|
| SEV1 | Critical | Confirmed ransomware; active PII/PHI exfiltration (>10K records); domain controller breach; defense evasion (CloudTrail disabled); supply chain compromise | All skills (parallel) | SOC Lead → CISO → CEO → Board Chair |
| SEV2 | High | Confirmed unauthorized access to sensitive systems; credential compromise with elevated privileges; lateral movement confirmed; ransomware indicators without confirmed execution | triage + containment + forensics | SOC Lead → CISO |
| SEV3 | Medium | Suspected unauthorized access (unconfirmed); malware detected and contained; single account compromise (no priv escalation) | triage + containment | SOC Lead → Security Manager |
| SEV4 | Low | Security alert with no confirmed impact; informational indicator; policy violation with no data risk | triage only | L3 Analyst queue |
---
## False Positive Filtering
The triage tool applies five filters before escalating to prevent false positive inflation.
### False Positive Filter Types
| Filter | Description | Example Pattern |
|--------|-------------|----------------|
| CI/CD agent activity | Known build/deploy agents flagged as anomalies | jenkins, github-actions, circleci, gitlab-runner |
| Test environment tagging | Assets tagged as non-production | test-, staging-, dev-, sandbox- |
| Scheduled job patterns | Expected batch processes triggering alerts | cron, scheduled_task, batch_job, backup_ |
| Whitelisted identities | Explicitly approved service accounts | svc_monitoring, svc_backup, datadog-agent |
| Scanner activity | Known security scanners and vulnerability tools | nessus, qualys, rapid7, aws_inspector |
A confirmed false positive suppresses escalation and logs the suppression reason for audit purposes. Recurring false positives from the same source should be tuned out at the detection layer, not filtered repeatedly at triage.
---
## Forensic Evidence Collection
Evidence collection follows the DFRWS six-phase framework and the principle of volatile-first acquisition.
### DFRWS Six Phases
| Phase | Activity | Priority |
|-------|----------|----------|
| Identification | Identify what evidence exists and where | Immediate |
| Preservation | Prevent modification — write-block, snapshot, legal hold | Immediate |
| Collection | Acquire evidence in order of volatility | Immediate |
| Examination | Technical analysis of collected evidence | Within 2 hours |
| Analysis | Interpret findings in investigative context | Within 4 hours |
| Presentation | Produce findings report with chain of custody | Before incident closure |
### Volatile Evidence — Collect First
1. Live memory (RAM dump) — lost on reboot
2. Running processes and open network connections (`netstat`, `ps`)
3. Logged-in users and active sessions
4. System uptime and current time (for timeline anchoring)
5. Environment variables and loaded kernel modules
### Chain of Custody Requirements
Every evidence item must be recorded with:
- SHA-256 hash at acquisition time
- Acquisition timestamp in UTC with timezone offset
- Tool provenance (FTK Imager, Volatility, dd, AWS CloudTrail export)
- Investigator identity
- Transfer log (who had custody and when)
---
## Escalation Paths
### By Severity
| Severity | Immediate Contact | Bridge Call | External Notification |
|----------|------------------|-------------|----------------------|
| SEV1 | SOC Lead + CISO (15 min) | Immediate war room | Legal + PR standby; regulatory notification per deadline table |
| SEV2 | SOC Lead (30 min async) | 1-hour bridge | Legal notification if PII involved |
| SEV3 | Security Manager (4 hours) | Async only | None unless scope expands |
| SEV4 | L3 Analyst queue (24 hours) | None | None |
### By Incident Type
| Incident Type | Primary Escalation | Secondary |
|--------------|-------------------|-----------|
| Ransomware / APT | CISO + CEO | Board if data at risk |
| PII/PHI breach | Legal + CISO | Regulatory body (per deadline table) |
| Cloud account compromise | Cloud security team | CISO |
| Insider threat | HR + Legal + CISO | Law enforcement if criminal |
| Supply chain | CISO + Vendor management | Board |
---
## Regulatory Notification Obligations
The notification clock starts at incident declaration, not at investigation completion.
| Framework | Incident Type | Deadline | Penalty |
|-----------|--------------|----------|---------|
| GDPR (EU 2016/679) | Personal data breach | 72 hours after discovery | Up to 4% global revenue |
| PCI-DSS v4.0 | Cardholder data breach | 24 hours to acquirer | Card brand fines |
| HIPAA (45 CFR 164) | PHI breach (>500 individuals) | 60 days after discovery | Up to $1.9M per violation category |
| NY DFS 23 NYCRR 500 | Cybersecurity event | 72 hours to DFS | Regulatory sanctions |
| SEC Rule (17 CFR 229.106) | Material cybersecurity incident | 4 business days after materiality determination | SEC enforcement |
| CCPA / CPRA | Breach of sensitive PI | Without unreasonable delay | AG enforcement; private right of action |
| NIS2 (EU 2022/2555) | Significant incident (essential services) | 24-hour early warning; 72-hour notification | National authority sanctions |
**Operational rule:** If scope is unclear at declaration, assume the most restrictive applicable deadline and confirm scope within the first response window.
Full deadline reference: `references/regulatory-deadlines.md`
---
## Workflows
### Workflow 1: Quick Triage (15 Minutes)
For single alert requiring classification before escalation decision:
```bash
# 1. Classify the event with false positive filtering
python3 scripts/incident_triage.py --input alert.json \
--classify --false-positive-check --json
# 2. Review severity, escalation_path, and false_positive_flag in output
# 3. If severity = sev1 or sev2, page SOC Lead immediately
# 4. If false_positive_flag = true, document and close
```
**Decision**: Exit code 2 = SEV1 war room now. Exit code 1 = SEV2 bridge call within 30 minutes.
### Workflow 2: Full Incident Response (SEV1)
```
T+0 Detection arrives (SIEM alert, EDR, user report)
T+5 Classify with incident_triage.py --classify --false-positive-check
T+10 If SEV1: page CISO, open war room, start regulatory clock
T+15 Initiate forensic collection (volatile evidence first)
T+15 Containment assessment (parallel with forensics)
T+30 Human approval gate for any containment action
T+45 Execute approved containment
T+60 Assess containment effectiveness, brief Legal if PII/PHI scope
T+4h Final forensic evidence package, dwell time estimate
T+8h Eradication and recovery plan
T+72h Regulatory notification submission (if GDPR/NIS2 triggered)
```
```bash
# Full classification with forensic context
python3 scripts/incident_triage.py --input incident.json \
--classify --false-positive-check --severity sev1 --json > incident_triage_output.json
# Forensic pre-analysis
python3 scripts/incident_triage.py --input incident.json --json | \
jq '.forensic_findings, .chain_of_custody_steps'
```
### Workflow 3: Tabletop Exercise Simulation
Simulate incidents at specific severity levels without real events:
```bash
# Simulate SEV1 ransomware incident
echo '{"event_type": "ransomware", "host": "prod-db-01", "user": "svc_backup"}' | \
python3 scripts/incident_triage.py --classify --severity sev1 --json
# Simulate SEV2 credential compromise
echo '{"event_type": "credential_compromise", "user": "admin_user", "source_ip": "203.0.113.5"}' | \
python3 scripts/incident_triage.py --classify --false-positive-check --json
# Verify escalation paths for all 14 incident types
for type in ransomware data_exfiltration credential_compromise lateral_movement; do
echo "{\"event_type\": \"$type\"}" | python3 scripts/incident_triage.py --classify --json
done
```
---
## Anti-Patterns
1. **Starting the notification clock at investigation completion** — Regulatory clocks (GDPR 72 hours, PCI 24 hours) start at discovery, not investigation completion. Declaring late exposes the organization to maximum penalties even if the incident itself was minor.
2. **Containing before collecting volatile evidence** — Rebooting or isolating a system destroys RAM, running processes, and active connections. Forensic collection of volatile evidence must happen in parallel with containment, never after.
3. **Skipping false positive verification before escalation** — Escalating every alert to SEV1 degrades SOC credibility and causes alert fatigue. Always run false positive filters before paging the CISO.
4. **Undocumented incident command decisions** — Every decision made during a SEV1, including decisions made under uncertainty, must be logged in the evidence chain with timestamp and rationale. Undocumented decisions cannot be defended in regulatory investigations.
5. **Treating incident closure as investigation completion** — Incidents are closed when eradication and recovery are complete, not when the investigation is done. The forensic report and regulatory submissions may continue after operational closure.
6. **Single-source classification** — Classifying an incident from a single data source (one SIEM alert) without corroborating evidence frequently leads to misclassification. Collect at least two independent signals before declaring SEV1.
7. **Bypassing human approval gates for containment** — Automated containment actions (network isolation, credential revocation) taken without human approval can cause production outages, destroy evidence, and create liability. Human approval is non-negotiable for all mutating containment actions.
---
## Cross-References
| Skill | Relationship |
|-------|-------------|
| [threat-detection](../threat-detection/SKILL.md) | Confirmed hunting findings escalate to incident-response for triage and classification |
| [cloud-security](../cloud-security/SKILL.md) | Cloud posture findings (IAM compromise, S3 exposure) may trigger incident classification |
| [red-team](../red-team/SKILL.md) | Red team findings validate detection coverage; confirmed gaps become hunting hypotheses |
| [security-pen-testing](../security-pen-testing/SKILL.md) | Pen test vulnerabilities exploited in the wild escalate to incident-response for active incident handling |
FILE:references/regulatory-deadlines.md
# Regulatory Notification Deadlines
Reference table for incident notification deadlines under major regulatory frameworks. The notification clock starts at the moment an incident is declared, not at investigation completion.
**Operational rule:** If the scope of a breach is unclear at declaration time, assume the most restrictive applicable deadline and confirm scope within the first response window. Document the assumption and its resolution in the incident record.
---
## Deadline Summary Table
| Framework | Jurisdiction | Incident Type | Notification Deadline | Recipient | Penalty for Non-Compliance |
|-----------|-------------|--------------|----------------------|-----------|---------------------------|
| GDPR (EU 2016/679) | EU/EEA | Personal data breach | 72 hours after discovery | Supervisory Authority (DPA) | Up to 4% of global annual turnover or €20M |
| GDPR (EU 2016/679) | EU/EEA | Personal data breach affecting individual rights/freedoms | Without undue delay | Affected data subjects | Up to 4% of global annual turnover |
| PCI-DSS v4.0 | Global (card brands) | Cardholder data breach | 24 hours after confirmation | Acquiring bank and card brands | Fines per card brand schedule; potential card processing suspension |
| HIPAA (45 CFR §164.408) | United States | PHI breach (>500 individuals) | 60 calendar days after discovery | HHS Office for Civil Rights | $100–$50,000 per violation; up to $1.9M per violation category per year |
| HIPAA (45 CFR §164.406) | United States | PHI breach (>500 individuals in a state) | 60 days after discovery | Prominent media outlets in affected state | Same as above |
| HIPAA Small Breach | United States | PHI breach (<500 individuals) | Within 60 days of end of calendar year in which breach occurred | HHS (annual report) | Same as above |
| NY DFS 23 NYCRR 500.17 | New York State | Cybersecurity event affecting NY-regulated entity | 72 hours | NY DFS Superintendent | Regulatory sanctions, fines, license revocation |
| SEC Cybersecurity Rule (17 CFR §229.106) | United States (public companies) | Material cybersecurity incident | 4 business days after materiality determination | SEC Form 8-K filing (public disclosure) | SEC enforcement action; restatement risk |
| CCPA / CPRA | California, United States | Breach of sensitive personal information | Without unreasonable delay | CA Attorney General (if >500 CA residents affected) | Civil penalties up to $7,500 per intentional violation |
| NIS2 (EU 2022/2555) | EU/EEA (essential/important entities) | Significant incident | 24-hour early warning; 72-hour full notification | National CSIRT or competent authority | Up to €10M or 2% of global turnover |
| DORA (EU 2022/2554) | EU/EEA (financial sector) | Major ICT-related incident | Initial notification: 4 hours; intermediate: 72 hours; final: 1 month | Financial supervisory authority | National authority sanctions |
| SOX (for material incidents) | United States (public companies) | Financial system compromise creating material weakness | Immediate disclosure required | SEC, audit committee, auditors | Enforcement action; officer certification liability |
| Australia Privacy Act | Australia | Eligible data breach (serious harm likely) | 30 days after awareness | OAIC (Office of the Australian Information Commissioner) | Up to AUD 50M per serious contravention |
| PIPL (China) | China | Personal information breach | Immediately; notify individuals without delay | National Internet Information Office (CAC) | Up to ¥50M or 5% of prior year revenue |
---
## GDPR — Detailed Requirements
### Article 33 — Notification to Supervisory Authority
**When:** Any personal data breach where there is a risk to the rights and freedoms of individuals.
**Exception:** No notification required if the breach is unlikely to result in risk (e.g., the data was encrypted with a key that was not compromised, and the key cannot be recovered).
**What to include:**
1. Nature of the breach, including categories and approximate number of data subjects and records
2. Name and contact details of the Data Protection Officer
3. Likely consequences of the breach
4. Measures taken or proposed to address the breach, including mitigation
**Staggered notification:** If full information is not available within 72 hours, submit what is known and provide additional information in phases. Document why the information is being provided in phases.
### Article 34 — Notification to Data Subjects
**When:** When a breach is likely to result in high risk to the rights and freedoms of individuals.
**How:** In clear, plain language. Direct communication to the affected individuals.
**Exception:** Notification to individuals not required if:
- The personal data was protected by appropriate technical measures (e.g., encryption)
- The controller has taken subsequent measures that ensure high risk no longer materializes
- It would involve disproportionate effort (use public communication instead)
---
## PCI-DSS v4.0 — Detailed Requirements
### Requirement 12.10.5
Report compromises of cardholder data to the applicable payment brands and acquiring bank immediately upon detection of a suspected compromise. Do not wait for internal investigation to complete.
**Immediate actions required upon suspicion:**
1. Contact acquiring bank within 24 hours of suspicion (even if not yet confirmed)
2. Preserve all logs and evidence — do not modify or delete
3. Implement containment without destroying forensic evidence
4. Engage a PCI Forensic Investigator (PFI) from the approved list
**Card brand notification channels:**
- Visa: Visa Fraud Control
- Mastercard: Mastercard Fraud Control
- American Express: AmEx Security
- Discover: Discover Security
---
## HIPAA — Detailed Requirements
### 45 CFR §164.408 — Breach Notification to HHS
**Notification form:** HHS breach notification portal (https://www.hhs.gov/hipaa/for-professionals/breach-notification/)
**Content required:**
- Name of covered entity or business associate
- Nature of PHI involved (type of PHI, not specific records)
- Unauthorized persons who accessed or used the PHI
- Whether PHI was actually acquired or viewed
- Extent to which risk has been mitigated
### Breach Risk Assessment (45 CFR §164.402)
HIPAA provides a risk assessment safe harbor. A breach is presumed unless the covered entity can demonstrate (low probability PHI was compromised) based on:
1. Nature and extent of PHI involved
2. Who accessed the information
3. Whether PHI was actually acquired or viewed
4. Extent to which risk has been mitigated
Document this risk assessment in writing and retain for 6 years.
---
## Notification Clock Management
### Starting the Clock
Document the exact timestamp when the incident was declared in the incident record. This is the official start of all regulatory clocks.
### Parallel Tracking
Incidents often cross multiple frameworks simultaneously. Track all applicable clocks in parallel:
```
Incident declared: 2024-01-15T14:30:00Z
GDPR notification due: 2024-01-18T14:30:00Z (72 hours)
PCI notification due: 2024-01-16T14:30:00Z (24 hours)
HIPAA HHS notification: 2024-03-15T14:30:00Z (60 days)
NY DFS notification: 2024-01-18T14:30:00Z (72 hours)
```
### Notification Drafting
Prepare draft notifications in parallel with investigation. Do not wait until investigation is complete to begin drafting. All external regulatory communications must be reviewed by Legal and approved by CISO before transmission.
FILE:scripts/incident_triage.py
#!/usr/bin/env python3
"""
incident_triage.py — Incident Classification, Triage, and Escalation
Classifies security events into 14 incident types, applies false-positive
filters, scores severity (SEV1-SEV4), determines escalation path, and
performs forensic pre-analysis for confirmed incidents.
Usage:
echo '{"event_type": "ransomware", "raw_payload": {...}}' | python3 incident_triage.py
python3 incident_triage.py --input event.json --json
python3 incident_triage.py --classify --false-positive-check --input event.json --json
Exit codes:
0 SEV3/SEV4 or clean — standard handling
1 SEV2 — elevated response required
2 SEV1 — critical incident declared
"""
import argparse
import json
import sys
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple
# ---------------------------------------------------------------------------
# Constants — Forensic Pre-Analysis Base (reused from pre_analysis.py logic)
# ---------------------------------------------------------------------------
DWELL_CRITICAL = 720 # hours (30 days)
DWELL_HIGH = 168 # hours (7 days)
DWELL_MEDIUM = 24 # hours (1 day)
EVIDENCE_SOURCES = [
"siem_logs",
"edr_telemetry",
"network_pcap",
"dns_logs",
"proxy_logs",
"cloud_trail",
"authentication_logs",
"endpoint_filesystem",
"memory_dump",
"email_headers",
]
CHAIN_OF_CUSTODY_STEPS = [
"Identify and preserve volatile evidence (RAM, network connections)",
"Hash all collected artifacts (SHA-256) before analysis",
"Document collection timestamp and analyst identity",
"Transfer artifacts to isolated forensic workstation",
"Maintain write-blockers for disk images",
"Log every access to evidence with timestamps",
"Store originals in secure, access-controlled evidence vault",
"Maintain dual-custody chain for legal proceedings",
]
# ---------------------------------------------------------------------------
# Constants — Incident Taxonomy and Escalation
# ---------------------------------------------------------------------------
INCIDENT_TAXONOMY: Dict[str, Dict[str, Any]] = {
"ransomware": {
"default_severity": "sev1",
"mitre": "T1486",
"response_sla_minutes": 15,
},
"data_exfiltration": {
"default_severity": "sev1",
"mitre": "T1048",
"response_sla_minutes": 15,
},
"apt_intrusion": {
"default_severity": "sev1",
"mitre": "T1190",
"response_sla_minutes": 15,
},
"supply_chain_compromise": {
"default_severity": "sev1",
"mitre": "T1195",
"response_sla_minutes": 15,
},
"credential_compromise": {
"default_severity": "sev2",
"mitre": "T1078",
"response_sla_minutes": 60,
},
"lateral_movement": {
"default_severity": "sev2",
"mitre": "T1021",
"response_sla_minutes": 60,
},
"privilege_escalation": {
"default_severity": "sev2",
"mitre": "T1068",
"response_sla_minutes": 60,
},
"malware_detected": {
"default_severity": "sev2",
"mitre": "T1204",
"response_sla_minutes": 60,
},
"phishing": {
"default_severity": "sev3",
"mitre": "T1566",
"response_sla_minutes": 240,
},
"unauthorized_access": {
"default_severity": "sev3",
"mitre": "T1078",
"response_sla_minutes": 240,
},
"policy_violation": {
"default_severity": "sev4",
"mitre": "T1530",
"response_sla_minutes": 1440,
},
"vulnerability_discovered": {
"default_severity": "sev4",
"mitre": "T1190",
"response_sla_minutes": 1440,
},
"dos_attack": {
"default_severity": "sev3",
"mitre": "T1498",
"response_sla_minutes": 240,
},
"insider_threat": {
"default_severity": "sev2",
"mitre": "T1078.002",
"response_sla_minutes": 60,
},
}
FALSE_POSITIVE_INDICATORS = [
{
"name": "ci_cd_automation",
"description": "CI/CD pipeline service account activity",
"patterns": [
"jenkins", "github-actions", "gitlab-ci", "terraform",
"ansible", "circleci", "codepipeline",
],
},
{
"name": "test_environment",
"description": "Activity in test/dev/staging environment",
"patterns": [
"test", "dev", "staging", "sandbox", "qa", "nonprod", "non-prod",
],
},
{
"name": "scheduled_scanner",
"description": "Known security scanner or automated tool",
"patterns": [
"nessus", "qualys", "rapid7", "tenable", "crowdstrike",
"defender", "sentinel",
],
},
{
"name": "scheduled_batch_job",
"description": "Recurring batch process with expected behavior",
"patterns": [
"backup", "sync", "batch", "cron", "scheduled", "nightly", "weekly",
],
},
{
"name": "whitelisted_identity",
"description": "Identity in approved exception list",
"patterns": [
"svc-", "sa-", "system@", "automation@", "monitor@", "health-check",
],
},
]
ESCALATION_ROUTING: Dict[str, Dict[str, Any]] = {
"sev1": {
"escalate_to": "CISO + CEO + Board Chair (if data at risk)",
"bridge_call": True,
"war_room": True,
},
"sev2": {
"escalate_to": "SOC Lead + CISO",
"bridge_call": True,
"war_room": False,
},
"sev3": {
"escalate_to": "SOC Lead + Security Manager",
"bridge_call": False,
"war_room": False,
},
"sev4": {
"escalate_to": "L3 Analyst queue",
"bridge_call": False,
"war_room": False,
},
}
SEV_ESCALATION_TRIGGERS = [
{"indicator": "ransomware_note_found", "escalate_to": "sev1"},
{"indicator": "active_exfiltration_confirmed", "escalate_to": "sev1"},
{"indicator": "siem_disabled", "escalate_to": "sev1"},
{"indicator": "domain_controller_access", "escalate_to": "sev1"},
{"indicator": "second_system_compromised", "escalate_to": "sev1"},
]
# ---------------------------------------------------------------------------
# Forensic Pre-Analysis Functions (base pre_analysis.py logic)
# ---------------------------------------------------------------------------
def parse_forensic_fields(fact: dict) -> dict:
"""
Parse and normalise forensic-relevant fields from the raw event.
Returns a dict with keys: source_ip, destination_ip, user_account,
hostname, process_name, dwell_hours, iocs, raw_payload.
"""
raw = fact.get("raw_payload", {}) if isinstance(fact.get("raw_payload"), dict) else {}
def _pick(*keys: str, default: Any = None) -> Any:
"""Return first non-None value found across fact and raw_payload."""
for k in keys:
v = fact.get(k) or raw.get(k)
if v is not None:
return v
return default
source_ip = _pick("source_ip", "src_ip", "sourceIp", default="unknown")
destination_ip = _pick("destination_ip", "dst_ip", "dest_ip", "destinationIp", default="unknown")
user_account = _pick("user", "user_account", "username", "actor", "identity", default="unknown")
hostname = _pick("hostname", "host", "device", "computer_name", default="unknown")
process_name = _pick("process", "process_name", "executable", "image", default="unknown")
# Dwell time: accept hours directly or compute from timestamps
dwell_hours: float = 0.0
raw_dwell = _pick("dwell_hours", "dwell_time_hours", "dwell")
if raw_dwell is not None:
try:
dwell_hours = float(raw_dwell)
except (TypeError, ValueError):
dwell_hours = 0.0
else:
first_seen = _pick("first_seen", "first_observed", "initial_access_time")
last_seen = _pick("last_seen", "last_observed", "detection_time")
if first_seen and last_seen:
try:
fmt = "%Y-%m-%dT%H:%M:%SZ"
dt_first = datetime.strptime(str(first_seen), fmt)
dt_last = datetime.strptime(str(last_seen), fmt)
dwell_hours = max(0.0, (dt_last - dt_first).total_seconds() / 3600.0)
except (ValueError, TypeError):
dwell_hours = 0.0
iocs: List[str] = []
raw_iocs = _pick("iocs", "indicators", "indicators_of_compromise")
if isinstance(raw_iocs, list):
iocs = [str(i) for i in raw_iocs]
elif isinstance(raw_iocs, str):
iocs = [raw_iocs]
return {
"source_ip": source_ip,
"destination_ip": destination_ip,
"user_account": user_account,
"hostname": hostname,
"process_name": process_name,
"dwell_hours": dwell_hours,
"iocs": iocs,
"raw_payload": raw,
}
def assess_dwell_severity(dwell_hours: float) -> str:
"""
Map dwell time (hours) to a severity label.
Returns 'critical', 'high', 'medium', or 'low'.
"""
if dwell_hours >= DWELL_CRITICAL:
return "critical"
if dwell_hours >= DWELL_HIGH:
return "high"
if dwell_hours >= DWELL_MEDIUM:
return "medium"
return "low"
def build_ioc_summary(fields: dict) -> dict:
"""
Build a structured IOC summary from parsed forensic fields.
Returns a dict suitable for embedding in the triage output.
"""
iocs = fields.get("iocs", [])
dwell_hours = fields.get("dwell_hours", 0.0)
dwell_severity = assess_dwell_severity(dwell_hours)
# Classify IOCs by rough heuristic
ip_iocs = [i for i in iocs if _looks_like_ip(i)]
hash_iocs = [i for i in iocs if _looks_like_hash(i)]
domain_iocs = [i for i in iocs if not _looks_like_ip(i) and not _looks_like_hash(i)]
return {
"total_ioc_count": len(iocs),
"ip_indicators": ip_iocs,
"hash_indicators": hash_iocs,
"domain_url_indicators": domain_iocs,
"dwell_hours": round(dwell_hours, 2),
"dwell_severity": dwell_severity,
"evidence_sources_applicable": [
src for src in EVIDENCE_SOURCES
if _source_applicable(src, fields)
],
"chain_of_custody_steps": CHAIN_OF_CUSTODY_STEPS,
}
def _looks_like_ip(value: str) -> bool:
"""Heuristic: does the string look like an IPv4 address?"""
import re
return bool(re.match(r"^\d{1,3}(\.\d{1,3}){3}$", value.strip()))
def _looks_like_hash(value: str) -> bool:
"""Heuristic: does the string look like a hex hash (MD5/SHA1/SHA256)?"""
import re
return bool(re.match(r"^[0-9a-fA-F]{32,64}$", value.strip()))
def _source_applicable(source: str, fields: dict) -> bool:
"""Decide if an evidence source is relevant given parsed fields."""
mapping = {
"network_pcap": fields.get("source_ip") not in (None, "unknown"),
"edr_telemetry": fields.get("hostname") not in (None, "unknown"),
"authentication_logs": fields.get("user_account") not in (None, "unknown"),
"dns_logs": fields.get("destination_ip") not in (None, "unknown"),
"endpoint_filesystem": fields.get("process_name") not in (None, "unknown"),
"memory_dump": fields.get("process_name") not in (None, "unknown"),
}
return mapping.get(source, True)
# ---------------------------------------------------------------------------
# New Classification and Escalation Functions
# ---------------------------------------------------------------------------
def classify_incident(fact: dict) -> Tuple[str, float]:
"""
Classify incident type from event fields.
Performs keyword matching against INCIDENT_TAXONOMY keys and the
flattened string representation of raw_payload content.
Returns:
(incident_type, confidence) where confidence is 0.0–1.0.
Returns ("unknown", 0.0) when no match is found.
"""
# Build a single searchable string from the fact
searchable = _flatten_to_string(fact).lower()
scores: Dict[str, int] = {}
for incident_type in INCIDENT_TAXONOMY:
# The incident type slug itself is a keyword
slug_words = incident_type.replace("_", " ").split()
score = 0
for word in slug_words:
if word in searchable:
score += 2 # direct slug match carries more weight
# Additional keyword synonyms per type
synonyms = _get_synonyms(incident_type)
for syn in synonyms:
if syn in searchable:
score += 1
if score > 0:
scores[incident_type] = score
if not scores:
# Last resort: check explicit event_type field
event_type = str(fact.get("event_type", "")).lower().replace(" ", "_").replace("-", "_")
if event_type in INCIDENT_TAXONOMY:
return event_type, 0.6
return "unknown", 0.0
best_type = max(scores, key=lambda k: scores[k])
max_score = scores[best_type]
# Normalise confidence: cap at 1.0, scale by how much the best
# outscores alternatives
total_score = sum(scores.values()) or 1
raw_confidence = max_score / total_score
# Boost if event_type field matches
event_type = str(fact.get("event_type", "")).lower().replace(" ", "_").replace("-", "_")
if event_type == best_type:
raw_confidence = min(1.0, raw_confidence + 0.25)
confidence = round(min(1.0, raw_confidence + 0.1 * min(max_score, 5)), 2)
return best_type, confidence
def _flatten_to_string(obj: Any, depth: int = 0) -> str:
"""Recursively flatten any JSON-like object into a single string."""
if depth > 6:
return ""
if isinstance(obj, dict):
parts = []
for k, v in obj.items():
parts.append(str(k))
parts.append(_flatten_to_string(v, depth + 1))
return " ".join(parts)
if isinstance(obj, list):
return " ".join(_flatten_to_string(i, depth + 1) for i in obj)
return str(obj)
def _get_synonyms(incident_type: str) -> List[str]:
"""Return additional keyword synonyms for an incident type."""
synonyms_map: Dict[str, List[str]] = {
"ransomware": ["encrypt", "ransom", "locked", "decrypt", "wiper", "crypto"],
"data_exfiltration": ["exfil", "upload", "transfer", "leak", "dump", "steal", "exfiltrate"],
"apt_intrusion": ["apt", "nation-state", "targeted", "backdoor", "persistence", "c2", "c&c"],
"supply_chain_compromise": ["supply chain", "dependency", "package", "solarwinds", "xz", "npm"],
"credential_compromise": ["credential", "password", "brute force", "spray", "stuffing", "stolen"],
"lateral_movement": ["lateral", "pivot", "pass-the-hash", "wmi", "psexec", "rdp movement"],
"priv_escalation": ["privesc", "su_exec", "priv_change", "elevated_session", "priv_grant", "priv_abuse"],
"malware_detected": ["malware", "trojan", "virus", "worm", "keylogger", "spyware", "rat"],
"phishing": ["phish", "spear", "bec", "email", "lure", "credential harvest"],
"unauthorized_access": ["unauthorized", "unauthenticated", "brute", "login failed", "access denied"],
"policy_violation": ["policy", "dlp", "data loss", "violation", "compliance"],
"vulnerability_discovered": ["vulnerability", "cve", "exploit", "patch", "zero-day", "rce"],
"dos_attack": ["dos", "ddos", "flood", "amplification", "bandwidth", "exhaustion"],
"insider_threat": ["insider", "employee", "contractor", "abuse", "privilege misuse"],
}
return synonyms_map.get(incident_type, [])
def check_false_positives(fact: dict) -> List[str]:
"""
Check fact fields against FALSE_POSITIVE_INDICATORS pattern lists.
Returns a list of triggered false positive indicator names.
"""
searchable = _flatten_to_string(fact).lower()
triggered: List[str] = []
for indicator in FALSE_POSITIVE_INDICATORS:
for pattern in indicator["patterns"]:
if pattern.lower() in searchable:
triggered.append(indicator["name"])
break # one match per indicator is enough
return triggered
def get_escalation_path(incident_type: str, severity: str) -> dict:
"""
Return escalation routing for a given incident type and severity level.
Falls back to sev4 routing if severity is not recognised.
"""
sev_key = severity.lower()
routing = ESCALATION_ROUTING.get(sev_key, ESCALATION_ROUTING["sev4"]).copy()
# Augment with taxonomy SLA if available
taxonomy = INCIDENT_TAXONOMY.get(incident_type, {})
routing["incident_type"] = incident_type
routing["severity"] = sev_key
routing["response_sla_minutes"] = taxonomy.get("response_sla_minutes", 1440)
routing["mitre_technique"] = taxonomy.get("mitre", "N/A")
return routing
def check_sev_escalation_triggers(fact: dict) -> Optional[str]:
"""
Scan fact fields for any SEV escalation trigger indicators.
Returns the escalation target (e.g. 'sev1') if a trigger fires,
or None if no triggers are present.
"""
searchable = _flatten_to_string(fact).lower()
# Also inspect a flat list of explicit indicator flags
explicit_indicators: List[str] = []
if isinstance(fact.get("indicators"), list):
explicit_indicators = [str(i).lower() for i in fact["indicators"]]
if isinstance(fact.get("escalation_triggers"), list):
explicit_indicators += [str(i).lower() for i in fact["escalation_triggers"]]
for trigger in SEV_ESCALATION_TRIGGERS:
indicator_key = trigger["indicator"].replace("_", " ")
indicator_raw = trigger["indicator"].lower()
if (
indicator_key in searchable
or indicator_raw in searchable
or indicator_raw in explicit_indicators
):
return trigger["escalate_to"]
return None
# ---------------------------------------------------------------------------
# Severity Normalisation Helpers
# ---------------------------------------------------------------------------
_SEV_ORDER = {"sev1": 1, "sev2": 2, "sev3": 3, "sev4": 4}
def _sev_to_int(sev: str) -> int:
return _SEV_ORDER.get(sev.lower(), 4)
def _int_to_sev(n: int) -> str:
return {1: "sev1", 2: "sev2", 3: "sev3", 4: "sev4"}.get(n, "sev4")
def _escalate_sev(current: str, target: str) -> str:
"""Return the higher severity (lower SEV number)."""
return _int_to_sev(min(_sev_to_int(current), _sev_to_int(target)))
# ---------------------------------------------------------------------------
# Text Report
# ---------------------------------------------------------------------------
def _print_text_report(result: dict) -> None:
"""Print a human-readable triage report to stdout."""
sep = "=" * 70
print(sep)
print(" INCIDENT TRIAGE REPORT")
print(sep)
print(f" Timestamp : {result.get('timestamp_utc', 'N/A')}")
print(f" Incident Type : {result.get('incident_type', 'unknown').upper()}")
print(f" Severity : {result.get('severity', 'N/A').upper()}")
print(f" Confidence : {result.get('classification_confidence', 0.0):.0%}")
print(sep)
fp = result.get("false_positive_indicators", [])
if fp:
print(f"\n [!] FALSE POSITIVE FLAGS: {', '.join(fp)}")
print(" Review before escalating.")
esc_trigger = result.get("escalation_trigger_fired")
if esc_trigger:
print(f"\n [!] ESCALATION TRIGGER FIRED -> {esc_trigger.upper()}")
path = result.get("escalation_path", {})
print(f"\n Escalate To : {path.get('escalate_to', 'N/A')}")
print(f" Response SLA : {path.get('response_sla_minutes', 'N/A')} minutes")
print(f" Bridge Call : {'YES' if path.get('bridge_call') else 'no'}")
print(f" War Room : {'YES' if path.get('war_room') else 'no'}")
print(f" MITRE : {path.get('mitre_technique', 'N/A')}")
forensics = result.get("forensic_analysis", {})
if forensics:
print(f"\n Forensic Fields:")
print(f" Source IP : {forensics.get('source_ip', 'N/A')}")
print(f" User Account : {forensics.get('user_account', 'N/A')}")
print(f" Hostname : {forensics.get('hostname', 'N/A')}")
print(f" Process : {forensics.get('process_name', 'N/A')}")
print(f" Dwell (hrs) : {forensics.get('dwell_hours', 0.0)}")
print(f" Dwell Severity: {forensics.get('dwell_severity', 'N/A')}")
ioc_summary = result.get("ioc_summary", {})
if ioc_summary:
print(f"\n IOC Summary:")
print(f" Total IOCs : {ioc_summary.get('total_ioc_count', 0)}")
if ioc_summary.get("ip_indicators"):
print(f" IPs : {', '.join(ioc_summary['ip_indicators'])}")
if ioc_summary.get("hash_indicators"):
print(f" Hashes : {len(ioc_summary['hash_indicators'])} hash(es)")
print(f" Evidence Srcs : {', '.join(ioc_summary.get('evidence_sources_applicable', []))}")
print(f"\n Recommended Action: {result.get('recommended_action', 'N/A')}")
print(sep)
# ---------------------------------------------------------------------------
# Main Entry Point
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Incident Classification, Triage, and Escalation",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
echo '{"event_type": "ransomware"}' | %(prog)s --json
%(prog)s --input event.json --classify --false-positive-check --json
%(prog)s --input event.json --severity sev1 --json
Exit codes:
0 SEV3/SEV4 or no confirmed incident
1 SEV2 — elevated response required
2 SEV1 — critical incident declared
""",
)
parser.add_argument(
"--input", "-i",
metavar="FILE",
help="JSON file path containing the security event (default: stdin)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output results as JSON",
)
parser.add_argument(
"--classify",
action="store_true",
help="Run incident classification against INCIDENT_TAXONOMY",
)
parser.add_argument(
"--false-positive-check",
action="store_true",
dest="false_positive_check",
help="Run false positive filter checks",
)
parser.add_argument(
"--severity",
choices=["sev1", "sev2", "sev3", "sev4"],
help="Explicit severity override (skips taxonomy-derived severity)",
)
args = parser.parse_args()
# --- Load input ---
try:
if args.input:
with open(args.input, "r", encoding="utf-8") as fh:
raw_event = json.load(fh)
else:
raw_event = json.load(sys.stdin)
except json.JSONDecodeError as exc:
msg = {"error": f"Invalid JSON input: {exc}"}
if args.json:
print(json.dumps(msg, indent=2))
else:
print(f"Error: {msg['error']}", file=sys.stderr)
sys.exit(1)
except FileNotFoundError as exc:
msg = {"error": str(exc)}
if args.json:
print(json.dumps(msg, indent=2))
else:
print(f"Error: {msg['error']}", file=sys.stderr)
sys.exit(1)
# --- Forensic pre-analysis (base logic) ---
fields = parse_forensic_fields(raw_event)
ioc_summary = build_ioc_summary(fields)
forensic_analysis = {
"source_ip": fields["source_ip"],
"destination_ip": fields["destination_ip"],
"user_account": fields["user_account"],
"hostname": fields["hostname"],
"process_name": fields["process_name"],
"dwell_hours": fields["dwell_hours"],
"dwell_severity": assess_dwell_severity(fields["dwell_hours"]),
}
# --- Classification ---
incident_type = "unknown"
confidence = 0.0
if args.classify or not args.severity:
incident_type, confidence = classify_incident(raw_event)
# Override with explicit event_type if classify not run
if not args.classify:
et = str(raw_event.get("event_type", "")).lower().replace(" ", "_").replace("-", "_")
if et in INCIDENT_TAXONOMY:
incident_type = et
confidence = 0.75
# --- Determine base severity ---
if args.severity:
severity = args.severity.lower()
else:
taxonomy_entry = INCIDENT_TAXONOMY.get(incident_type, {})
severity = taxonomy_entry.get("default_severity", "sev4")
# Factor in dwell severity
dwell_sev_map = {"critical": "sev1", "high": "sev2", "medium": "sev3", "low": "sev4"}
dwell_derived = dwell_sev_map.get(forensic_analysis["dwell_severity"], "sev4")
severity = _escalate_sev(severity, dwell_derived)
# --- Escalation trigger check ---
escalation_trigger_fired: Optional[str] = None
trigger_result = check_sev_escalation_triggers(raw_event)
if trigger_result:
escalation_trigger_fired = trigger_result
severity = _escalate_sev(severity, trigger_result)
# --- False positive check ---
fp_indicators: List[str] = []
if args.false_positive_check:
fp_indicators = check_false_positives(raw_event)
# --- Escalation path ---
escalation_path = get_escalation_path(incident_type, severity)
# --- Recommended action ---
if fp_indicators:
recommended_action = (
f"Verify false positive flags before escalating: {', '.join(fp_indicators)}. "
"Confirm with asset owner and close or reclassify."
)
elif severity == "sev1":
recommended_action = (
"IMMEDIATE: Declare SEV1, open war room, page CISO and CEO. "
"Isolate affected systems, preserve evidence, activate IR playbook."
)
elif severity == "sev2":
recommended_action = (
"URGENT: Page SOC Lead and CISO. Open bridge call. "
"Contain impacted accounts/hosts and begin forensic collection."
)
elif severity == "sev3":
recommended_action = (
"Notify SOC Lead and Security Manager. "
"Investigate during business hours and document findings."
)
else:
recommended_action = (
"Queue for L3 Analyst review. "
"Document and track per standard operating procedure."
)
# --- Assemble output ---
result: Dict[str, Any] = {
"incident_type": incident_type,
"classification_confidence": confidence,
"severity": severity,
"false_positive_indicators": fp_indicators,
"escalation_trigger_fired": escalation_trigger_fired,
"escalation_path": escalation_path,
"forensic_analysis": forensic_analysis,
"ioc_summary": ioc_summary,
"recommended_action": recommended_action,
"taxonomy": INCIDENT_TAXONOMY.get(incident_type, {}),
"timestamp_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
}
# --- Output ---
if args.json:
print(json.dumps(result, indent=2))
else:
_print_text_report(result)
# --- Exit code ---
if severity == "sev1":
sys.exit(2)
elif severity == "sev2":
sys.exit(1)
else:
sys.exit(0)
if __name__ == "__main__":
main()
Triển khai ISMS ISO 27001 và quản trị an ninh mạng cho HealthTech/MedTech: đánh giá rủi ro, kiểm soát, chứng nhận và audit bảo mật.
---
name: "information-security-manager-iso27001"
description: ISO 27001 ISMS implementation and cybersecurity governance for HealthTech and MedTech companies. Use for ISMS design, security risk assessment, control implementation, ISO 27001 certification, security audits, incident response, and compliance verification. Covers ISO 27001, ISO 27002, healthcare security, and medical device cybersecurity.
---
# Information Security Manager - ISO 27001
Implement and manage Information Security Management Systems (ISMS) aligned with ISO 27001:2022 and healthcare regulatory requirements.
---
## Table of Contents
- [Trigger Phrases](#trigger-phrases)
- [Quick Start](#quick-start)
- [Tools](#tools)
- [Workflows](#workflows)
- [Reference Guides](#reference-guides)
- [Validation Checkpoints](#validation-checkpoints)
---
## Trigger Phrases
Use this skill when you hear:
- "implement ISO 27001"
- "ISMS implementation"
- "security risk assessment"
- "information security policy"
- "ISO 27001 certification"
- "security controls implementation"
- "incident response plan"
- "healthcare data security"
- "medical device cybersecurity"
- "security compliance audit"
---
## Quick Start
### Run Security Risk Assessment
```bash
python scripts/risk_assessment.py --scope "patient-data-system" --output risk_register.json
```
### Check Compliance Status
```bash
python scripts/compliance_checker.py --standard iso27001 --controls-file controls.csv
```
### Generate Gap Analysis Report
```bash
python scripts/compliance_checker.py --standard iso27001 --gap-analysis --output gaps.md
```
---
## Tools
### risk_assessment.py
Automated security risk assessment following ISO 27001 Clause 6.1.2 methodology.
**Usage:**
```bash
# Full risk assessment
python scripts/risk_assessment.py --scope "cloud-infrastructure" --output risks.json
# Healthcare-specific assessment
python scripts/risk_assessment.py --scope "ehr-system" --template healthcare --output risks.json
# Quick asset-based assessment
python scripts/risk_assessment.py --assets assets.csv --output risks.json
```
**Parameters:**
| Parameter | Required | Description |
|-----------|----------|-------------|
| `--scope` | Yes | System or area to assess |
| `--template` | No | Assessment template: `general`, `healthcare`, `cloud` |
| `--assets` | No | CSV file with asset inventory |
| `--output` | No | Output file (default: stdout) |
| `--format` | No | Output format: `json`, `csv`, `markdown` |
**Output:**
- Asset inventory with classification
- Threat and vulnerability mapping
- Risk scores (likelihood × impact)
- Treatment recommendations
- Residual risk calculations
### compliance_checker.py
Verify ISO 27001/27002 control implementation status.
**Usage:**
```bash
# Check all ISO 27001 controls
python scripts/compliance_checker.py --standard iso27001
# Gap analysis with recommendations
python scripts/compliance_checker.py --standard iso27001 --gap-analysis
# Check specific control domains
python scripts/compliance_checker.py --standard iso27001 --domains "access-control,cryptography"
# Export compliance report
python scripts/compliance_checker.py --standard iso27001 --output compliance_report.md
```
**Parameters:**
| Parameter | Required | Description |
|-----------|----------|-------------|
| `--standard` | Yes | Standard to check: `iso27001`, `iso27002`, `hipaa` |
| `--controls-file` | No | CSV with current control status |
| `--gap-analysis` | No | Include remediation recommendations |
| `--domains` | No | Specific control domains to check |
| `--output` | No | Output file path |
**Output:**
- Control implementation status
- Compliance percentage by domain
- Gap analysis with priorities
- Remediation recommendations
---
## Workflows
### Workflow 1: ISMS Implementation
**Step 1: Define Scope and Context**
Document organizational context and ISMS boundaries:
- Identify interested parties and requirements
- Define ISMS scope and boundaries
- Document internal/external issues
**Validation:** Scope statement reviewed and approved by management.
**Step 2: Conduct Risk Assessment**
```bash
python scripts/risk_assessment.py --scope "full-organization" --template general --output initial_risks.json
```
- Identify information assets
- Assess threats and vulnerabilities
- Calculate risk levels
- Determine risk treatment options
**Validation:** Risk register contains all critical assets with assigned owners.
**Step 3: Select and Implement Controls**
Map risks to ISO 27002 controls:
```bash
python scripts/compliance_checker.py --standard iso27002 --gap-analysis --output control_gaps.md
```
Control categories:
- Organizational (policies, roles, responsibilities)
- People (screening, awareness, training)
- Physical (perimeters, equipment, media)
- Technological (access, crypto, network, application)
**Validation:** Statement of Applicability (SoA) documents all controls with justification.
**Step 4: Establish Monitoring**
Define security metrics:
- Incident count and severity trends
- Control effectiveness scores
- Training completion rates
- Audit findings closure rate
**Validation:** Dashboard shows real-time compliance status.
### Workflow 2: Security Risk Assessment
**Step 1: Asset Identification**
Create asset inventory:
| Asset Type | Examples | Classification |
|------------|----------|----------------|
| Information | Patient records, source code | Confidential |
| Software | EHR system, APIs | Critical |
| Hardware | Servers, medical devices | High |
| Services | Cloud hosting, backup | High |
| People | Admin accounts, developers | Varies |
**Validation:** All assets have assigned owners and classifications.
**Step 2: Threat Analysis**
Identify threats per asset category:
| Asset | Threats | Likelihood |
|-------|---------|------------|
| Patient data | Unauthorized access, breach | High |
| Medical devices | Malware, tampering | Medium |
| Cloud services | Misconfiguration, outage | Medium |
| Credentials | Phishing, brute force | High |
**Validation:** Threat model covers top-10 industry threats.
**Step 3: Vulnerability Assessment**
```bash
python scripts/risk_assessment.py --scope "network-infrastructure" --output vuln_risks.json
```
Document vulnerabilities:
- Technical (unpatched systems, weak configs)
- Process (missing procedures, gaps)
- People (lack of training, insider risk)
**Validation:** Vulnerability scan results mapped to risk register.
**Step 4: Risk Evaluation and Treatment**
Calculate risk: `Risk = Likelihood × Impact`
| Risk Level | Score | Treatment |
|------------|-------|-----------|
| Critical | 20-25 | Immediate action required |
| High | 15-19 | Treatment plan within 30 days |
| Medium | 10-14 | Treatment plan within 90 days |
| Low | 5-9 | Accept or monitor |
| Minimal | 1-4 | Accept |
**Validation:** All high/critical risks have approved treatment plans.
### Workflow 3: Incident Response
**Step 1: Detection and Reporting**
Incident categories:
- Security breach (unauthorized access)
- Malware infection
- Data leakage
- System compromise
- Policy violation
**Validation:** Incident logged within 15 minutes of detection.
**Step 2: Triage and Classification**
| Severity | Criteria | Response Time |
|----------|----------|---------------|
| Critical | Data breach, system down | Immediate |
| High | Active threat, significant risk | 1 hour |
| Medium | Contained threat, limited impact | 4 hours |
| Low | Minor violation, no impact | 24 hours |
**Validation:** Severity assigned and escalation triggered if needed.
**Step 3: Containment and Eradication**
Immediate actions:
1. Isolate affected systems
2. Preserve evidence
3. Block threat vectors
4. Remove malicious artifacts
**Validation:** Containment confirmed, no ongoing compromise.
**Step 4: Recovery and Lessons Learned**
Post-incident activities:
1. Restore systems from clean backups
2. Verify integrity before reconnection
3. Document timeline and actions
4. Conduct post-incident review
5. Update controls and procedures
**Validation:** Post-incident report completed within 5 business days.
---
## Reference Guides
### When to Use Each Reference
**references/iso27001-controls.md**
- Control selection for SoA
- Implementation guidance
- Evidence requirements
- Audit preparation
**references/risk-assessment-guide.md**
- Risk methodology selection
- Asset classification criteria
- Threat modeling approaches
- Risk calculation methods
**references/incident-response.md**
- Response procedures
- Escalation matrices
- Communication templates
- Recovery checklists
---
## Validation Checkpoints
### ISMS Implementation Validation
| Phase | Checkpoint | Evidence Required |
|-------|------------|-------------------|
| Scope | Scope approved | Signed scope document |
| Risk | Register complete | Risk register with owners |
| Controls | SoA approved | Statement of Applicability |
| Operation | Metrics active | Dashboard screenshots |
| Audit | Internal audit done | Audit report |
### Certification Readiness
Before Stage 1 audit:
- [ ] ISMS scope documented and approved
- [ ] Information security policy published
- [ ] Risk assessment completed
- [ ] Statement of Applicability finalized
- [ ] Internal audit conducted
- [ ] Management review completed
- [ ] Nonconformities addressed
Before Stage 2 audit:
- [ ] Controls implemented and operational
- [ ] Evidence of effectiveness available
- [ ] Staff trained and aware
- [ ] Incidents logged and managed
- [ ] Metrics collected for 3+ months
### Compliance Verification
Run periodic checks:
```bash
# Monthly compliance check
python scripts/compliance_checker.py --standard iso27001 --output monthly_$(date +%Y%m).md
# Quarterly gap analysis
python scripts/compliance_checker.py --standard iso27001 --gap-analysis --output quarterly_gaps.md
```
---
## Worked Example: Healthcare Risk Assessment
**Scenario:** Assess security risks for a patient data management system.
### Step 1: Define Assets
```bash
python scripts/risk_assessment.py --scope "patient-data-system" --template healthcare
```
**Asset inventory output:**
| Asset ID | Asset | Type | Owner | Classification |
|----------|-------|------|-------|----------------|
| A001 | Patient database | Information | DBA Team | Confidential |
| A002 | EHR application | Software | App Team | Critical |
| A003 | Database server | Hardware | Infra Team | High |
| A004 | Admin credentials | Access | Security | Critical |
### Step 2: Identify Risks
**Risk register output:**
| Risk ID | Asset | Threat | Vulnerability | L | I | Score |
|---------|-------|--------|---------------|---|---|-------|
| R001 | A001 | Data breach | Weak encryption | 3 | 5 | 15 |
| R002 | A002 | SQL injection | Input validation | 4 | 4 | 16 |
| R003 | A004 | Credential theft | No MFA | 4 | 5 | 20 |
### Step 3: Determine Treatment
| Risk | Treatment | Control | Timeline |
|------|-----------|---------|----------|
| R001 | Mitigate | Implement AES-256 encryption | 30 days |
| R002 | Mitigate | Add input validation, WAF | 14 days |
| R003 | Mitigate | Enforce MFA for all admins | 7 days |
### Step 4: Verify Implementation
```bash
python scripts/compliance_checker.py --controls-file implemented_controls.csv
```
**Verification output:**
```
Control Implementation Status
=============================
Cryptography (A.8.24): IMPLEMENTED
- AES-256 at rest: YES
- TLS 1.3 in transit: YES
Access Control (A.8.5): IMPLEMENTED
- MFA enabled: YES
- Admin accounts: 100% coverage
Application Security (A.8.26): PARTIAL
- Input validation: YES
- WAF deployed: PENDING
Overall Compliance: 87%
```
FILE:references/incident-response.md
# Incident Response Procedures
Security incident detection, response, and recovery procedures per ISO 27001 requirements.
---
## Table of Contents
- [Incident Classification](#incident-classification)
- [Response Procedures](#response-procedures)
- [Escalation Matrix](#escalation-matrix)
- [Communication Templates](#communication-templates)
- [Recovery Checklists](#recovery-checklists)
- [Post-Incident Activities](#post-incident-activities)
---
## Incident Classification
### Incident Categories
| Category | Description | Examples |
|----------|-------------|----------|
| Security Breach | Unauthorized access to systems/data | Account compromise, data exfiltration |
| Malware | Malicious software infection | Ransomware, virus, trojan |
| Data Leakage | Unauthorized data disclosure | Accidental email, misconfigured storage |
| Denial of Service | Service availability impact | DDoS attack, resource exhaustion |
| Policy Violation | Security policy breach | Unauthorized software, data handling |
| Physical | Physical security incident | Theft, unauthorized entry |
### Severity Levels
| Level | Criteria | Response Time | Examples |
|-------|----------|---------------|----------|
| **Critical (P1)** | Active breach, data loss, system down | Immediate (15 min) | Ransomware, confirmed breach |
| **High (P2)** | Active threat, potential data exposure | 1 hour | Malware detected, suspicious access |
| **Medium (P3)** | Contained threat, limited impact | 4 hours | Failed attacks, policy violations |
| **Low (P4)** | Minor issue, no immediate risk | 24 hours | Suspicious emails, minor violations |
### Severity Decision Tree
```
Is there active data loss or system compromise?
├── Yes → CRITICAL (P1)
└── No → Is there an active uncontained threat?
├── Yes → HIGH (P2)
└── No → Is there potential for data exposure?
├── Yes → MEDIUM (P3)
└── No → LOW (P4)
```
---
## Response Procedures
### Phase 1: Detection and Reporting
**Objective:** Identify and report security incidents promptly.
**Steps:**
1. Identify potential incident through monitoring, alerts, or reports
2. Document initial observations (time, systems, symptoms)
3. Report to Security Team via designated channel
4. Assign incident ID and log in tracking system
**Validation:** Incident logged within 15 minutes of detection.
**Documentation Required:**
- Date/time of detection
- Detection source (monitoring, user report, automated alert)
- Affected systems/users (initial assessment)
- Reporter information
### Phase 2: Triage and Assessment
**Objective:** Determine incident scope and severity.
**Steps:**
1. Gather additional information (logs, system state)
2. Determine incident category and severity
3. Identify affected assets and potential impact
4. Assign incident owner and response team
**Validation:** Severity assigned and escalation triggered if needed.
**Assessment Checklist:**
- [ ] Systems affected identified
- [ ] Data types potentially impacted
- [ ] Attack vector determined
- [ ] Scope (single system vs. widespread)
- [ ] Business impact assessed
### Phase 3: Containment
**Objective:** Limit damage and prevent spread.
**Immediate Containment (Short-term):**
1. Isolate affected systems from network
2. Disable compromised accounts
3. Block malicious IPs/domains
4. Preserve evidence before changes
**Long-term Containment:**
1. Apply temporary fixes
2. Implement additional monitoring
3. Strengthen access controls
4. Prepare for eradication
**Validation:** Containment confirmed, no ongoing spread.
**Containment Actions by Incident Type:**
| Incident Type | Containment Actions |
|---------------|---------------------|
| Account Compromise | Disable account, revoke sessions, reset credentials |
| Malware | Isolate host, block C2 domains, scan related systems |
| Data Breach | Block exfiltration path, revoke access, enable DLP |
| DDoS | Enable DDoS protection, rate limiting, traffic scrubbing |
### Phase 4: Eradication
**Objective:** Remove threat from environment.
**Steps:**
1. Identify root cause
2. Remove malware/backdoors
3. Close vulnerabilities exploited
4. Reset compromised credentials
5. Verify threat elimination
**Validation:** No indicators of compromise remain.
**Eradication Checklist:**
- [ ] Malware removed from all systems
- [ ] Vulnerabilities patched
- [ ] Backdoors/persistence removed
- [ ] Compromised credentials rotated
- [ ] Security gaps closed
### Phase 5: Recovery
**Objective:** Restore systems to normal operation.
**Steps:**
1. Restore from clean backups if needed
2. Rebuild compromised systems
3. Verify system integrity
4. Monitor for re-infection
5. Return to production gradually
**Validation:** Systems operational with enhanced monitoring.
**Recovery Checklist:**
- [ ] Systems restored to known-good state
- [ ] Integrity verification completed
- [ ] Enhanced monitoring in place
- [ ] Business operations resumed
- [ ] User access restored (verified accounts only)
### Phase 6: Lessons Learned
**Objective:** Improve security posture and response capability.
**Steps:**
1. Conduct post-incident review (within 5 business days)
2. Document timeline and actions taken
3. Identify what worked and what didn't
4. Update procedures and controls
5. Share relevant findings (internally, externally if required)
**Validation:** Post-incident report completed and actions tracked.
---
## Escalation Matrix
### Escalation Paths
| Severity | Initial Response | 1 Hour | 4 Hours | 24 Hours |
|----------|------------------|--------|---------|----------|
| Critical | Security Team | CISO + Management | Executive Team | Board notification |
| High | Security Team | CISO | Management | - |
| Medium | Security Team | Security Manager | CISO if unresolved | - |
| Low | Security Analyst | Security Team Lead | - | - |
### Contact Information (Template)
| Role | Primary | Backup | Contact Method |
|------|---------|--------|----------------|
| Security On-Call | [Name] | [Name] | Phone, Slack |
| CISO | [Name] | [Name] | Phone, Email |
| IT Director | [Name] | [Name] | Phone, Email |
| Legal Counsel | [Name] | [Firm] | Phone |
| PR/Communications | [Name] | [Name] | Phone |
| Executive Sponsor | [Name] | [Name] | Phone |
### External Notifications
| Condition | Notify | Timeline |
|-----------|--------|----------|
| Patient data breach | HHS (HIPAA) | 60 days |
| EU personal data breach | Supervisory Authority (GDPR) | 72 hours |
| Significant breach | Law enforcement | As appropriate |
| Third-party involved | Affected vendor | Immediately |
---
## Communication Templates
### Internal Notification (Initial)
```
Subject: [SEVERITY] Security Incident - [Brief Description]
INCIDENT SUMMARY
----------------
Incident ID: INC-[YYYY]-[###]
Detected: [Date/Time]
Severity: [Critical/High/Medium/Low]
Status: [Investigating/Contained/Resolved]
WHAT HAPPENED
[Brief description of the incident]
CURRENT IMPACT
[Systems affected, business impact]
ACTIONS BEING TAKEN
[Current response activities]
WHAT YOU NEED TO DO
[Any required user actions]
NEXT UPDATE
Expected by: [Time]
Contact: Security Team - [contact info]
```
### External Notification (Breach)
```
Subject: Important Security Notice from [Organization]
Dear [Affected Party],
We are writing to inform you of a security incident that may have
involved your personal information.
WHAT HAPPENED
On [date], we discovered [brief description].
WHAT INFORMATION WAS INVOLVED
[Types of data potentially affected]
WHAT WE ARE DOING
[Actions taken to address the incident]
WHAT YOU CAN DO
[Recommended protective actions]
FOR MORE INFORMATION
[Contact information, resources]
We sincerely regret any concern this may cause and are committed
to protecting your information.
[Signature]
```
### Status Update
```
Subject: UPDATE: Security Incident INC-[ID] - [Status]
CURRENT STATUS
--------------
Status: [Contained/Eradicating/Recovering]
Last Update: [Time]
PROGRESS SINCE LAST UPDATE
[Actions completed]
CURRENT ACTIVITIES
[Ongoing response work]
REMAINING ACTIONS
[What still needs to be done]
ESTIMATED RESOLUTION
[Timeframe if known]
NEXT UPDATE
Expected: [Time]
```
---
## Recovery Checklists
### System Recovery Checklist
- [ ] Verify backup integrity before restoration
- [ ] Restore to isolated environment first
- [ ] Scan restored systems for malware
- [ ] Apply all security patches
- [ ] Reset all credentials on system
- [ ] Review and harden configurations
- [ ] Verify application functionality
- [ ] Enable enhanced logging/monitoring
- [ ] Conduct security scan before production
- [ ] Document recovery steps taken
### Account Compromise Recovery
- [ ] Disable compromised account
- [ ] Revoke all active sessions
- [ ] Reset password with strong credential
- [ ] Enable MFA if not already
- [ ] Review account activity logs
- [ ] Check for unauthorized changes
- [ ] Review connected applications
- [ ] Verify account recovery options
- [ ] Notify account owner securely
- [ ] Monitor for suspicious activity
### Ransomware Recovery
- [ ] Isolate affected systems immediately
- [ ] Identify ransomware variant
- [ ] Check for decryption tools available
- [ ] Assess backup availability/integrity
- [ ] Report to law enforcement
- [ ] Document encrypted files/systems
- [ ] Restore from clean backups
- [ ] Rebuild systems that cannot be restored
- [ ] Patch vulnerability exploited
- [ ] Implement additional controls
---
## Post-Incident Activities
### Post-Incident Review Meeting
**Timing:** Within 5 business days of resolution
**Attendees:**
- Incident response team
- Affected system owners
- Security management
- Relevant stakeholders
**Agenda:**
1. Incident timeline review
2. What worked well
3. What could be improved
4. Root cause analysis
5. Preventive measures
6. Action items and owners
### Post-Incident Report Template
```
INCIDENT POST-MORTEM REPORT
===========================
Incident ID: INC-[YYYY]-[###]
Date: [Report date]
Author: [Name]
Classification: [Internal/Confidential]
EXECUTIVE SUMMARY
[2-3 paragraph summary]
INCIDENT TIMELINE
[Detailed chronological events]
ROOT CAUSE ANALYSIS
[5 Whys or similar analysis]
IMPACT ASSESSMENT
- Systems affected: [list]
- Data impacted: [description]
- Business impact: [description]
- Financial impact: [estimate if known]
RESPONSE EFFECTIVENESS
What worked well:
- [item]
- [item]
Areas for improvement:
- [item]
- [item]
RECOMMENDATIONS
| # | Recommendation | Priority | Owner | Due Date |
|---|----------------|----------|-------|----------|
| 1 | [action] | High | [name] | [date] |
| 2 | [action] | Medium | [name] | [date] |
LESSONS LEARNED
[Key takeaways for future incidents]
APPENDICES
- Detailed logs
- Evidence inventory
- Communication records
```
### Metrics to Track
| Metric | Target | Purpose |
|--------|--------|---------|
| Mean Time to Detect (MTTD) | < 1 hour | Detection capability |
| Mean Time to Respond (MTTR) | < 4 hours | Response speed |
| Mean Time to Contain (MTTC) | < 2 hours | Containment effectiveness |
| Incidents by severity | Decreasing trend | Overall security posture |
| Repeat incidents | 0 | Root cause resolution |
FILE:references/iso27001-controls.md
# ISO 27001:2022 Controls Implementation Guide
Implementation guidance for Annex A controls with evidence requirements and audit preparation.
---
## Table of Contents
- [Control Categories Overview](#control-categories-overview)
- [Organizational Controls (A.5)](#organizational-controls-a5)
- [People Controls (A.6)](#people-controls-a6)
- [Physical Controls (A.7)](#physical-controls-a7)
- [Technological Controls (A.8)](#technological-controls-a8)
- [Evidence Requirements](#evidence-requirements)
- [Statement of Applicability](#statement-of-applicability)
---
## Control Categories Overview
ISO 27001:2022 Annex A contains 93 controls across 4 categories:
| Category | Controls | Focus Areas |
|----------|----------|-------------|
| Organizational (A.5) | 37 | Policies, governance, supplier management |
| People (A.6) | 8 | HR security, awareness, remote working |
| Physical (A.7) | 14 | Perimeters, equipment, environment |
| Technological (A.8) | 34 | Access, crypto, network, development |
---
## Organizational Controls (A.5)
### A.5.1 - Policies for Information Security
**Requirement:** Define, approve, publish, and communicate information security policies.
**Implementation:**
1. Draft information security policy covering scope, objectives, principles
2. Obtain management approval signature
3. Communicate to all employees and relevant parties
4. Review annually or after significant changes
**Evidence:**
- Signed policy document
- Communication records (email, intranet)
- Acknowledgment records
- Review meeting minutes
### A.5.2 - Information Security Roles and Responsibilities
**Requirement:** Define and allocate information security responsibilities.
**Implementation:**
1. Create RACI matrix for security activities
2. Appoint Information Security Manager
3. Define responsibilities in job descriptions
4. Establish reporting lines
**Evidence:**
- RACI matrix document
- ISM appointment letter
- Job descriptions with security duties
- Organizational chart
### A.5.9 - Inventory of Information and Assets
**Requirement:** Identify and maintain inventory of information assets.
**Implementation:**
1. Create asset register with classification
2. Assign owners for each asset
3. Define acceptable use rules
4. Review quarterly
**Evidence:**
- Asset inventory/register
- Classification scheme
- Owner assignment records
- Review logs
### A.5.15 - Access Control
**Requirement:** Establish and implement rules for controlling access.
**Implementation:**
1. Document access control policy
2. Implement role-based access control (RBAC)
3. Define access provisioning/deprovisioning process
4. Conduct access reviews quarterly
**Evidence:**
- Access control policy
- RBAC role definitions
- Access request forms
- Review reports
---
## People Controls (A.6)
### A.6.1 - Screening
**Requirement:** Verify backgrounds of candidates prior to employment.
**Implementation:**
1. Define screening requirements by role
2. Conduct background checks
3. Verify references and qualifications
4. Document screening results
**Evidence:**
- Screening policy
- Background check reports
- Verification records
- Consent forms
### A.6.3 - Information Security Awareness and Training
**Requirement:** Ensure personnel receive appropriate awareness and training.
**Implementation:**
1. Develop annual training program
2. Include role-specific training
3. Conduct phishing simulations
4. Track completion and effectiveness
**Evidence:**
- Training materials
- Completion records
- Test/quiz results
- Phishing simulation reports
### A.6.7 - Remote Working
**Requirement:** Implement security measures for remote working.
**Implementation:**
1. Establish remote working policy
2. Require VPN for network access
3. Mandate endpoint protection
4. Secure home network guidance
**Evidence:**
- Remote working policy
- VPN configuration records
- Endpoint compliance reports
- User acknowledgments
---
## Physical Controls (A.7)
### A.7.1 - Physical Security Perimeters
**Requirement:** Define and use security perimeters to protect information.
**Implementation:**
1. Define secure areas and boundaries
2. Implement access controls (badges, locks)
3. Monitor entry points
4. Maintain visitor logs
**Evidence:**
- Site security plan
- Access control system records
- CCTV footage retention
- Visitor logs
### A.7.4 - Physical Security Monitoring
**Requirement:** Monitor premises continuously for unauthorized access.
**Implementation:**
1. Deploy CCTV coverage
2. Implement intrusion detection
3. Define monitoring procedures
4. Establish incident response
**Evidence:**
- CCTV deployment records
- Monitoring procedures
- Alert configurations
- Incident logs
---
## Technological Controls (A.8)
### A.8.2 - Privileged Access Rights
**Requirement:** Restrict and manage privileged access.
**Implementation:**
1. Implement privileged access management (PAM)
2. Enforce separate admin accounts
3. Require MFA for privileged access
4. Monitor and log privileged activities
**Evidence:**
- PAM solution records
- Admin account inventory
- MFA enforcement reports
- Privileged activity logs
### A.8.5 - Secure Authentication
**Requirement:** Implement secure authentication mechanisms.
**Implementation:**
1. Enforce strong password policy
2. Implement MFA for all users
3. Use secure authentication protocols
4. Monitor authentication events
**Evidence:**
- Password policy
- MFA enrollment records
- Authentication configuration
- Failed login reports
### A.8.7 - Protection Against Malware
**Requirement:** Implement detection, prevention, and recovery for malware.
**Implementation:**
1. Deploy endpoint protection on all devices
2. Configure automatic updates
3. Implement email filtering
4. Define malware incident response
**Evidence:**
- Endpoint protection deployment
- Update/patch status
- Email filter configuration
- Malware incident records
### A.8.8 - Management of Technical Vulnerabilities
**Requirement:** Identify and address technical vulnerabilities.
**Implementation:**
1. Conduct regular vulnerability scans
2. Define remediation SLAs by severity
3. Track remediation progress
4. Verify patches applied
**Evidence:**
- Vulnerability scan reports
- Remediation tracking
- Patch deployment records
- Penetration test reports
### A.8.13 - Information Backup
**Requirement:** Maintain and test backup copies of information.
**Implementation:**
1. Define backup policy (frequency, retention)
2. Implement automated backups
3. Encrypt backup data
4. Test restoration regularly
**Evidence:**
- Backup policy
- Backup job logs
- Encryption configuration
- Restoration test records
### A.8.15 - Logging
**Requirement:** Produce, retain, and protect logs of activities.
**Implementation:**
1. Define logging requirements
2. Deploy centralized log management (SIEM)
3. Set retention periods per compliance
4. Protect log integrity
**Evidence:**
- Logging policy
- SIEM configuration
- Log retention settings
- Access controls on logs
### A.8.24 - Use of Cryptography
**Requirement:** Define and implement cryptographic controls.
**Implementation:**
1. Document cryptography policy
2. Encrypt data at rest (AES-256)
3. Encrypt data in transit (TLS 1.3)
4. Manage keys securely
**Evidence:**
- Cryptography policy
- Encryption configuration
- Certificate inventory
- Key management procedures
---
## Evidence Requirements
### Document Evidence
| Control Area | Required Documents |
|-------------|-------------------|
| Policies | Approved policy documents |
| Procedures | Documented processes with version control |
| Records | Completed forms, logs, reports |
| Contracts | Signed agreements with security clauses |
### Technical Evidence
| Control Area | Required Evidence |
|-------------|------------------|
| Access Control | System configurations, access lists |
| Logging | SIEM dashboards, sample logs |
| Encryption | Configuration screenshots, certificate details |
| Vulnerability | Scan reports, remediation tracking |
### Retention Requirements
| Evidence Type | Minimum Retention |
|--------------|-------------------|
| Policies | Current + 2 previous versions |
| Audit reports | 3 years |
| Access logs | 1 year minimum |
| Incident records | 3 years |
| Training records | Duration of employment + 2 years |
---
## Statement of Applicability
### SoA Structure
For each Annex A control, document:
| Field | Description |
|-------|-------------|
| Control ID | A.5.1, A.8.24, etc. |
| Control Name | Official control title |
| Applicable | Yes/No |
| Justification | Why applicable or not |
| Implementation Status | Implemented, Partial, Planned, N/A |
| Implementation Description | How control is implemented |
| Evidence Reference | Links to evidence |
### Sample SoA Entry
```
Control: A.8.5 - Secure Authentication
Applicable: Yes
Justification: Required for all user and system access to protect
information assets from unauthorized access.
Implementation Status: Implemented
Implementation Description:
- MFA enforced for all user accounts via Azure AD
- Admin accounts require hardware token
- Password policy: 12+ chars, complexity, 90-day rotation
- Failed login lockout after 5 attempts
Evidence:
- Azure AD MFA configuration (screenshot)
- Password policy document (DOC-SEC-015)
- Authentication audit logs (SIEM dashboard)
```
### Exclusion Justification Examples
| Control | Justification for Exclusion |
|---------|---------------------------|
| A.7.x (Physical) | Cloud-only operations, no physical facilities |
| A.8.19 (Software) | No user-installed software permitted |
| A.8.23 (Web filter) | Handled by cloud proxy service |
FILE:references/risk-assessment-guide.md
# Risk Assessment Methodology Guide
Comprehensive guidance for conducting information security risk assessments per ISO 27001 Clause 6.1.2.
---
## Table of Contents
- [Risk Assessment Process](#risk-assessment-process)
- [Asset Identification](#asset-identification)
- [Threat Analysis](#threat-analysis)
- [Vulnerability Assessment](#vulnerability-assessment)
- [Risk Calculation](#risk-calculation)
- [Risk Treatment](#risk-treatment)
- [Templates and Tools](#templates-and-tools)
---
## Risk Assessment Process
### ISO 27001 Requirements (Clause 6.1.2)
The organization shall:
1. Define risk assessment process
2. Establish risk criteria (acceptance, assessment)
3. Identify information security risks
4. Analyze and evaluate risks
5. Ensure repeatable and consistent results
### Process Overview
```
1. Context → 2. Asset ID → 3. Threat ID → 4. Vuln ID → 5. Risk Calc → 6. Treatment
↑ |
└──────────────────── Review & Update ←───────────────────────────────┘
```
---
## Asset Identification
### Asset Categories
| Category | Examples | Typical Classification |
|----------|----------|----------------------|
| Information | Patient records, source code, contracts | Confidential-Critical |
| Software | EHR systems, databases, custom apps | High-Critical |
| Hardware | Servers, medical devices, network gear | High |
| Services | Cloud hosting, backup, email | High |
| People | Admin accounts, key personnel | Critical |
| Intangibles | Reputation, intellectual property | High |
### Classification Scheme
| Level | Definition | Impact if Compromised |
|-------|------------|----------------------|
| Critical | Business-critical, regulated data | Severe - regulatory fines, safety risk |
| High | Important business data | Significant - major disruption |
| Medium | Internal business data | Moderate - operational impact |
| Low | Non-sensitive data | Minor - limited impact |
| Public | Intended for public release | Minimal - no impact |
### Asset Inventory Template
| ID | Asset Name | Type | Owner | Location | Classification | Value |
|----|------------|------|-------|----------|----------------|-------|
| A001 | Patient DB | Information | DBA Lead | AWS RDS | Critical | $5M |
| A002 | EHR App | Software | App Team | AWS ECS | Critical | $2M |
| A003 | Admin Creds | Access | Security | Vault | Critical | N/A |
---
## Threat Analysis
### Healthcare Threat Landscape
| Threat | Likelihood | Target Assets | Motivation |
|--------|------------|---------------|------------|
| Ransomware | High | All systems | Financial |
| Data breach | High | Patient data | Financial/Competitive |
| Phishing | Very High | User accounts | Access |
| Insider threat | Medium | Sensitive data | Various |
| DDoS | Medium | Public services | Disruption |
| Supply chain | Medium | Third-party systems | Access |
### Threat Modeling Approaches
**STRIDE Model:**
- **S**poofing identity
- **T**ampering with data
- **R**epudiation
- **I**nformation disclosure
- **D**enial of service
- **E**levation of privilege
**Threat Actor Categories:**
| Actor | Capability | Motivation | Typical Targets |
|-------|-----------|------------|-----------------|
| Nation-state | Very High | Espionage, disruption | Critical infrastructure |
| Organized crime | High | Financial gain | Healthcare, finance |
| Hacktivists | Medium | Ideology | Public-facing systems |
| Insiders | Varies | Financial, revenge | Sensitive data |
| Script kiddies | Low | Notoriety | Unpatched systems |
---
## Vulnerability Assessment
### Vulnerability Categories
| Category | Examples | Detection Method |
|----------|----------|------------------|
| Technical | Unpatched software, weak configs | Vulnerability scans |
| Process | Missing procedures, gaps | Process audits |
| People | Lack of training, social engineering | Phishing tests |
| Physical | Inadequate access controls | Physical audits |
### Vulnerability Scoring (CVSS Alignment)
| Score Range | Severity | Example |
|-------------|----------|---------|
| 9.0-10.0 | Critical | RCE without authentication |
| 7.0-8.9 | High | Authentication bypass |
| 4.0-6.9 | Medium | Information disclosure |
| 0.1-3.9 | Low | Minor configuration issue |
### Vulnerability Sources
1. **Automated Scans:** Nessus, Qualys, OpenVAS
2. **Penetration Testing:** Annual third-party tests
3. **Code Analysis:** SAST/DAST tools
4. **Configuration Audits:** CIS benchmarks
5. **Threat Intelligence:** CVE feeds, vendor advisories
---
## Risk Calculation
### Risk Formula
```
Risk = Likelihood × Impact
```
### Likelihood Scale (1-5)
| Score | Likelihood | Definition |
|-------|-----------|------------|
| 5 | Almost Certain | Expected to occur multiple times per year |
| 4 | Likely | Expected to occur at least once per year |
| 3 | Possible | Could occur within 2-3 years |
| 2 | Unlikely | Could occur within 5 years |
| 1 | Rare | Unlikely to occur |
### Impact Scale (1-5)
| Score | Impact | Financial | Operational | Reputational |
|-------|--------|-----------|-------------|--------------|
| 5 | Catastrophic | >$10M | Total shutdown | International news |
| 4 | Major | $1M-$10M | Major disruption | National news |
| 3 | Moderate | $100K-$1M | Significant impact | Local news |
| 2 | Minor | $10K-$100K | Minor disruption | Complaints |
| 1 | Negligible | <$10K | Minimal impact | Internal only |
### Risk Matrix
| | Impact 1 | Impact 2 | Impact 3 | Impact 4 | Impact 5 |
|-----|----------|----------|----------|----------|----------|
| **L5** | 5 (Low) | 10 (Med) | 15 (High) | 20 (Crit) | 25 (Crit) |
| **L4** | 4 (Low) | 8 (Med) | 12 (Med) | 16 (High) | 20 (Crit) |
| **L3** | 3 (Min) | 6 (Low) | 9 (Med) | 12 (Med) | 15 (High) |
| **L2** | 2 (Min) | 4 (Low) | 6 (Low) | 8 (Med) | 10 (Med) |
| **L1** | 1 (Min) | 2 (Min) | 3 (Min) | 4 (Low) | 5 (Low) |
### Risk Levels
| Level | Score Range | Action Required |
|-------|-------------|-----------------|
| Critical | 20-25 | Immediate action, escalate to management |
| High | 15-19 | Treatment plan within 30 days |
| Medium | 10-14 | Treatment plan within 90 days |
| Low | 5-9 | Accept or implement low-cost controls |
| Minimal | 1-4 | Accept risk, document decision |
---
## Risk Treatment
### Treatment Options (ISO 27001)
| Option | Description | When to Use |
|--------|-------------|-------------|
| Modify | Implement controls to reduce risk | Most risks |
| Avoid | Eliminate the risk source | Unacceptable risks |
| Share | Transfer via insurance/outsourcing | High financial impact |
| Retain | Accept the risk | Low risks, cost-prohibitive controls |
### Control Selection Criteria
1. **Effectiveness:** Reduces likelihood or impact
2. **Cost:** Implementation and maintenance costs
3. **Feasibility:** Technical and operational viability
4. **Compliance:** Meets regulatory requirements
5. **Integration:** Works with existing controls
### Residual Risk
After implementing controls:
```
Residual Risk = Inherent Risk × (1 - Control Effectiveness)
```
| Control Effectiveness | Residual Risk Factor |
|----------------------|---------------------|
| 90%+ | Very Low (0.1×) |
| 70-89% | Low (0.2-0.3×) |
| 50-69% | Moderate (0.4-0.5×) |
| <50% | Limited reduction |
---
## Templates and Tools
### Risk Register Template
| Risk ID | Asset | Threat | Vulnerability | L | I | Inherent | Control | Residual | Owner | Status |
|---------|-------|--------|---------------|---|---|----------|---------|----------|-------|--------|
| R001 | Patient DB | Data breach | Weak encryption | 4 | 5 | 20 | AES-256 | 8 | DBA | Open |
| R002 | Admin access | Credential theft | No MFA | 5 | 5 | 25 | MFA | 5 | Security | Closed |
### Risk Assessment Report Sections
1. **Executive Summary**
- Key findings
- Critical/high risks count
- Overall risk posture
2. **Methodology**
- Assessment scope
- Criteria used
- Limitations
3. **Asset Summary**
- Asset inventory
- Classification distribution
4. **Risk Findings**
- Risk register
- Heat map visualization
- Trend analysis
5. **Recommendations**
- Priority treatments
- Timeline and resources
- Residual risk projection
6. **Appendices**
- Detailed asset list
- Threat catalog
- Control mapping
FILE:scripts/compliance_checker.py
#!/usr/bin/env python3
"""
ISO 27001/27002 Compliance Checker
Verify control implementation status and generate compliance reports.
Supports gap analysis and remediation recommendations.
Usage:
python compliance_checker.py --standard iso27001
python compliance_checker.py --standard iso27001 --gap-analysis --output gaps.md
"""
import argparse
import csv
import json
import sys
from datetime import datetime
from typing import Dict, List, Any, Optional
# ISO 27001:2022 Annex A Controls (simplified)
ISO27001_CONTROLS = {
"organizational": {
"name": "Organizational Controls",
"controls": [
{"id": "A.5.1", "name": "Policies for information security", "priority": "high"},
{"id": "A.5.2", "name": "Information security roles and responsibilities", "priority": "high"},
{"id": "A.5.3", "name": "Segregation of duties", "priority": "medium"},
{"id": "A.5.4", "name": "Management responsibilities", "priority": "high"},
{"id": "A.5.5", "name": "Contact with authorities", "priority": "medium"},
{"id": "A.5.6", "name": "Contact with special interest groups", "priority": "low"},
{"id": "A.5.7", "name": "Threat intelligence", "priority": "medium"},
{"id": "A.5.8", "name": "Information security in project management", "priority": "medium"},
{"id": "A.5.9", "name": "Inventory of information and assets", "priority": "high"},
{"id": "A.5.10", "name": "Acceptable use of information", "priority": "high"},
]
},
"people": {
"name": "People Controls",
"controls": [
{"id": "A.6.1", "name": "Screening", "priority": "high"},
{"id": "A.6.2", "name": "Terms and conditions of employment", "priority": "high"},
{"id": "A.6.3", "name": "Information security awareness and training", "priority": "high"},
{"id": "A.6.4", "name": "Disciplinary process", "priority": "medium"},
{"id": "A.6.5", "name": "Responsibilities after termination", "priority": "high"},
{"id": "A.6.6", "name": "Confidentiality agreements", "priority": "high"},
{"id": "A.6.7", "name": "Remote working", "priority": "high"},
{"id": "A.6.8", "name": "Information security event reporting", "priority": "high"},
]
},
"physical": {
"name": "Physical Controls",
"controls": [
{"id": "A.7.1", "name": "Physical security perimeters", "priority": "high"},
{"id": "A.7.2", "name": "Physical entry", "priority": "high"},
{"id": "A.7.3", "name": "Securing offices and facilities", "priority": "medium"},
{"id": "A.7.4", "name": "Physical security monitoring", "priority": "medium"},
{"id": "A.7.5", "name": "Protecting against environmental threats", "priority": "medium"},
{"id": "A.7.6", "name": "Working in secure areas", "priority": "medium"},
{"id": "A.7.7", "name": "Clear desk and screen", "priority": "medium"},
{"id": "A.7.8", "name": "Equipment siting and protection", "priority": "medium"},
]
},
"technological": {
"name": "Technological Controls",
"controls": [
{"id": "A.8.1", "name": "User endpoint devices", "priority": "high"},
{"id": "A.8.2", "name": "Privileged access rights", "priority": "critical"},
{"id": "A.8.3", "name": "Information access restriction", "priority": "high"},
{"id": "A.8.4", "name": "Access to source code", "priority": "high"},
{"id": "A.8.5", "name": "Secure authentication", "priority": "critical"},
{"id": "A.8.6", "name": "Capacity management", "priority": "medium"},
{"id": "A.8.7", "name": "Protection against malware", "priority": "critical"},
{"id": "A.8.8", "name": "Management of technical vulnerabilities", "priority": "critical"},
{"id": "A.8.9", "name": "Configuration management", "priority": "high"},
{"id": "A.8.10", "name": "Information deletion", "priority": "high"},
{"id": "A.8.11", "name": "Data masking", "priority": "medium"},
{"id": "A.8.12", "name": "Data leakage prevention", "priority": "high"},
{"id": "A.8.13", "name": "Information backup", "priority": "critical"},
{"id": "A.8.14", "name": "Redundancy of information processing", "priority": "high"},
{"id": "A.8.15", "name": "Logging", "priority": "critical"},
{"id": "A.8.16", "name": "Monitoring activities", "priority": "high"},
{"id": "A.8.17", "name": "Clock synchronization", "priority": "medium"},
{"id": "A.8.18", "name": "Use of privileged utility programs", "priority": "high"},
{"id": "A.8.19", "name": "Installation of software", "priority": "high"},
{"id": "A.8.20", "name": "Networks security", "priority": "critical"},
{"id": "A.8.21", "name": "Security of network services", "priority": "high"},
{"id": "A.8.22", "name": "Segregation of networks", "priority": "high"},
{"id": "A.8.23", "name": "Web filtering", "priority": "medium"},
{"id": "A.8.24", "name": "Use of cryptography", "priority": "critical"},
{"id": "A.8.25", "name": "Secure development lifecycle", "priority": "high"},
{"id": "A.8.26", "name": "Application security requirements", "priority": "high"},
{"id": "A.8.27", "name": "Secure system architecture", "priority": "high"},
{"id": "A.8.28", "name": "Secure coding", "priority": "high"},
]
},
}
# Remediation recommendations by control
REMEDIATION_GUIDANCE = {
"A.5.1": "Develop and publish information security policy signed by management",
"A.5.2": "Define RACI matrix for security roles; appoint Information Security Manager",
"A.5.9": "Create asset inventory with owners and classification",
"A.6.3": "Implement annual security awareness training program",
"A.6.7": "Establish remote working policy with technical controls",
"A.8.2": "Implement privileged access management (PAM) solution",
"A.8.5": "Deploy MFA for all user and admin accounts",
"A.8.7": "Deploy endpoint protection on all devices with central management",
"A.8.8": "Implement vulnerability scanning with 30-day remediation SLA",
"A.8.13": "Configure automated backups with encryption and offsite storage",
"A.8.15": "Deploy SIEM with log retention per compliance requirements",
"A.8.20": "Implement firewall, IDS/IPS, and network monitoring",
"A.8.24": "Enforce TLS 1.3 for transit, AES-256 for data at rest",
}
def get_control_status(control_id: str, controls_data: Optional[Dict] = None) -> str:
"""Get implementation status for a control."""
if controls_data and control_id in controls_data:
return controls_data[control_id]
# Default: simulate partial implementation
import random
random.seed(hash(control_id))
statuses = ["implemented", "implemented", "partial", "partial", "not_implemented"]
return random.choice(statuses)
def load_controls_from_csv(filepath: str) -> Dict[str, str]:
"""Load control status from CSV file."""
controls = {}
try:
with open(filepath, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
control_id = row.get("control_id", row.get("id", ""))
status = row.get("status", "not_implemented").lower()
if control_id:
controls[control_id] = status
except FileNotFoundError:
print(f"Error: Controls file not found: {filepath}", file=sys.stderr)
sys.exit(1)
return controls
def check_compliance(
standard: str,
controls_data: Optional[Dict] = None,
domains: Optional[List[str]] = None
) -> Dict[str, Any]:
"""Check compliance against standard controls."""
if standard not in ["iso27001", "iso27002"]:
print(f"Error: Unsupported standard: {standard}", file=sys.stderr)
sys.exit(1)
results = {
"standard": standard,
"timestamp": datetime.now().isoformat(),
"domains": {},
"summary": {
"total_controls": 0,
"implemented": 0,
"partial": 0,
"not_implemented": 0,
},
"findings": [],
}
for domain_key, domain_data in ISO27001_CONTROLS.items():
if domains and domain_key not in domains:
continue
domain_results = {
"name": domain_data["name"],
"controls": [],
"implemented": 0,
"partial": 0,
"not_implemented": 0,
}
for control in domain_data["controls"]:
status = get_control_status(control["id"], controls_data)
control_result = {
"id": control["id"],
"name": control["name"],
"priority": control["priority"],
"status": status,
}
domain_results["controls"].append(control_result)
results["summary"]["total_controls"] += 1
if status == "implemented":
domain_results["implemented"] += 1
results["summary"]["implemented"] += 1
elif status == "partial":
domain_results["partial"] += 1
results["summary"]["partial"] += 1
else:
domain_results["not_implemented"] += 1
results["summary"]["not_implemented"] += 1
# Add to findings if high priority
if control["priority"] in ["critical", "high"]:
results["findings"].append({
"control_id": control["id"],
"control_name": control["name"],
"priority": control["priority"],
"status": status,
"remediation": REMEDIATION_GUIDANCE.get(
control["id"],
"Implement control per ISO 27001 requirements"
),
})
results["domains"][domain_key] = domain_results
# Calculate compliance percentage
total = results["summary"]["total_controls"]
implemented = results["summary"]["implemented"]
partial = results["summary"]["partial"]
results["summary"]["compliance_percentage"] = round(
((implemented + partial * 0.5) / total) * 100, 1
) if total > 0 else 0
return results
def generate_gap_analysis(results: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Generate gap analysis with prioritized recommendations."""
gaps = []
for finding in results["findings"]:
gap = {
"control_id": finding["control_id"],
"control_name": finding["control_name"],
"current_status": finding["status"],
"priority": finding["priority"],
"remediation": finding["remediation"],
"effort": "medium" if finding["priority"] == "high" else "high",
"timeline": "30 days" if finding["priority"] == "critical" else "90 days",
}
gaps.append(gap)
# Sort by priority
priority_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
gaps.sort(key=lambda x: priority_order.get(x["priority"], 99))
return gaps
def format_output(
results: Dict[str, Any],
gap_analysis: bool,
output_format: str
) -> str:
"""Format compliance results for output."""
if output_format == "json":
if gap_analysis:
results["gap_analysis"] = generate_gap_analysis(results)
return json.dumps(results, indent=2)
# Markdown format
lines = [
f"# {results['standard'].upper()} Compliance Report",
f"",
f"**Generated:** {results['timestamp']}",
f"",
f"## Summary",
f"",
f"| Metric | Value |",
f"|--------|-------|",
f"| Total Controls | {results['summary']['total_controls']} |",
f"| Implemented | {results['summary']['implemented']} |",
f"| Partial | {results['summary']['partial']} |",
f"| Not Implemented | {results['summary']['not_implemented']} |",
f"| **Compliance** | **{results['summary']['compliance_percentage']}%** |",
f"",
]
# Domain breakdown
lines.extend([
f"## Compliance by Domain",
f"",
f"| Domain | Implemented | Partial | Not Impl | Score |",
f"|--------|-------------|---------|----------|-------|",
])
for domain_key, domain_data in results["domains"].items():
total = len(domain_data["controls"])
score = round(
((domain_data["implemented"] + domain_data["partial"] * 0.5) / total) * 100
) if total > 0 else 0
lines.append(
f"| {domain_data['name']} | {domain_data['implemented']} | "
f"{domain_data['partial']} | {domain_data['not_implemented']} | {score}% |"
)
# Findings
if results["findings"]:
lines.extend([
f"",
f"## Priority Findings",
f"",
f"| Control | Name | Priority | Status |",
f"|---------|------|----------|--------|",
])
for finding in results["findings"][:15]: # Top 15
lines.append(
f"| {finding['control_id']} | {finding['control_name']} | "
f"{finding['priority'].capitalize()} | {finding['status'].replace('_', ' ').capitalize()} |"
)
# Gap analysis
if gap_analysis:
gaps = generate_gap_analysis(results)
lines.extend([
f"",
f"## Gap Analysis & Remediation",
f"",
])
for gap in gaps[:10]: # Top 10 gaps
lines.extend([
f"### {gap['control_id']}: {gap['control_name']}",
f"",
f"- **Priority:** {gap['priority'].capitalize()}",
f"- **Current Status:** {gap['current_status'].replace('_', ' ').capitalize()}",
f"- **Remediation:** {gap['remediation']}",
f"- **Timeline:** {gap['timeline']}",
f"",
])
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="ISO 27001/27002 Compliance Checker"
)
parser.add_argument(
"--standard", "-s",
required=True,
choices=["iso27001", "iso27002", "hipaa"],
help="Compliance standard to check"
)
parser.add_argument(
"--controls-file", "-c",
help="CSV file with current control implementation status"
)
parser.add_argument(
"--gap-analysis", "-g",
action="store_true",
help="Include gap analysis with remediation recommendations"
)
parser.add_argument(
"--domains", "-d",
help="Comma-separated list of domains to check (e.g., organizational,technological)"
)
parser.add_argument(
"--output", "-o",
help="Output file path (default: stdout)"
)
parser.add_argument(
"--format", "-f",
choices=["json", "markdown"],
default="markdown",
help="Output format (default: markdown)"
)
args = parser.parse_args()
# Load control status if provided
controls_data = None
if args.controls_file:
controls_data = load_controls_from_csv(args.controls_file)
# Parse domains
domains = None
if args.domains:
domains = [d.strip().lower().replace("-", "_") for d in args.domains.split(",")]
# Check compliance
results = check_compliance(args.standard, controls_data, domains)
# Format output
output = format_output(results, args.gap_analysis, args.format)
# Write output
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
f.write(output)
print(f"Report saved to: {args.output}", file=sys.stderr)
else:
print(output)
if __name__ == "__main__":
main()
FILE:scripts/risk_assessment.py
#!/usr/bin/env python3
"""
Security Risk Assessment Tool
Automated risk assessment following ISO 27001 Clause 6.1.2 methodology.
Identifies assets, threats, vulnerabilities, and calculates risk scores.
Usage:
python risk_assessment.py --scope "system-name" --output risks.json
python risk_assessment.py --assets assets.csv --template healthcare
"""
import argparse
import csv
import json
import sys
from datetime import datetime
from typing import Dict, List, Any, Optional
# Threat catalogs by template
THREAT_CATALOGS = {
"general": [
{"id": "T01", "name": "Unauthorized access", "category": "Access", "likelihood": 4},
{"id": "T02", "name": "Data breach", "category": "Confidentiality", "likelihood": 3},
{"id": "T03", "name": "Malware infection", "category": "Integrity", "likelihood": 4},
{"id": "T04", "name": "Phishing attack", "category": "Social Engineering", "likelihood": 5},
{"id": "T05", "name": "Denial of service", "category": "Availability", "likelihood": 3},
{"id": "T06", "name": "Insider threat", "category": "Personnel", "likelihood": 2},
{"id": "T07", "name": "Physical theft", "category": "Physical", "likelihood": 2},
{"id": "T08", "name": "System misconfiguration", "category": "Technical", "likelihood": 4},
{"id": "T09", "name": "Third-party compromise", "category": "Supply Chain", "likelihood": 3},
{"id": "T10", "name": "Natural disaster", "category": "Environmental", "likelihood": 1},
],
"healthcare": [
{"id": "T01", "name": "Patient data breach", "category": "Confidentiality", "likelihood": 4},
{"id": "T02", "name": "Ransomware attack", "category": "Availability", "likelihood": 4},
{"id": "T03", "name": "Medical device tampering", "category": "Integrity", "likelihood": 3},
{"id": "T04", "name": "EHR unauthorized access", "category": "Access", "likelihood": 4},
{"id": "T05", "name": "HIPAA violation", "category": "Compliance", "likelihood": 3},
{"id": "T06", "name": "Clinical data corruption", "category": "Integrity", "likelihood": 2},
{"id": "T07", "name": "Telemedicine interception", "category": "Confidentiality", "likelihood": 3},
{"id": "T08", "name": "Credential theft", "category": "Access", "likelihood": 5},
{"id": "T09", "name": "Third-party vendor breach", "category": "Supply Chain", "likelihood": 3},
{"id": "T10", "name": "Insider data theft", "category": "Personnel", "likelihood": 2},
],
"cloud": [
{"id": "T01", "name": "Cloud misconfiguration", "category": "Technical", "likelihood": 5},
{"id": "T02", "name": "API vulnerability exploit", "category": "Application", "likelihood": 4},
{"id": "T03", "name": "Account hijacking", "category": "Access", "likelihood": 4},
{"id": "T04", "name": "Data exfiltration", "category": "Confidentiality", "likelihood": 3},
{"id": "T05", "name": "Shared tenancy attack", "category": "Infrastructure", "likelihood": 2},
{"id": "T06", "name": "Service outage", "category": "Availability", "likelihood": 3},
{"id": "T07", "name": "Compliance violation", "category": "Compliance", "likelihood": 3},
{"id": "T08", "name": "Shadow IT exposure", "category": "Governance", "likelihood": 4},
{"id": "T09", "name": "Encryption key exposure", "category": "Cryptography", "likelihood": 2},
{"id": "T10", "name": "CSP vendor lock-in", "category": "Strategic", "likelihood": 3},
],
}
# Vulnerability patterns
VULNERABILITY_PATTERNS = {
"access": ["No MFA", "Weak passwords", "Excessive privileges", "Shared accounts"],
"technical": ["Unpatched systems", "Weak encryption", "Missing logging", "Open ports"],
"process": ["No incident response", "Missing backups", "No change control", "Lack of monitoring"],
"people": ["Untrained staff", "No security awareness", "Social engineering susceptibility"],
}
# Asset classification criteria
CLASSIFICATION_CRITERIA = {
"critical": {"description": "Business-critical, severe impact if compromised", "impact": 5},
"high": {"description": "Important assets, significant impact", "impact": 4},
"medium": {"description": "Standard business assets, moderate impact", "impact": 3},
"low": {"description": "Limited business value, minor impact", "impact": 2},
"minimal": {"description": "Public or non-sensitive, negligible impact", "impact": 1},
}
# Risk treatment options
TREATMENT_OPTIONS = {
"critical": "Immediate mitigation required - implement controls within 7 days",
"high": "Priority mitigation - implement controls within 30 days",
"medium": "Planned mitigation - implement controls within 90 days",
"low": "Accept risk with monitoring or implement low-cost controls",
"minimal": "Accept risk - document acceptance decision",
}
def calculate_risk_score(likelihood: int, impact: int) -> int:
"""Calculate risk score as likelihood × impact."""
return likelihood * impact
def get_risk_level(score: int) -> str:
"""Determine risk level from score."""
if score >= 20:
return "critical"
elif score >= 15:
return "high"
elif score >= 10:
return "medium"
elif score >= 5:
return "low"
return "minimal"
def load_assets_from_csv(filepath: str) -> List[Dict[str, Any]]:
"""Load asset inventory from CSV file."""
assets = []
try:
with open(filepath, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
asset = {
"id": row.get("id", f"A{len(assets)+1:03d}"),
"name": row.get("name", "Unknown"),
"type": row.get("type", "Information"),
"owner": row.get("owner", "Unassigned"),
"classification": row.get("classification", "medium").lower(),
}
assets.append(asset)
except FileNotFoundError:
print(f"Error: Asset file not found: {filepath}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error reading asset file: {e}", file=sys.stderr)
sys.exit(1)
return assets
def generate_sample_assets(scope: str, template: str) -> List[Dict[str, Any]]:
"""Generate sample asset inventory based on scope and template."""
base_assets = []
if template == "healthcare":
base_assets = [
{"id": "A001", "name": "Patient Database", "type": "Information", "owner": "DBA Team", "classification": "critical"},
{"id": "A002", "name": "EHR Application", "type": "Software", "owner": "App Team", "classification": "critical"},
{"id": "A003", "name": "Medical Imaging System", "type": "Software", "owner": "Radiology", "classification": "high"},
{"id": "A004", "name": "Database Servers", "type": "Hardware", "owner": "Infrastructure", "classification": "high"},
{"id": "A005", "name": "Admin Credentials", "type": "Access", "owner": "Security", "classification": "critical"},
{"id": "A006", "name": "Backup Systems", "type": "Service", "owner": "IT Ops", "classification": "high"},
{"id": "A007", "name": "Network Infrastructure", "type": "Hardware", "owner": "Network Team", "classification": "high"},
{"id": "A008", "name": "API Gateway", "type": "Software", "owner": "Platform Team", "classification": "high"},
]
elif template == "cloud":
base_assets = [
{"id": "A001", "name": "Cloud Storage Buckets", "type": "Service", "owner": "Platform", "classification": "high"},
{"id": "A002", "name": "Container Registry", "type": "Service", "owner": "DevOps", "classification": "high"},
{"id": "A003", "name": "API Services", "type": "Software", "owner": "Engineering", "classification": "critical"},
{"id": "A004", "name": "Database Instances", "type": "Service", "owner": "DBA Team", "classification": "critical"},
{"id": "A005", "name": "IAM Configuration", "type": "Access", "owner": "Security", "classification": "critical"},
{"id": "A006", "name": "Secrets Manager", "type": "Service", "owner": "Security", "classification": "critical"},
{"id": "A007", "name": "Load Balancers", "type": "Infrastructure", "owner": "Platform", "classification": "high"},
{"id": "A008", "name": "Monitoring Systems", "type": "Service", "owner": "SRE", "classification": "medium"},
]
else: # general
base_assets = [
{"id": "A001", "name": "Corporate Data", "type": "Information", "owner": "Data Team", "classification": "high"},
{"id": "A002", "name": "Business Applications", "type": "Software", "owner": "IT", "classification": "high"},
{"id": "A003", "name": "Server Infrastructure", "type": "Hardware", "owner": "Infrastructure", "classification": "high"},
{"id": "A004", "name": "User Credentials", "type": "Access", "owner": "Security", "classification": "critical"},
{"id": "A005", "name": "Email System", "type": "Service", "owner": "IT", "classification": "medium"},
{"id": "A006", "name": "File Servers", "type": "Hardware", "owner": "Infrastructure", "classification": "medium"},
{"id": "A007", "name": "Network Equipment", "type": "Hardware", "owner": "Network", "classification": "high"},
{"id": "A008", "name": "Backup Infrastructure", "type": "Service", "owner": "IT Ops", "classification": "high"},
]
# Tag assets with scope
for asset in base_assets:
asset["scope"] = scope
return base_assets
def assess_risks(
assets: List[Dict[str, Any]],
template: str
) -> List[Dict[str, Any]]:
"""Perform risk assessment on assets."""
threats = THREAT_CATALOGS.get(template, THREAT_CATALOGS["general"])
risks = []
risk_id = 1
for asset in assets:
classification = asset.get("classification", "medium")
impact = CLASSIFICATION_CRITERIA.get(classification, {}).get("impact", 3)
# Map relevant threats to asset
relevant_threats = threats[:5] # Top 5 threats for each asset
for threat in relevant_threats:
likelihood = threat["likelihood"]
score = calculate_risk_score(likelihood, impact)
level = get_risk_level(score)
# Identify potential vulnerabilities
vuln_category = threat["category"].lower()
vulns = VULNERABILITY_PATTERNS.get("technical", ["Unknown vulnerability"])
if "access" in vuln_category:
vulns = VULNERABILITY_PATTERNS["access"]
elif "personnel" in vuln_category or "social" in vuln_category:
vulns = VULNERABILITY_PATTERNS["people"]
risk = {
"id": f"R{risk_id:03d}",
"asset_id": asset["id"],
"asset_name": asset["name"],
"threat_id": threat["id"],
"threat_name": threat["name"],
"threat_category": threat["category"],
"vulnerability": vulns[0] if vulns else "Unidentified",
"likelihood": likelihood,
"impact": impact,
"score": score,
"level": level,
"treatment": TREATMENT_OPTIONS.get(level, "Review required"),
}
risks.append(risk)
risk_id += 1
# Sort by risk score descending
risks.sort(key=lambda x: x["score"], reverse=True)
return risks
def calculate_residual_risk(risk: Dict[str, Any], control_effectiveness: float = 0.7) -> Dict[str, Any]:
"""Calculate residual risk after applying controls."""
residual_likelihood = max(1, int(risk["likelihood"] * (1 - control_effectiveness)))
residual_score = calculate_risk_score(residual_likelihood, risk["impact"])
return {
"risk_id": risk["id"],
"inherent_score": risk["score"],
"control_effectiveness": control_effectiveness,
"residual_likelihood": residual_likelihood,
"residual_score": residual_score,
"residual_level": get_risk_level(residual_score),
}
def generate_report(
scope: str,
template: str,
assets: List[Dict[str, Any]],
risks: List[Dict[str, Any]],
output_format: str
) -> str:
"""Generate risk assessment report."""
timestamp = datetime.now().isoformat()
# Calculate summary statistics
risk_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "minimal": 0}
for risk in risks:
risk_counts[risk["level"]] += 1
report_data = {
"metadata": {
"scope": scope,
"template": template,
"timestamp": timestamp,
"methodology": "ISO 27001 Clause 6.1.2",
},
"summary": {
"total_assets": len(assets),
"total_risks": len(risks),
"risk_distribution": risk_counts,
"critical_risks": risk_counts["critical"],
"high_risks": risk_counts["high"],
},
"assets": assets,
"risks": risks,
"residual_risks": [calculate_residual_risk(r) for r in risks[:10]], # Top 10
}
if output_format == "json":
return json.dumps(report_data, indent=2)
elif output_format == "csv":
lines = ["risk_id,asset,threat,likelihood,impact,score,level,treatment"]
for risk in risks:
lines.append(
f"{risk['id']},{risk['asset_name']},{risk['threat_name']},"
f"{risk['likelihood']},{risk['impact']},{risk['score']},"
f"{risk['level']},{risk['treatment']}"
)
return "\n".join(lines)
else: # markdown
lines = [
f"# Security Risk Assessment Report",
f"",
f"**Scope:** {scope}",
f"**Template:** {template}",
f"**Date:** {timestamp}",
f"**Methodology:** ISO 27001 Clause 6.1.2",
f"",
f"## Summary",
f"",
f"| Metric | Value |",
f"|--------|-------|",
f"| Total Assets | {len(assets)} |",
f"| Total Risks | {len(risks)} |",
f"| Critical Risks | {risk_counts['critical']} |",
f"| High Risks | {risk_counts['high']} |",
f"| Medium Risks | {risk_counts['medium']} |",
f"",
f"## Asset Inventory",
f"",
f"| ID | Asset | Type | Owner | Classification |",
f"|----|-------|------|-------|----------------|",
]
for asset in assets:
lines.append(
f"| {asset['id']} | {asset['name']} | {asset['type']} | "
f"{asset['owner']} | {asset['classification'].capitalize()} |"
)
lines.extend([
f"",
f"## Risk Register",
f"",
f"| Risk ID | Asset | Threat | L | I | Score | Level |",
f"|---------|-------|--------|---|---|-------|-------|",
])
for risk in risks[:20]: # Top 20 risks
lines.append(
f"| {risk['id']} | {risk['asset_name']} | {risk['threat_name']} | "
f"{risk['likelihood']} | {risk['impact']} | {risk['score']} | "
f"{risk['level'].capitalize()} |"
)
lines.extend([
f"",
f"## Treatment Recommendations",
f"",
])
for level, treatment in TREATMENT_OPTIONS.items():
count = risk_counts[level]
if count > 0:
lines.append(f"**{level.capitalize()} ({count} risks):** {treatment}")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Security Risk Assessment Tool - ISO 27001 Clause 6.1.2"
)
parser.add_argument(
"--scope", "-s",
required=True,
help="System or area to assess"
)
parser.add_argument(
"--template", "-t",
choices=["general", "healthcare", "cloud"],
default="general",
help="Assessment template (default: general)"
)
parser.add_argument(
"--assets", "-a",
help="CSV file with asset inventory"
)
parser.add_argument(
"--output", "-o",
help="Output file path (default: stdout)"
)
parser.add_argument(
"--format", "-f",
choices=["json", "csv", "markdown"],
default="markdown",
help="Output format (default: markdown)"
)
args = parser.parse_args()
# Load or generate assets
if args.assets:
assets = load_assets_from_csv(args.assets)
else:
assets = generate_sample_assets(args.scope, args.template)
# Perform risk assessment
risks = assess_risks(assets, args.template)
# Generate report
report = generate_report(
args.scope,
args.template,
assets,
risks,
args.format
)
# Output
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
f.write(report)
print(f"Report saved to: {args.output}", file=sys.stderr)
else:
print(report)
if __name__ == "__main__":
main()
Xây và duy trì một câu chuyện công ty nhất quán cho nhân viên, nhà đầu tư, khách hàng, ứng viên và đối tác, phát hiện mâu thuẫn.
--- name: "internal-narrative" description: "Build and maintain one coherent company story across all audiences — employees, investors, customers, candidates, and partners. Detects narrative contradictions and ensures the same truth is framed for each audience's needs. Use when preparing investor updates, all-hands presentations, board communications, recruiting narratives, crisis communications, or when user mentions company narrative, messaging consistency, storytelling, all-hands, investor update, or crisis communication." license: MIT metadata: version: 1.0.0 author: Alireza Rezvani category: c-level domain: narrative-strategy updated: 2026-03-05 frameworks: narrative-frameworks, all-hands-template --- # Internal Narrative Builder One company. Many audiences. Same truth — different lenses. Narrative inconsistency is trust erosion. This skill builds and maintains coherent communication across every stakeholder group. ## Keywords narrative, company story, internal communication, investor update, all-hands, board communication, crisis communication, messaging, storytelling, narrative consistency, audience translation, founder narrative, employee communication, candidate narrative, partner communication ## Core Principle **The same fact lands differently depending on who hears it and what they need.** "We're shifting resources from Product A to Product B" means: - To employees: "Is my job safe? Why are we abandoning what I built?" - To investors: "Smart capital allocation — they're doubling down on the winner" - To customers of Product A: "Are they abandoning us?" - To candidates: "Exciting new focus — are they decisive?" Same fact. Four different narratives needed. The skill is maintaining truth while serving each audience's actual question. --- ## Framework ### Step 1: Build the Core Narrative One paragraph that every other communication derives from. This is the source of truth. **Core narrative template:** > [Company name] exists to [mission — present tense, specific]. We're building [what you're building] because [the problem you're solving]. Our approach is [your unique way of doing this]. We're at [honest description of current state] and heading toward [where you're going in concrete terms]. **Good core narrative (example):** > Acme Health exists to reduce preventable falls in elderly care using smartphone-based mobility analysis. We're building an AI diagnostic tool for care teams because current fall risk assessments are subjective, infrequent, and often wrong. Our approach — using the phone's camera during a 10-second walking test — means no new hardware, no specialist required. We have 80 care facilities in DACH paying us €800K ARR, and we're heading to €3M ARR by demonstrating clinical value at scale before our Series B. **Bad core narrative:** > Acme Health is an innovative AI company revolutionizing elderly care through cutting-edge technology that empowers care providers and improves patient outcomes across the continuum of care. The good version is usable. The bad version says nothing. --- ### Step 2: Audience Translation Matrix Take the core narrative and translate it for each audience. Same truth, different frame. | Fact | Employees need to hear | Investors need to hear | Customers need to hear | Candidates need to hear | |------|----------------------|----------------------|----------------------|------------------------| | We have 80 customers | "We've proven the model — your work matters" | "Product-market fit signal, capital efficient" | "80 care facilities trust us" | "Traction you'd be joining" | | We pivoted from hardware | "We were honest enough to change course" | "Capital-efficient pivot to better unit economics" | "We found a faster, simpler way to serve you" | "We make decisions based on evidence, not ego" | | We missed Q2 revenue | "Here's why, here's the plan, here's what you can do" | "Revenue mix shifted — trailing indicator improving" | [Usually don't tell customers revenue misses] | [Usually not shared externally] | | We're hiring fast | "The team is growing — your network matters" | "Headcount plan aligned to growth" | [Not relevant unless it affects service] | "This is a rocket ship moment" | **Rules:** - Never contradict yourself across audiences. Different framing ≠ different facts. - "We told investors growth, told employees efficiency" is a contradiction. Audit for this. - Investors and employees see each other. Board members talk to your team. Candidates google you. --- ### Step 3: Contradiction Detection Before any major communication, run the contradiction check: **Question 1:** What did we tell investors last month about [topic]? **Question 2:** What did we tell employees about the same topic? **Question 3:** Are these consistent? If not — which version is true? **Common contradictions:** - "Efficient growth" to investors + "we're hiring aggressively" to candidates - "Strong pipeline" to investors + "sales is struggling" at all-hands - "Customer-first" in culture + recent decisions that clearly prioritized revenue over customer need **When you catch a contradiction:** Fix the less accurate version, then communicate the correction explicitly. "Last month I said X. After more reflection, X is not quite right. Here's the clearer version." Correcting yourself before someone else catches it builds more trust than getting caught. --- ### Step 4: Audience-Specific Communication Cadence | Audience | Format | Frequency | Owner | |----------|--------|-----------|-------| | Employees | All-hands | Monthly | CEO | | Employees | Team updates | Weekly | Team leads | | Investors | Written update | Monthly | CEO + CFO | | Board | Board meeting + memo | Quarterly | CEO | | Customers | Product updates | Per release | CPO / CS | | Candidates | Careers page + interview narrative | Ongoing | CHRO + Founders | | Partners | Quarterly business review | Quarterly | BD Lead | --- ### Step 5: All-Hands Structure and Cadence See `templates/all-hands-template.md` for the full template. **Principles:** - Lead with honest state of the company. No spin. - Connect company performance to individual work: "Here's how what you built contributed to this outcome." - Give people a reason to be proud of their choice to work here. - Leave time for real Q&A — not curated questions. **All-hands failure modes:** - CEO speaks for 55 of 60 minutes; Q&A is "any quick questions?" - All good news, all the time — employees know when you're not being honest - Metrics without context: "ARR grew 15%" without explaining if that's good, bad, or expected - Questions deflected: "That's a great point, we should follow up on that" → never followed up --- ### Step 6: Crisis Communication When the narrative breaks — someone leaves publicly, a product fails, a security breach, a press article. **The 4-hour rule:** If something is public or about to be, communicate internally within 4 hours. Employees should never learn about company news from Twitter. **Crisis communication sequence:** **Hour 0–4 (internal first):** 1. CEO or relevant leader sends an internal message 2. Acknowledge what happened factually 3. State what you know and what you don't know yet 4. Tell people what you're doing about it 5. Tell people what they should do if they're asked about it **Hour 4–24 (external if needed):** 1. External statement (press, social) only if the event is public 2. Consistent with the internal message — same facts, audience-appropriate framing 3. Legal review if any claims or liability involved **What not to do in a crisis:** - Silence: letting rumors fill the vacuum - Spin: people can detect it and it destroys trust - "No comment": says "we have something to hide" - Blaming: even if someone else caused the problem, your audience only cares what you're doing about it **Template for crisis internal communication:** > "Here's what happened: [factual description]. Here's what we know right now: [known facts]. Here's what we don't know yet: [honest uncertainty]. Here's what we're doing: [specific actions]. Here's what you should do if you're asked about this: [specific guidance]. I'll update you by [specific time] with more information." --- ## Narrative Consistency Checklist Run before any major external communication: - [ ] Is this consistent with what we told investors last month? - [ ] Is this consistent with what we told employees at the last all-hands? - [ ] Does this contradict anything on our website, careers page, or press releases? - [ ] If an employee read this external communication, would they recognize the company being described? - [ ] If an investor read our internal all-hands deck, would they find anything inconsistent? - [ ] Have we been accurate about our current state, or are we projecting an aspiration? --- ## Key Questions for Narrative - "Could a new employee explain to a friend why our company exists? What would they say?" - "What do we tell investors about our strategy? What do we tell employees? Are these the same?" - "If a journalist asked our team members to describe the company independently, what would they say?" - "When did we last update our 'why we exist' story? Is it still true?" - "What's the hardest question we'd get from each audience? Do we have an honest answer?" ## Red Flags - Different departments describe the company mission differently - Investor narrative emphasizes growth; employee narrative emphasizes stability (or vice versa) - All-hands presentations are mostly slides, mostly one-way - Q&A questions are screened or deflected - Bad news reaches employees through Slack rumors before leadership communication - Careers page describes a culture that employees don't recognize ## Integration with Other C-Suite Roles | When... | Work with... | To... | |---------|-------------|-------| | Investor update prep | CFO | Align financial narrative with company narrative | | Reorg or leadership change | CHRO + CEO | Sequence: employees first, then external | | Product pivot | CPO | Align customer communication with investor story | | Culture change | Culture Architect | Ensure internal story is consistent with external employer brand | | M&A or partnership | CEO + COO | Control information flow, prevent narrative leaks | | Crisis | All C-suite | Single voice, consistent story, internal first | ## Detailed References - `references/narrative-frameworks.md` — Storytelling structures, founder narrative, bad news delivery, all-hands templates - `templates/all-hands-template.md` — All-hands presentation template FILE:references/narrative-frameworks.md # Narrative Frameworks Reference frameworks for building compelling, consistent business narratives. --- ## 1. Storytelling Structure for Business ### The SCR Framework (Situation, Complication, Resolution) Barbara Minto's Pyramid Principle adapted for business narrative. Works for any audience. **Situation:** The established facts everyone agrees on. **Complication:** What changed, what problem arose, what makes the situation untenable. **Resolution:** What you're doing about it, and why this solution works. **Example — Investor update:** > **Situation:** We entered Q2 with €650K ARR and a target of €800K. Our DACH pipeline was strong at 3x coverage. > > **Complication:** Two large deals (€90K combined ARR) that were expected to close in May pushed to Q3 due to procurement delays on the customer side. We ended Q2 at €710K — below target but within the range we'd flag as manageable. > > **Resolution:** Both deals are now signed with June start dates. We're entering Q3 at €800K ARR. We've added a new procurement risk flag to our pipeline methodology to catch this pattern earlier. **Why it works:** It respects the audience's intelligence, acknowledges the problem directly, and frames your response before they can object. --- ### The Problem-Solution-Evidence Structure Best for pitches, product announcements, and strategy communications. 1. **The world as it is:** What's the current reality? 2. **What's broken about it:** Why is the status quo painful or inefficient? 3. **What we're doing:** Your specific solution 4. **Why it works:** Evidence, mechanism, or proof 5. **What happens next:** Call to action or forward look **Example — All-hands strategy communication:** > The world as it is: We have 80 customers in DACH. Churn is 8% annually. That means we're losing 6–7 customers a year just to stay flat. > > What's broken: Our onboarding takes 6 weeks. By week 4, customers haven't seen value yet and they're questioning the decision. We've traced 60% of churn to customers who never completed onboarding. > > What we're doing: We're redesigning onboarding to show the first meaningful mobility report within 48 hours of account activation. > > Why it will work: We ran this with 5 pilot customers in Q2. Time-to-first-value dropped from 4 weeks to 2 days. 4 of 5 expanded their contract within 60 days. > > What happens next: Engineering ships the new onboarding flow by August 15. CS is retrained by August 22. We'll run the new flow with all new customers from September 1 and report back at the October all-hands. --- ## 2. The Founder's Narrative The founder's personal story is one of the most underutilized assets in a startup. Used well, it anchors the company's mission and creates genuine connection. ### The Founder Story Structure **Origin:** What led you to this problem? (Ideally personal — you experienced it, someone you loved experienced it, you couldn't stop thinking about why nobody was solving it) **Insight:** What did you see that others didn't? (Your unique perspective or unfair advantage) **Decision:** The moment you committed. (Specific, not aspirational — "I left my job on March 14" not "I decided to pursue my passion") **What you've learned:** 2–3 honest observations that shaped your approach. (Including what you got wrong) **Where you're going:** Connection from your personal why to where the company is heading. **Example (condensed):** > My mother had a fall in 2018 that broke her hip. She spent 3 months in rehabilitation. The terrifying part: nobody saw it coming. Her doctor had assessed her fall risk 4 months earlier — using a paper questionnaire. I spent two years talking to geriatricians trying to understand why this assessment was still done by hand, on paper, in 2018. The answer: nobody had made it easy enough for a non-specialist to do it digitally. That's what we're building. **Why it matters:** Investors, candidates, and customers all respond to a founder who started from a real problem rather than a market opportunity. The narrative makes you memorable and makes the mission credible. --- ## 3. How to Deliver Bad News Across Audiences ### Universal principles 1. **Internal first.** Always. Every time. No exceptions. 2. **Direct, not hedged.** "We missed our Q2 target by 12%" beats "Q2 performance came in below our expectations." 3. **Own it before explaining it.** Context comes after acknowledgment, not before. 4. **State what you're doing.** Bad news without a response plan creates panic. 5. **Give a timeline.** "We'll know more by [date]" is better than open-ended uncertainty. ### Delivering bad news to employees **Format:** Synchronous (all-hands or team meeting), followed by written summary. **What to say:** - What happened (factual, no spin) - What it means for the company - What it means for them specifically (will roles change? Will comp change?) - What you're doing about it - When you'll have more information **What not to say:** - "I can't share the details" (share everything you legally can) - "This is actually good news because..." (if it's bad news, don't reframe it before acknowledging it) - "We saw this coming" (if you did, why didn't you tell them?) **Example — Missed fundraise:** > "I have to share news that's disappointing. We went out to raise a Series A in Q1, and we didn't close the round. We had term sheets that fell through when the market conditions shifted in April. We're not in crisis — we have 12 months of runway — but we need to recalibrate. Here's what that means concretely: we're pausing 3 open headcount. Everyone currently on the team keeps their role. We're going back to market in Q4 with stronger metrics. I'll share our updated financial model with everyone by Friday and answer every question you have." --- ### Delivering bad news to investors **Format:** Written update (monthly update format) + proactive call if material. **What to say:** - Headline the bad news in the first paragraph (don't bury it) - Context: what changed and what didn't change - What you're doing about it - What you need from them (if anything) **What investors hate:** - Finding out from someone other than you - Bad news wrapped in so much context they have to work to find it - "We're watching it closely" without specific action - Consistent over-optimism followed by consistent misses **Example — Investor update paragraph:** > "Revenue miss: We ended Q2 at €710K ARR vs. a target of €800K. Two deals totaling €90K pushed to Q3 due to customer procurement delays (not product or relationship issues — both have since signed). We've adjusted our sales process to flag procurement risk earlier. Q3 is starting at €800K with those deals live." --- ### Delivering bad news to customers **Scope:** Only share bad news that affects them. Don't share internal struggles that aren't relevant to their experience. **Format:** Proactive communication from their account owner or a senior leader. **What customers need:** - What happened (that affects them) - What you're doing about it - What they should do (if anything) - Who to contact **What customers don't need:** - Your internal financial struggles - Drama about team changes - More detail than affects their use of your product **Example — Service disruption:** > "Yesterday evening we experienced a 90-minute service outage that affected your access to [feature]. We've identified the root cause (a failed database migration) and deployed a fix. Your data is intact and complete. We've implemented additional monitoring to prevent this from recurring. I'd like to schedule a brief call to answer any questions you have." --- ## 4. Narrative Consistency Checklist Use before any significant external communication. ### Pre-communication audit **Factual consistency:** - [ ] Is the ARR/revenue figure consistent with what we've shared with investors? - [ ] Is the team size consistent with what's on LinkedIn and our careers page? - [ ] Are our stated priorities consistent with our published roadmap? - [ ] Is our "stage" description consistent across all channels? (We can't be "early stage" to investors and "established leader" to customers) **Message consistency:** - [ ] Does this message conflict with anything said in the last 90 days? - [ ] If an employee read this external message, would they recognize the company? - [ ] If an investor read our internal all-hands, would they find anything that contradicts what we've told them? **Audience appropriateness:** - [ ] Have we answered the key question for this specific audience? - [ ] Have we avoided sharing information this audience doesn't need and shouldn't have? - [ ] Have we framed the message for what this audience cares about — not what we want them to care about? --- ## 5. All-Hands Presentation Templates See `templates/all-hands-template.md` for the complete slide-by-slide template. ### Monthly all-hands (30–45 min) **Structure:** 1. State of the company (10 min) — honest, metric-driven 2. Progress on quarterly rocks (5 min) — on track / off track / done 3. Team spotlight (5 min) — one team's work, why it matters 4. What's coming next 30 days (5 min) — what to expect 5. Q&A (10–15 min) — real questions, real answers ### Quarterly all-hands (60–90 min) **Structure:** 1. Last quarter results vs. targets (15 min) 2. What we learned (10 min) — honest reflection on what didn't work 3. Next quarter priorities (15 min) — company rocks, why these three 4. Strategy update (10 min) — anything changing? Why? 5. Team recognition (10 min) — specific, values-linked examples 6. Q&A (15–20 min) ### Annual all-hands (2–4 hours, often a full day) **Structure:** 1. Year in review: what we achieved (30 min) 2. What we learned — what we'd do differently (20 min) 3. State of the company: financial health, competitive position (20 min) 4. 3-year vision update (30 min) 5. Next year's strategy and priorities (30 min) 6. Department presentations: what each team is building (60 min) 7. Celebrations and recognition (20 min) 8. Q&A + social (open-ended) ### The "no-BS questions" technique At any all-hands, reserve the last 5 minutes for: "What question are you afraid to ask publicly? Submit anonymously via [link]." Read 3–5 of the hardest ones out loud and answer them honestly. This builds more trust than 45 minutes of polished presentation. FILE:templates/all-hands-template.md # All-Hands Presentation Template **Monthly format (30–45 min) | Adjust timing for quarterly/annual** --- ## Slide 1: State of the Company **Headline:** One honest sentence about where we are right now. > "We're ahead on revenue, behind on hiring, and Q3 is looking strong." **3 key metrics (vs. target):** | Metric | Target | Actual | Status | |--------|--------|--------|--------| | ARR / Revenue | | | 🟢/🟡/🔴 | | [Key growth metric] | | | | | [Key health metric] | | | | **One sentence on momentum:** Are we accelerating, steady, or facing headwinds? Be honest. --- ## Slide 2: Progress on Quarterly Rocks For each company-level rock: | Rock | Owner | Status | |------|-------|--------| | [Rock 1 description] | [Name] | ✅ Done / 🟡 On track / 🔴 At risk | | [Rock 2 description] | [Name] | | | [Rock 3 description] | [Name] | | For any 🔴 at-risk rock: one sentence on what changed and what we're doing about it. --- ## Slide 3: What We're Proud Of **One specific win from the last 30 days.** Not the metric — the story behind it. > "CS team saved the Müller Group account after a critical feature gap was flagged 72 hours before their renewal. They pulled together engineering, product, and sales in 24 hours and presented a roadmap commitment that converted a churned account into an expansion. That's what customer obsession looks like." Tie to a company value. Name the people involved. --- ## Slide 4: What We Learned / What Didn't Work **One honest thing that didn't go as planned.** > "Our Q2 product launch was delayed 3 weeks because we underestimated the testing scope for the new export feature. We shipped it, customers are using it, but we learned that our pre-launch testing checklist needs to include third-party integration validation. We've added that to the template." If it's small: 2 sentences. If it's big: more time here, less elsewhere. **Why this slide exists:** A company that only celebrates wins teaches people to hide problems. This slide teaches people that honesty is valued. --- ## Slide 5: What's Coming Next 30 Days **3 things to know about:** 1. [Upcoming release / launch / event] — [What it is and why it matters] 2. [Hiring update or org change] — [Honest current state] 3. [External event / partnership / market development] — [What we're watching] **What NOT to include:** Vague aspirations. Only things people can actually act on or prepare for. --- ## Slide 6: Q&A **Format:** Live questions preferred. Anonymous submission option always available. **CEO rules for Q&A:** - Answer the question asked, not the one you wish they'd asked - "I don't know, but I'll find out and share by [date]" > vague answer - "I can't share that yet because [reason], but I will when I can" > "no comment" - If the same question has been asked three times across all-hands, it's a communication gap — fix it **Closing line:** > "Thanks for your time. If you have a question you didn't get to ask — Slack me directly, or use the anonymous form. I read every one." --- ## Presenter Notes **Before every all-hands:** - [ ] Review last all-hands deck — anything promised that wasn't delivered? - [ ] Check: is there anything employees should have heard from us before this meeting? - [ ] Have 3–5 real Q&A answers prepared for the hardest questions you'd expect **During Q&A:** - [ ] Don't deflect hard questions — they remember - [ ] Don't over-explain — short answers signal confidence - [ ] Don't let one person dominate — "let's take that to a 1:1" is a valid response **After every all-hands:** - [ ] Send a written summary within 24 hours (key metrics, decisions, answers to top questions) - [ ] Follow up on any commitments made during Q&A within the stated timeframe
Chiến lược mở rộng thị trường quốc tế: chọn thị trường, hình thức thâm nhập, bản địa hóa, tuân thủ quy định và go-to-market theo khu vực.
--- name: "intl-expansion" description: "International market expansion strategy. Market selection, entry modes, localization, regulatory compliance, and go-to-market by region. Use when expanding to new countries, evaluating international markets, planning localization, or building regional teams." license: MIT metadata: version: 1.0.0 author: Alireza Rezvani category: c-level domain: international-strategy updated: 2026-03-05 --- # International Expansion Frameworks for expanding into new markets: selection, entry, localization, and execution. ## Keywords international expansion, market entry, localization, go-to-market, GTM, regional strategy, international markets, market selection, cross-border, global expansion ## Quick Start **Decision sequence:** Market selection → Entry mode → Regulatory assessment → Localization plan → GTM strategy → Team structure → Launch. ## Market Selection Framework ### Scoring Matrix | Factor | Weight | How to Assess | |--------|--------|---------------| | Market size (addressable) | 25% | TAM in target segment, willingness to pay | | Competitive intensity | 20% | Incumbent strength, market gaps | | Regulatory complexity | 20% | Barriers to entry, compliance cost, timeline | | Cultural distance | 15% | Language, business practices, buying behavior | | Existing traction | 10% | Inbound demand, existing customers, partnerships | | Operational complexity | 10% | Time zones, infrastructure, payment systems | ### Entry Modes | Mode | Investment | Control | Risk | Best For | |------|-----------|---------|------|----------| | **Export** (sell remotely) | Low | Low | Low | Testing demand | | **Partnership** (reseller/distributor) | Medium | Medium | Medium | Markets with strong local requirements | | **Local team** (hire in-market) | High | High | High | Strategic markets with proven demand | | **Entity** (full subsidiary) | Very high | Full | High | Major markets, regulatory requirement | | **Acquisition** | Highest | Full | Highest | Fast market entry with existing base | **Default path:** Export → Partnership → Local team → Entity (graduate as revenue justifies). ## Localization Checklist ### Product - [ ] Language (UI, documentation, support content) - [ ] Currency and pricing (local pricing, not just conversion) - [ ] Payment methods (varies wildly by market) - [ ] Date/time/number formats - [ ] Legal requirements (data residency, privacy) - [ ] Cultural adaptation (not just translation) ### Go-to-Market - [ ] Messaging adaptation (what resonates locally) - [ ] Channel strategy (channels differ by market) - [ ] Local case studies and social proof - [ ] Local partnerships and integrations - [ ] Event/conference presence - [ ] Local SEO and content ### Operations - [ ] Legal entity (if required) - [ ] Tax compliance - [ ] Employment law (if hiring locally) - [ ] Customer support (hours, language) - [ ] Banking and payments ## Key Questions - "Is there pull from the market, or are we pushing?" - "What's the cost of entry vs the 3-year revenue opportunity?" - "Can we serve this market from HQ, or do we need boots on the ground?" - "What's the regulatory timeline? Can we launch before the paperwork is done?" - "Who's winning in this market and what would it take to displace them?" ## Common Mistakes | Mistake | Why It Happens | Prevention | |---------|---------------|------------| | Entering too many markets at once | FOMO, board pressure | Max 1-2 new markets per year | | Copy-paste GTM from home market | Assuming buyers are the same | Research local buying behavior | | Underestimating regulatory cost | "We'll figure it out" | Regulatory assessment BEFORE committing | | Hiring too early | Optimism | Prove demand before hiring local team | | Wrong pricing (just converting) | Laziness | Research willingness to pay locally | ## Integration with C-Suite Roles | Role | Contribution | |------|-------------| | CEO | Market selection, strategic commitment | | CFO | Investment sizing, ROI modeling, entity structure | | CRO | Revenue targets, sales model adaptation | | CMO | Positioning, channel strategy, local brand | | CPO | Localization roadmap, feature priorities | | CTO | Infrastructure, data residency, scaling | | CHRO | Local hiring, employment law, comp | | COO | Operations setup, process adaptation | ## Resources - `references/market-entry-playbook.md` — detailed entry playbook by market type - `references/regional-guide.md` — specific considerations for key regions (EU, US, APAC, LATAM) FILE:references/market-entry-playbook.md # Market Entry Playbook Step-by-step framework for entering a new international market. ## Phase 0: Validation (4-8 weeks) Before committing resources, validate demand: ### Signal Assessment | Signal | Strength | Action | |--------|----------|--------| | Inbound inquiries from the market | Strong | Fast-track evaluation | | Existing customers using from that market | Strong | Interview them, understand needs | | Competitor succeeding there | Medium | Market exists, but competition too | | Partner referral | Medium | Validate independently | | Market research says it's big | Weak | Research ≠ demand | | Board says "we should be in X" | Weakest | Push back with data | ### Lightweight Validation 1. **Landing page test** — localized landing page with waitlist 2. **Ad spend test** — $2-5K in targeted ads, measure conversion 3. **Sales outreach** — 20 calls to potential customers in market 4. **Partner conversations** — 3-5 potential local partners 5. **Competitor analysis** — who's there, what they charge, customer reviews **Pass criteria:** At least 2 of: qualified pipeline > $50K, waitlist > 100, partner willing to co-sell. ## Phase 1: Planning (4-6 weeks) ### Market-Specific GTM | Element | Home Market | New Market | Notes | |---------|------------|------------|-------| | ICP | [your ICP] | [adapted ICP] | May be different segment | | Pricing | [home price] | [local price] | Value-based, not conversion | | Channels | [home channels] | [local channels] | Research what works locally | | Sales model | [home model] | [adapted model] | Self-serve may not work everywhere | | Support | [home support] | [local support] | Language, hours, expectations | ### Pricing Strategy by Market - **Developed markets (US, UK, DACH, Nordics):** Price for value, premium positioning - **Growth markets (Southern Europe, Eastern Europe):** 20-40% discount from core market - **Emerging markets (LATAM, SEA):** 40-60% discount or different packaging - **Enterprise everywhere:** Don't discount — add local value instead ### Regulatory Pre-Work 1. Data residency requirements (where must data live?) 2. Industry-specific regulations (healthcare, finance, education) 3. Tax obligations (VAT, withholding, nexus) 4. Employment law basics (if hiring) 5. Import/export restrictions (if applicable) 6. Timeline to compliance (weeks, months, years?) ## Phase 2: Entry (8-12 weeks) ### Minimum Viable Presence | Element | MVP | Full | When to Upgrade | |---------|-----|------|-----------------| | Legal entity | None (sell cross-border) | Local subsidiary | Revenue > $500K/year | | Team | Remote sales + support | Local office | > 5 local employees | | Product | English + key translations | Full localization | Customer feedback demands it | | Payments | International card processing | Local payment methods | Conversion drops | | Support | Home team covers (extended hours) | Local support team | Volume requires it | ### Launch Sequence 1. **Week 1-2:** Product localization (minimum viable) 2. **Week 3-4:** Local pricing and payment setup 3. **Week 5-6:** Marketing launch (content, ads, PR) 4. **Week 7-8:** Sales activation (outreach, partner launch) 5. **Week 9-12:** Iterate based on first customers ### First 10 Customers These are your foundation. Over-invest in their success: - Weekly check-ins for first 90 days - Dedicated support contact - Feedback loop to product team - Case study development - Referral program ## Phase 3: Scale (6-12 months) ### When to Invest More | Signal | Action | |--------|--------| | Pipeline > 3x capacity | Hire more sales | | Support tickets in local language > 30% | Hire local support | | Regulatory requirement for local entity | Establish subsidiary | | Revenue > $500K ARR from market | Appoint country manager | | 3+ enterprise deals require local presence | Open local office | ### Country Manager Profile First local hire matters enormously: - **Must have:** Domain expertise, local network, startup mentality - **Nice to have:** Experience with your type of product - **Red flag:** Wants to build a big team immediately - **Ideal:** Someone who can sell, support, and partner — a generalist ### Common Scaling Mistakes 1. **Hiring a country manager too early** — Before product-market fit in that market 2. **Building a full local team before proving the model** — Expensive and hard to unwind 3. **Letting the local team operate independently** — They need to integrate, not isolate 4. **Ignoring local competition** — They know the market better than you 5. **Applying home-market playbook** — What works in the US may fail in Germany ## Market Type Playbooks ### Expanding Within Europe (DACH → EU) - Regulatory: GDPR already covers you, but check industry-specific - Languages: English works for Nordics/Netherlands, but not for France/Spain/Italy - Pricing: PPP varies less within EU, but willingness to pay differs - Sales: Direct works for DACH/Nordics, partner-heavy for Southern Europe - Fastest path: UK → Nordics → Benelux → France → Spain → Italy ### Entering the US from Europe - Legal: Delaware C-Corp for investment compatibility - Sales: Everything is bigger — territories, deal sizes, expectations - Pricing: Usually 20-30% higher than Europe - Support: US customers expect fast response, US business hours - Competition: More competitors, but also more budget - Entry: Start with coast (NYC or SF), not middle America ### Entering APAC - Diversity: APAC is not one market — it's 20+ - Start: Singapore (English, business-friendly) or Australia - Japan/Korea: Need local partner, high localization bar - India: Large market, price-sensitive, relationship-driven - China: Separate strategy entirely, regulatory complexity extreme ## Measuring Success | Metric | Month 3 Target | Month 6 Target | Month 12 Target | |--------|---------------|----------------|-----------------| | Pipeline | 10x of revenue target | 5x of revenue target | 3x of revenue target | | Customers | 5-10 | 20-50 | 50-100+ | | ARR | $50-100K | $200-500K | $500K-1M | | NPS | > 30 | > 40 | > 50 | | Churn | < 5% monthly | < 3% monthly | < 2% monthly | Metrics should improve each quarter. If they flatten, something's wrong with product-market fit in that specific market. FILE:references/regional-guide.md # Regional Expansion Guide Specific considerations for key regions. Not exhaustive — these are the patterns that trip up most expanding companies. ## Europe ### DACH (Germany, Austria, Switzerland) - **Language:** German required for SMB. Enterprise sometimes English. - **Sales:** Relationship-driven, longer cycles, value formal proposals - **Pricing:** Willing to pay premium for quality and reliability - **Compliance:** GDPR, industry-specific (MDR for medical devices, BaFin for finance) - **Payment:** SEPA, invoice preferred for B2B (not credit cards) - **Culture:** Punctuality matters. Directness is respected. Don't oversell. - **Data:** Strong preference for EU data residency - **Entity:** GmbH for subsidiary, typically €25K minimum capital ### Nordics (Sweden, Norway, Denmark, Finland) - **Language:** English widely accepted in business - **Sales:** Consensus-driven decisions, flat hierarchies - **Pricing:** High willingness to pay, value innovation - **Compliance:** GDPR, strong data protection culture - **Culture:** Equality-focused, sustainability matters, low-key approach preferred - **Entry:** Often the easiest European expansion for English-speaking companies ### France - **Language:** French required, even for enterprise (most buyers prefer it) - **Sales:** Formal, hierarchical decision-making, relationships matter - **Pricing:** Price-sensitive but willing to invest in proven solutions - **Compliance:** GDPR + CNIL (strict data authority), French hosting preference - **Culture:** Business lunches are real meetings. Email etiquette matters. - **Entity:** SAS or SARL, complex employment law ### UK - **Language:** English (obviously) - **Sales:** Similar to US but smaller deal sizes - **Pricing:** Competitive market, price comparisons common - **Compliance:** UK GDPR (post-Brexit), FCA for finance - **Culture:** Understated, humor works, don't be too pushy - **Post-Brexit:** Separate data adequacy, some regulatory divergence ### Southern Europe (Spain, Italy, Portugal) - **Language:** Local language strongly preferred - **Sales:** Relationship-heavy, trust-based, longer cycles - **Pricing:** Lower willingness to pay than Northern Europe - **Entry:** Partner/reseller model often more effective than direct - **Culture:** Personal relationships precede business relationships - **Timing:** August is essentially closed in many industries ### Eastern Europe (Poland, Czech Republic, Romania) - **Language:** Local language for SMB, English for enterprise/tech - **Sales:** Growing market, value-conscious, quick adoption of new tech - **Pricing:** 30-50% of Western European pricing - **Talent:** Excellent engineering talent for local offices - **Entry:** Often good for first offshore team, not just sales ## United States ### General - **Entity:** Delaware C-Corp if seeking US investment - **Sales:** Expect American-style responsiveness (same-day replies) - **Pricing:** Higher than Europe (typically 20-40%) - **Compliance:** State-by-state complexity (privacy, tax nexus) - **Culture:** Optimistic, results-oriented, comfortable with direct outreach - **Legal:** More litigious environment, good contracts essential ### Regional Differences | Region | Characteristics | |--------|----------------| | **West Coast** | Tech-forward, early adopters, startup-friendly | | **East Coast** | Enterprise-heavy, finance and healthcare strong | | **Midwest** | Manufacturing, agriculture, relationship-driven, underserved | | **South** | Growing tech hubs (Austin, Atlanta, Nashville), cost-conscious | ### Key Considerations - Sales tax: Complex, state-dependent, use automation (Stripe Tax, Avalara) - Privacy: California (CCPA/CPRA), Virginia, Colorado, Connecticut have state laws - Employment: At-will, but benefits expectations are high - Health insurance: Expected by employees (significant cost) ## APAC ### Singapore - **Best entry point for APAC** (English, business-friendly, strong rule of law) - Low tax, easy incorporation, access to Southeast Asian markets - Small domestic market — use as hub, not primary market ### Australia - **English-speaking, familiar business culture** (similar to UK) - Strong B2B market, good for SaaS - Data privacy: Australian Privacy Act - Time zones: Challenge for support from Europe ### Japan - **Highest quality bar in the world** — products must be polished - Local partner essential (trust, introductions, support) - Japanese localization is non-negotiable - Long sales cycles but very loyal once committed - Business etiquette matters significantly ### India - **Huge market but price-sensitive** - Strong engineering talent market - Relationship-driven, patience required - UPI and local payment methods essential - Often better as talent market than sales market initially ## LATAM ### General - Portuguese (Brazil) and Spanish (rest) — two distinct markets - Growing SaaS adoption, especially in Brazil, Mexico, Colombia - Price-sensitive but growing willingness to pay for quality - Boleto (Brazil) and local payment methods essential - Currency volatility can affect pricing strategy ### Brazil - Largest LATAM market by far - Complex tax system (NF-e, ICMS, PIS/COFINS) - Portuguese required, no exceptions - Strong startup ecosystem (São Paulo) - Data privacy: LGPD (similar to GDPR) ### Mexico - Second largest LATAM market - Growing US business ties - Spanish required - Proximity to US is strategic advantage - Increasing SaaS adoption ## Cross-Region Patterns ### What Works Everywhere - Start with existing customer demand (pull, not push) - Invest in local language support before local sales - Price for the market, not for your cost structure - Build local case studies as fast as possible - Find one strong local partner before hiring ### What Never Works - Assuming English is enough (even when people speak it) - Copy-pasting marketing materials with just translation - Ignoring local payment preferences - Treating "Europe" or "APAC" as single markets - Sending your best home-market rep without local context
Chiến lược mua bán và sáp nhập doanh nghiệp: thẩm định, định giá, tích hợp và cấu trúc thương vụ, cho cả bên mua lẫn bên bán.
--- name: "ma-playbook" description: "M&A strategy for acquiring companies or being acquired. Due diligence, valuation, integration, and deal structure. Use when evaluating acquisitions, preparing for acquisition, M&A due diligence, integration planning, or deal negotiation." license: MIT metadata: version: 1.0.0 author: Alireza Rezvani category: c-level domain: ma-strategy updated: 2026-03-05 --- # M&A Playbook Frameworks for both sides of M&A: acquiring companies and being acquired. ## Keywords M&A, mergers and acquisitions, due diligence, acquisition, acqui-hire, integration, deal structure, valuation, LOI, term sheet, earnout ## Quick Start **Acquiring:** Start with strategic rationale → target screening → due diligence → valuation → negotiation → integration. **Being Acquired:** Start with readiness assessment → data room prep → advisor selection → negotiation → transition. ## When You're Acquiring ### Strategic Rationale (answer before anything else) - **Buy vs Build:** Can you build this faster/cheaper? If yes, don't acquire. - **Acqui-hire vs Product vs Market:** What are you really buying? Talent? Technology? Customers? - **Integration complexity:** How hard is it to merge this into your company? ### Due Diligence Checklist | Domain | Key Questions | Red Flags | |--------|--------------|-----------| | Financial | Revenue quality, customer concentration, burn rate | >30% revenue from 1 customer | | Technical | Code quality, tech debt, architecture fit | Monolith with no tests | | Legal | IP ownership, pending litigation, contracts | Key IP owned by individuals | | People | Key person risk, culture fit, retention risk | Founders have no lockup/earnout | | Market | Market position, competitive threats | Declining market share | | Customers | Churn rate, NPS, contract terms | High churn, short contracts | ### Valuation Approaches - **Revenue multiple:** Industry-dependent (2-15x ARR for SaaS) - **Comparable transactions:** What similar companies sold for - **DCF:** For profitable companies only (most startups: use multiples) - **Acqui-hire:** $1-3M per engineer in hot markets ### Integration Frameworks See `references/integration-playbook.md` for the 100-day integration plan. ## When You're Being Acquired ### Readiness Signals - Inbound interest from strategic buyers - Market consolidation happening around you - Fundraising becomes harder than operating - Founder ready for a transition ### Preparation (6-12 months before) 1. Clean up financials (audited if possible) 2. Document all IP and contracts 3. Reduce customer concentration 4. Lock up key employees 5. Build the data room 6. Engage an M&A advisor ### Negotiation Points | Term | What to Watch | Your Leverage | |------|--------------|---------------| | Valuation | Earnout traps (unreachable targets) | Multiple competing offers | | Earnout | Milestone definitions, measurement period | Cash-heavy vs earnout-heavy split | | Lockup | Duration, conditions | Your replaceability | | Rep & warranties | Scope of liability | Escrow vs indemnification cap | | Employee retention | Who gets offers, at what terms | Key person dependencies | ## Red Flags (Both Sides) - No clear strategic rationale beyond "it's a good deal" - Culture clash visible during due diligence and ignored - Key people not locked in before close - Integration plan doesn't exist or is "we'll figure it out" - Valuation based on projections, not actuals ## Integration with C-Suite Roles | Role | Contribution to M&A | |------|-------------------| | CEO | Strategic rationale, negotiation lead | | CFO | Valuation, deal structure, financing | | CTO | Technical due diligence, integration architecture | | CHRO | People due diligence, retention planning | | COO | Integration execution, process merge | | CPO | Product roadmap impact, customer overlap | ## Resources - `references/integration-playbook.md` — 100-day post-acquisition integration plan - `references/due-diligence-checklist.md` — comprehensive DD checklist by domain FILE:references/due-diligence-checklist.md # M&A Due Diligence Checklist Comprehensive due diligence organized by domain. Not every item applies to every deal — focus on what matters for YOUR acquisition rationale. ## Financial Due Diligence ### Revenue Quality - [ ] Revenue by customer (top 10 customer concentration) - [ ] Revenue by product line - [ ] Revenue by geography - [ ] MRR/ARR trend (24 months minimum) - [ ] Churn rate (gross and net, by cohort) - [ ] Revenue recognition policies - [ ] Deferred revenue / backlog - [ ] One-time vs recurring revenue split - [ ] Professional services vs product revenue ### Profitability - [ ] Gross margin by product line - [ ] Operating expenses breakdown - [ ] Burn rate trend (improving or worsening?) - [ ] Path to profitability (realistic or aspirational?) - [ ] Unit economics (LTV, CAC, payback by channel) ### Cash & Liabilities - [ ] Cash position and burn rate - [ ] Outstanding debt (terms, covenants) - [ ] Accounts receivable aging - [ ] Accounts payable - [ ] Pending or contingent liabilities - [ ] Tax obligations (any back taxes?) - [ ] Cap table (fully diluted, option pool) ### Financial Controls - [ ] Audit history (audited vs reviewed vs compiled) - [ ] Financial reporting cadence and quality - [ ] Budget vs actual variance history - [ ] Key financial policies ## Technical Due Diligence ### Architecture - [ ] Architecture diagrams (current state) - [ ] Technology stack inventory - [ ] Infrastructure (cloud provider, regions, costs) - [ ] Scalability assessment (current capacity vs load) - [ ] Security architecture (encryption, access controls) ### Code Quality - [ ] Test coverage (unit, integration, e2e) - [ ] CI/CD pipeline maturity - [ ] Technical debt inventory (estimated remediation cost) - [ ] Code review practices - [ ] Documentation quality ### Data - [ ] Data architecture and storage - [ ] Data privacy compliance (GDPR, CCPA) - [ ] Data portability (can you migrate it?) - [ ] Proprietary data assets (training data, user data) - [ ] Data retention policies ### Operational - [ ] Uptime history (SLA compliance) - [ ] Incident history (frequency, severity, resolution time) - [ ] Monitoring and alerting coverage - [ ] Disaster recovery plan and testing history - [ ] On-call rotation and processes ## Legal Due Diligence ### Intellectual Property - [ ] Patents (granted and pending) - [ ] Trademarks - [ ] Copyright registrations - [ ] IP assignment agreements (all employees/contractors) - [ ] Open source usage and compliance - [ ] Trade secrets protection measures ### Contracts - [ ] Customer contracts (terms, renewals, termination rights) - [ ] Vendor contracts (key dependencies, terms) - [ ] Partnership agreements - [ ] Lease agreements - [ ] Employment agreements (non-competes, IP clauses) ### Compliance & Litigation - [ ] Pending or threatened litigation - [ ] Regulatory compliance status - [ ] Government investigations - [ ] Insurance coverage - [ ] Prior legal disputes and resolutions ## People Due Diligence ### Team Composition - [ ] Org chart with roles and tenure - [ ] Key person dependencies (bus factor) - [ ] Compensation details (salary, equity, bonuses) - [ ] Employment agreements and non-competes - [ ] Contractor vs employee classification ### Culture & Retention - [ ] Recent engagement survey results - [ ] Turnover rate (last 12-24 months) - [ ] Glassdoor/reputation assessment - [ ] Management quality assessment - [ ] Culture compatibility analysis ### HR Compliance - [ ] Employee handbook and policies - [ ] HR complaints or investigations - [ ] Benefits programs - [ ] Equity plan details and administration ## Market Due Diligence ### Market Position - [ ] Market size (TAM, SAM, SOM) with sources - [ ] Market share estimate - [ ] Growth rate (market and company) - [ ] Competitive landscape (direct and indirect) - [ ] Barriers to entry / competitive moat ### Customer Analysis - [ ] Customer segmentation - [ ] Win/loss analysis (why customers chose them) - [ ] NPS or satisfaction scores - [ ] Customer acquisition channels - [ ] Customer lifetime and expansion patterns ## Red Flag Severity Guide | Severity | Examples | Action | |----------|---------|--------| | **Deal killer** | IP not properly assigned, undisclosed litigation, fraud | Walk away | | **Major renegotiation** | Customer concentration >40%, key person risk, technical debt >6 months | Reduce price or add protections | | **Integration risk** | Culture mismatch, legacy systems, manual processes | Budget for remediation | | **Monitor** | High churn, declining NPS, aging tech stack | Track post-close | ## Due Diligence Timeline | Phase | Duration | Focus | |-------|----------|-------| | Preliminary | 1-2 weeks | Public info, financials, high-level tech | | Deep dive | 4-6 weeks | All domains, interviews, code review | | Confirmation | 1-2 weeks | Verify claims, resolve open questions | | Final | 1 week | Legal review, final terms negotiation | FILE:references/integration-playbook.md # Post-Acquisition Integration Playbook The 100-day plan for integrating an acquisition. Most acquisitions fail not because of bad deals but bad integration. ## The Integration Paradox Move too fast → you break what you bought. Move too slow → talent leaves, customers churn, value evaporates. **The rule:** Decide on day 1 what stays separate and what merges. Then execute without wavering. ## Pre-Close (Day -30 to 0) ### Integration Lead - Appoint ONE integration lead (not a committee) - This person reports to the CEO, has authority over all workstreams - Full-time role for 100 days minimum ### Planning | Workstream | Owner | Day 1 Decisions | |-----------|-------|-----------------| | People | CHRO | Who stays, comp alignment, reporting lines | | Technology | CTO | Systems to merge, timeline, migration order | | Customers | CRO | Communication plan, account ownership | | Product | CPO | Roadmap integration, feature consolidation | | Operations | COO | Process alignment, tool consolidation | | Finance | CFO | Entity structure, billing, reporting | | Legal | External | Contract assignments, IP transfer | ### Communication Plan (ready before close) - Employee announcement (both companies) — Day 1 - Customer notification — Day 1-3 - Partner/vendor notification — Week 1 - Public announcement — per deal terms ## Week 1 (Days 1-7): Stabilize **Goal:** No one leaves, no customer churns, operations continue. - [ ] All-hands meeting (both companies together) - [ ] 1:1 with every acquired leader (within 48 hours) - [ ] Retention packages confirmed for key employees - [ ] Customer communication sent (personal for top 20 accounts) - [ ] Systems access provisioned (email, Slack, tools) - [ ] Integration FAQ published internally ### The First All-Hands What people want to hear: 1. Why this happened (honest version) 2. What changes (be specific, not vague) 3. What doesn't change (equally important) 4. Their job security (be direct) 5. Timeline for decisions What NOT to say: "Nothing will change." (It will. They know it.) ## Month 1 (Days 1-30): Orient **Goal:** Teams know each other, quick wins shipped, blockers identified. ### People - [ ] Org chart finalized and communicated - [ ] Comp band alignment completed - [ ] Benefits transition timeline published - [ ] Cross-team introductions facilitated (not forced) - [ ] Culture assessment: what's different, what's compatible ### Technology - [ ] Architecture assessment complete - [ ] Migration priority ranked (quick wins first) - [ ] Shared development environment established - [ ] Code access and permissions set up - [ ] Technical debt from both sides documented ### Customers - [ ] Top 20 accounts contacted personally by leadership - [ ] Unified support channel established (or plan for it) - [ ] Pricing/contract transition plan for overlapping customers - [ ] Product roadmap communication (what's coming, what's being deprecated) ### Quick Wins Ship something visible in the first 30 days. A feature that combines both companies' strengths. This proves the acquisition works better than any memo. ## Month 2-3 (Days 31-100): Integrate **Goal:** Core systems merged, one team operating, value creation visible. ### Systems Integration Priority 1. **Communication** (Slack, email) — Week 2 2. **Identity** (SSO, accounts) — Week 3 3. **Development** (repos, CI/CD) — Month 1 4. **Data** (analytics, CRM) — Month 2 5. **Product** (shared platform) — Month 2-3 6. **Finance** (billing, reporting) — Month 3 ### Culture Integration - **Don't:** Force the acquired team to adopt everything immediately - **Do:** Find the best practices from BOTH cultures, adopt the winner - **Don't:** Rename everything on Day 1 - **Do:** Co-create the combined identity over 60 days - **Watch for:** "Us vs them" language, meeting exclusions, information hoarding ### Measuring Integration Success | Metric | Target | Frequency | |--------|--------|-----------| | Employee retention (key people) | > 90% at 100 days | Weekly | | Customer retention | > 95% at 100 days | Monthly | | Cross-team collaboration (PRs, meetings) | Increasing trend | Weekly | | Synergy revenue (combined offerings) | First deal within 60 days | Monthly | | Integration milestones hit | > 80% on time | Weekly | ## Post-100 Days Integration isn't "done" at 100 days. But the foundation should be solid. ### Ongoing - Quarterly integration retrospective (what's working, what isn't) - Culture health check at 6 months - Full financial integration assessment at 12 months - Earnout milestone tracking (if applicable) ### Common Failure Modes | Failure | Root Cause | Prevention | |---------|-----------|------------| | Key talent leaves at month 4 | Retention cliff, culture mismatch | Longer earnout, culture attention | | Customer churn spike at month 6 | Product changes without warning | Over-communicate product roadmap | | "Two companies in a trenchcoat" | Incomplete integration | Force cross-functional projects | | Value never materializes | Wrong acquisition rationale | Kill the deal if rationale was wrong | | Acquirer culture overwhelms | "Our way is the only way" | Adopt best of both explicitly | ## The Kill Switch Sometimes acquisitions don't work. Signs it's failing: - Key people leaving despite retention packages - Customers churning above baseline - Integration milestones consistently missed - Culture clash worsening, not improving - Revenue synergies aren't materializing at month 6 **Options:** 1. Double down with new integration lead and plan 2. Operate as semi-autonomous unit (less integration) 3. Spin off or divest (expensive, but sometimes necessary) Admitting failure early costs less than dragging it out.
Thiết kế và triển khai MCP server sẵn sàng production từ hợp đồng OpenAPI, hỗ trợ Python và TypeScript, kiểm tra schema và tiến hóa an toàn.
---
name: "mcp-server-builder"
description: "Design and ship production-ready MCP (Model Context Protocol) servers from OpenAPI contracts instead of hand-written tool wrappers. Python and TypeScript support, schema validation, safe evolution. Use when exposing an existing API as an MCP server, building tool integrations for Claude or Codex or Cursor, or scaffolding an MCP project from scratch."
---
# MCP Server Builder
**Tier:** POWERFUL
**Category:** Engineering
**Domain:** AI / API Integration
## Overview
Use this skill to design and ship production-ready MCP servers from API contracts instead of hand-written one-off tool wrappers. It focuses on fast scaffolding, schema quality, validation, and safe evolution.
The workflow supports both Python and TypeScript MCP implementations and treats OpenAPI as the source of truth.
## Core Capabilities
- Convert OpenAPI paths/operations into MCP tool definitions
- Generate starter server scaffolds (Python or TypeScript)
- Enforce naming, descriptions, and schema consistency
- Validate MCP tool manifests for common production failures
- Apply versioning and backward-compatibility checks
- Separate transport/runtime decisions from tool contract design
## When to Use
- You need to expose an internal/external REST API to an LLM agent
- You are replacing brittle browser automation with typed tools
- You want one MCP server shared across teams and assistants
- You need repeatable quality checks before publishing MCP tools
- You want to bootstrap an MCP server from existing OpenAPI specs
## Key Workflows
### 1. OpenAPI to MCP Scaffold
1. Start from a valid OpenAPI spec.
2. Generate tool manifest + starter server code.
3. Review naming and auth strategy.
4. Add endpoint-specific runtime logic.
```bash
python3 scripts/openapi_to_mcp.py \
--input openapi.json \
--server-name billing-mcp \
--language python \
--output-dir ./out \
--format text
```
Supports stdin as well:
```bash
cat openapi.json | python3 scripts/openapi_to_mcp.py --server-name billing-mcp --language typescript
```
### 2. Validate MCP Tool Definitions
Run validator before integration tests:
```bash
python3 scripts/mcp_validator.py --input out/tool_manifest.json --strict --format text
```
Checks include duplicate names, invalid schema shape, missing descriptions, empty required fields, and naming hygiene.
### 3. Runtime Selection
- Choose **Python** for fast iteration and data-heavy backends.
- Choose **TypeScript** for unified JS stacks and tighter frontend/backend contract reuse.
- Keep tool contracts stable even if transport/runtime changes.
### 4. Auth & Safety Design
- Keep secrets in env, not in tool schemas.
- Prefer explicit allowlists for outbound hosts.
- Return structured errors (`code`, `message`, `details`) for agent recovery.
- Avoid destructive operations without explicit confirmation inputs.
### 5. Versioning Strategy
- Additive fields only for non-breaking updates.
- Never rename tool names in-place.
- Introduce new tool IDs for breaking behavior changes.
- Maintain changelog of tool contracts per release.
## Script Interfaces
- `python3 scripts/openapi_to_mcp.py --help`
- Reads OpenAPI from stdin or `--input`
- Produces manifest + server scaffold
- Emits JSON summary or text report
- `python3 scripts/mcp_validator.py --help`
- Validates manifests and optional runtime config
- Returns non-zero exit in strict mode when errors exist
## Common Pitfalls
1. Tool names derived directly from raw paths (`get__v1__users___id`)
2. Missing operation descriptions (agents choose tools poorly)
3. Ambiguous parameter schemas with no required fields
4. Mixing transport errors and domain errors in one opaque message
5. Building tool contracts that expose secret values
6. Breaking clients by changing schema keys without versioning
## Best Practices
1. Use `operationId` as canonical tool name when available.
2. Keep one task intent per tool; avoid mega-tools.
3. Add concise descriptions with action verbs.
4. Validate contracts in CI using strict mode.
5. Keep generated scaffold committed, then customize incrementally.
6. Pair contract changes with changelog entries.
## Reference Material
- [references/openapi-extraction-guide.md](references/openapi-extraction-guide.md)
- [references/python-server-template.md](references/python-server-template.md)
- [references/typescript-server-template.md](references/typescript-server-template.md)
- [references/validation-checklist.md](references/validation-checklist.md)
- [README.md](README.md)
## Architecture Decisions
Choose the server approach per constraint:
- Python runtime: faster iteration, data pipelines, backend-heavy teams
- TypeScript runtime: shared types with JS stack, frontend-heavy teams
- Single MCP server: easiest operations, broader blast radius
- Split domain servers: cleaner ownership and safer change boundaries
## Contract Quality Gates
Before publishing a manifest:
1. Every tool has clear verb-first name.
2. Every tool description explains intent and expected result.
3. Every required field is explicitly typed.
4. Destructive actions include confirmation parameters.
5. Error payload format is consistent across all tools.
6. Validator returns zero errors in strict mode.
## Testing Strategy
- Unit: validate transformation from OpenAPI operation to MCP tool schema.
- Contract: snapshot `tool_manifest.json` and review diffs in PR.
- Integration: call generated tool handlers against staging API.
- Resilience: simulate 4xx/5xx upstream errors and verify structured responses.
## Deployment Practices
- Pin MCP runtime dependencies per environment.
- Roll out server updates behind versioned endpoint/process.
- Keep backward compatibility for one release window minimum.
- Add changelog notes for new/removed/changed tool contracts.
## Security Controls
- Keep outbound host allowlist explicit.
- Do not proxy arbitrary URLs from user-provided input.
- Redact secrets and auth headers from logs.
- Rate-limit high-cost tools and add request timeouts.
FILE:README.md
# MCP Server Builder
Generate and validate MCP servers from OpenAPI contracts with production-focused tooling. This skill helps teams bootstrap fast and enforce schema quality before shipping.
## Quick Start
```bash
# Generate scaffold from OpenAPI
python3 scripts/openapi_to_mcp.py \
--input openapi.json \
--server-name my-mcp \
--language python \
--output-dir ./generated \
--format text
# Validate generated manifest
python3 scripts/mcp_validator.py --input generated/tool_manifest.json --strict --format text
```
## Included Tools
- `scripts/openapi_to_mcp.py`: OpenAPI -> `tool_manifest.json` + starter server scaffold
- `scripts/mcp_validator.py`: structural and quality validation for MCP tool definitions
## References
- `references/openapi-extraction-guide.md`
- `references/python-server-template.md`
- `references/typescript-server-template.md`
- `references/validation-checklist.md`
## Installation
### Claude Code
```bash
cp -R engineering/mcp-server-builder ~/.claude/skills/mcp-server-builder
```
### OpenAI Codex
```bash
cp -R engineering/mcp-server-builder ~/.codex/skills/mcp-server-builder
```
### OpenClaw
```bash
cp -R engineering/mcp-server-builder ~/.openclaw/skills/mcp-server-builder
```
FILE:references/openapi-extraction-guide.md
# OpenAPI Extraction Guide
## Goal
Turn stable API operations into stable MCP tools with clear names and reliable schemas.
## Extraction Rules
1. Prefer `operationId` as tool name.
2. Fallback naming: `<method>_<path>` sanitized to snake_case.
3. Pull `summary` for tool description; fallback to `description`.
4. Merge path/query parameters into `inputSchema.properties`.
5. Merge `application/json` request-body object properties when available.
6. Preserve required fields from both parameters and request body.
## Naming Guidance
Good names:
- `list_customers`
- `create_invoice`
- `archive_project`
Avoid:
- `tool1`
- `run`
- `get__v1__customer___id`
## Schema Guidance
- `inputSchema.type` must be `object`.
- Every `required` key must exist in `properties`.
- Include concise descriptions on high-risk fields (IDs, dates, money, destructive flags).
FILE:references/python-server-template.md
# Python MCP Server Template
```python
from fastmcp import FastMCP
import httpx
import os
mcp = FastMCP(name="my-server")
API_BASE = os.environ["API_BASE"]
API_TOKEN = os.environ["API_TOKEN"]
@mcp.tool()
def list_items(input: dict) -> dict:
with httpx.Client(base_url=API_BASE, headers={"Authorization": f"Bearer {API_TOKEN}"}) as client:
resp = client.get("/items", params=input)
if resp.status_code >= 400:
return {"error": {"code": "upstream_error", "message": "List failed", "details": resp.text}}
return resp.json()
if __name__ == "__main__":
mcp.run()
```
FILE:references/typescript-server-template.md
# TypeScript MCP Server Template
```ts
import { FastMCP } from "fastmcp";
const server = new FastMCP({ name: "my-server" });
server.tool(
"list_items",
"List items from upstream service",
async (input) => {
return {
content: [{ type: "text", text: JSON.stringify({ status: "todo", input }) }],
};
}
);
server.run();
```
FILE:references/validation-checklist.md
# MCP Validation Checklist
## Structural Integrity
- [ ] Tool names are unique across the manifest
- [ ] Tool names use lowercase snake_case (3-64 chars, `[a-z0-9_]`)
- [ ] `inputSchema.type` is always `"object"`
- [ ] Every `required` field exists in `properties`
- [ ] No empty `properties` objects (warn if inputs truly optional)
## Descriptive Quality
- [ ] All tools include actionable descriptions (≥10 chars)
- [ ] Descriptions start with a verb ("Create…", "Retrieve…", "Delete…")
- [ ] Parameter descriptions explain expected values, not just types
## Security & Safety
- [ ] Auth tokens and secrets are NOT exposed in tool schemas
- [ ] Destructive tools require explicit confirmation input parameters
- [ ] No tool accepts arbitrary URLs or file paths without validation
- [ ] Outbound host allowlists are explicit where applicable
## Versioning & Compatibility
- [ ] Breaking tool changes use new tool IDs (never rename in-place)
- [ ] Additive-only changes for non-breaking updates
- [ ] Contract changelog is maintained per release
- [ ] Deprecated tools include sunset timeline in description
## Runtime & Error Handling
- [ ] Error responses use consistent structure (`code`, `message`, `details`)
- [ ] Timeout and rate-limit behaviors are documented
- [ ] Large response payloads are paginated or truncated
FILE:scripts/mcp_validator.py
#!/usr/bin/env python3
"""Validate MCP tool manifest files for common contract issues.
Input sources:
- --input <manifest.json>
- stdin JSON
Validation domains:
- structural correctness
- naming hygiene
- schema consistency
- descriptive completeness
"""
import argparse
import json
import re
import sys
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
TOOL_NAME_RE = re.compile(r"^[a-z0-9_]{3,64}$")
class CLIError(Exception):
"""Raised for expected CLI failures."""
@dataclass
class ValidationResult:
errors: List[str]
warnings: List[str]
tool_count: int
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Validate MCP tool definitions.")
parser.add_argument("--input", help="Path to manifest JSON file. If omitted, reads from stdin.")
parser.add_argument("--strict", action="store_true", help="Exit non-zero when errors are found.")
parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format.")
return parser.parse_args()
def load_manifest(input_path: Optional[str]) -> Dict[str, Any]:
if input_path:
try:
data = Path(input_path).read_text(encoding="utf-8")
except Exception as exc:
raise CLIError(f"Failed reading --input: {exc}") from exc
else:
if sys.stdin.isatty():
raise CLIError("No input provided. Use --input or pipe manifest JSON via stdin.")
data = sys.stdin.read().strip()
if not data:
raise CLIError("Empty stdin.")
try:
payload = json.loads(data)
except json.JSONDecodeError as exc:
raise CLIError(f"Invalid JSON input: {exc}") from exc
if not isinstance(payload, dict):
raise CLIError("Manifest root must be a JSON object.")
return payload
def validate_schema(tool_name: str, schema: Dict[str, Any]) -> Tuple[List[str], List[str]]:
errors: List[str] = []
warnings: List[str] = []
if schema.get("type") != "object":
errors.append(f"{tool_name}: inputSchema.type must be 'object'.")
props = schema.get("properties", {})
if not isinstance(props, dict):
errors.append(f"{tool_name}: inputSchema.properties must be an object.")
props = {}
required = schema.get("required", [])
if not isinstance(required, list):
errors.append(f"{tool_name}: inputSchema.required must be an array.")
required = []
prop_keys = set(props.keys())
for req in required:
if req not in prop_keys:
errors.append(f"{tool_name}: required field '{req}' is not defined in properties.")
if not props:
warnings.append(f"{tool_name}: no input properties declared.")
for pname, pdef in props.items():
if not isinstance(pdef, dict):
errors.append(f"{tool_name}: property '{pname}' must be an object.")
continue
ptype = pdef.get("type")
if not ptype:
warnings.append(f"{tool_name}: property '{pname}' has no explicit type.")
return errors, warnings
def validate_manifest(payload: Dict[str, Any]) -> ValidationResult:
errors: List[str] = []
warnings: List[str] = []
tools = payload.get("tools")
if not isinstance(tools, list):
raise CLIError("Manifest must include a 'tools' array.")
seen_names = set()
for idx, tool in enumerate(tools):
if not isinstance(tool, dict):
errors.append(f"tool[{idx}] is not an object.")
continue
name = str(tool.get("name", "")).strip()
desc = str(tool.get("description", "")).strip()
schema = tool.get("inputSchema")
if not name:
errors.append(f"tool[{idx}] missing name.")
continue
if name in seen_names:
errors.append(f"duplicate tool name: {name}")
seen_names.add(name)
if not TOOL_NAME_RE.match(name):
warnings.append(
f"{name}: non-standard naming; prefer lowercase snake_case (3-64 chars, [a-z0-9_])."
)
if len(desc) < 10:
warnings.append(f"{name}: description too short; provide actionable purpose.")
if not isinstance(schema, dict):
errors.append(f"{name}: missing or invalid inputSchema object.")
continue
schema_errors, schema_warnings = validate_schema(name, schema)
errors.extend(schema_errors)
warnings.extend(schema_warnings)
return ValidationResult(errors=errors, warnings=warnings, tool_count=len(tools))
def to_text(result: ValidationResult) -> str:
lines = [
"MCP manifest validation",
f"- tools: {result.tool_count}",
f"- errors: {len(result.errors)}",
f"- warnings: {len(result.warnings)}",
]
if result.errors:
lines.append("Errors:")
lines.extend([f"- {item}" for item in result.errors])
if result.warnings:
lines.append("Warnings:")
lines.extend([f"- {item}" for item in result.warnings])
return "\n".join(lines)
def main() -> int:
args = parse_args()
payload = load_manifest(args.input)
result = validate_manifest(payload)
if args.format == "json":
print(json.dumps(asdict(result), indent=2))
else:
print(to_text(result))
if args.strict and result.errors:
return 1
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except CLIError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
raise SystemExit(2)
FILE:scripts/openapi_to_mcp.py
#!/usr/bin/env python3
"""Generate MCP scaffold files from an OpenAPI specification.
Input sources:
- --input <file>
- stdin (JSON or YAML when PyYAML is available)
Output:
- tool_manifest.json
- server.py or server.ts scaffold
- summary in text/json
"""
import argparse
import json
import re
import sys
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Any, Dict, List, Optional
HTTP_METHODS = {"get", "post", "put", "patch", "delete"}
class CLIError(Exception):
"""Raised for expected CLI failures."""
@dataclass
class GenerationSummary:
server_name: str
language: str
operations_total: int
tools_generated: int
output_dir: str
manifest_path: str
scaffold_path: str
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate MCP server scaffold from OpenAPI.")
parser.add_argument("--input", help="OpenAPI file path (JSON or YAML). If omitted, reads from stdin.")
parser.add_argument("--server-name", required=True, help="MCP server name.")
parser.add_argument("--language", choices=["python", "typescript"], default="python", help="Scaffold language.")
parser.add_argument("--output-dir", default=".", help="Directory to write generated files.")
parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format.")
return parser.parse_args()
def load_raw_input(input_path: Optional[str]) -> str:
if input_path:
try:
return Path(input_path).read_text(encoding="utf-8")
except Exception as exc:
raise CLIError(f"Failed to read --input file: {exc}") from exc
if sys.stdin.isatty():
raise CLIError("No input provided. Use --input <spec-file> or pipe OpenAPI via stdin.")
data = sys.stdin.read().strip()
if not data:
raise CLIError("Stdin was provided but empty.")
return data
def parse_openapi(raw: str) -> Dict[str, Any]:
try:
return json.loads(raw)
except json.JSONDecodeError:
try:
import yaml # type: ignore
parsed = yaml.safe_load(raw)
if not isinstance(parsed, dict):
raise CLIError("YAML OpenAPI did not parse into an object.")
return parsed
except ImportError as exc:
raise CLIError("Input is not valid JSON and PyYAML is unavailable for YAML parsing.") from exc
except Exception as exc:
raise CLIError(f"Failed to parse OpenAPI input: {exc}") from exc
def sanitize_tool_name(name: str) -> str:
cleaned = re.sub(r"[^a-zA-Z0-9_]+", "_", name).strip("_")
cleaned = re.sub(r"_+", "_", cleaned)
return cleaned.lower() or "unnamed_tool"
def schema_from_parameter(param: Dict[str, Any]) -> Dict[str, Any]:
schema = param.get("schema", {})
if not isinstance(schema, dict):
schema = {}
out = {
"type": schema.get("type", "string"),
"description": param.get("description", ""),
}
if "enum" in schema:
out["enum"] = schema["enum"]
return out
def extract_tools(spec: Dict[str, Any]) -> List[Dict[str, Any]]:
paths = spec.get("paths", {})
if not isinstance(paths, dict):
raise CLIError("OpenAPI spec missing valid 'paths' object.")
tools = []
for path, methods in paths.items():
if not isinstance(methods, dict):
continue
for method, operation in methods.items():
method_l = str(method).lower()
if method_l not in HTTP_METHODS or not isinstance(operation, dict):
continue
op_id = operation.get("operationId")
if op_id:
name = sanitize_tool_name(str(op_id))
else:
name = sanitize_tool_name(f"{method_l}_{path}")
description = str(operation.get("summary") or operation.get("description") or f"{method_l.upper()} {path}")
properties: Dict[str, Any] = {}
required: List[str] = []
for param in operation.get("parameters", []):
if not isinstance(param, dict):
continue
pname = str(param.get("name", "")).strip()
if not pname:
continue
properties[pname] = schema_from_parameter(param)
if bool(param.get("required")):
required.append(pname)
request_body = operation.get("requestBody", {})
if isinstance(request_body, dict):
content = request_body.get("content", {})
if isinstance(content, dict):
app_json = content.get("application/json", {})
if isinstance(app_json, dict):
schema = app_json.get("schema", {})
if isinstance(schema, dict) and schema.get("type") == "object":
rb_props = schema.get("properties", {})
if isinstance(rb_props, dict):
for key, val in rb_props.items():
if isinstance(val, dict):
properties[key] = val
rb_required = schema.get("required", [])
if isinstance(rb_required, list):
required.extend([str(x) for x in rb_required])
tool = {
"name": name,
"description": description,
"inputSchema": {
"type": "object",
"properties": properties,
"required": sorted(set(required)),
},
"x-openapi": {"path": path, "method": method_l},
}
tools.append(tool)
return tools
def python_scaffold(server_name: str, tools: List[Dict[str, Any]]) -> str:
handlers = []
for tool in tools:
fname = sanitize_tool_name(tool["name"])
handlers.append(
f"@mcp.tool()\ndef {fname}(input: dict) -> dict:\n"
f" \"\"\"{tool['description']}\"\"\"\n"
f" return {{\"tool\": \"{tool['name']}\", \"status\": \"todo\", \"input\": input}}\n"
)
return "\n".join(
[
"#!/usr/bin/env python3",
'"""Generated MCP server scaffold."""',
"",
"from fastmcp import FastMCP",
"",
f"mcp = FastMCP(name={server_name!r})",
"",
*handlers,
"",
"if __name__ == '__main__':",
" mcp.run()",
"",
]
)
def typescript_scaffold(server_name: str, tools: List[Dict[str, Any]]) -> str:
registrations = []
for tool in tools:
const_name = sanitize_tool_name(tool["name"])
registrations.append(
"server.tool(\n"
f" '{tool['name']}',\n"
f" '{tool['description']}',\n"
" async (input) => ({\n"
f" content: [{{ type: 'text', text: JSON.stringify({{ tool: '{const_name}', status: 'todo', input }}) }}],\n"
" })\n"
");"
)
return "\n".join(
[
"// Generated MCP server scaffold",
"import { FastMCP } from 'fastmcp';",
"",
f"const server = new FastMCP({{ name: '{server_name}' }});",
"",
*registrations,
"",
"server.run();",
"",
]
)
def write_outputs(server_name: str, language: str, output_dir: Path, tools: List[Dict[str, Any]]) -> GenerationSummary:
output_dir.mkdir(parents=True, exist_ok=True)
manifest_path = output_dir / "tool_manifest.json"
manifest = {"server": server_name, "tools": tools}
manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
if language == "python":
scaffold_path = output_dir / "server.py"
scaffold_path.write_text(python_scaffold(server_name, tools), encoding="utf-8")
else:
scaffold_path = output_dir / "server.ts"
scaffold_path.write_text(typescript_scaffold(server_name, tools), encoding="utf-8")
return GenerationSummary(
server_name=server_name,
language=language,
operations_total=len(tools),
tools_generated=len(tools),
output_dir=str(output_dir.resolve()),
manifest_path=str(manifest_path.resolve()),
scaffold_path=str(scaffold_path.resolve()),
)
def main() -> int:
args = parse_args()
raw = load_raw_input(args.input)
spec = parse_openapi(raw)
tools = extract_tools(spec)
if not tools:
raise CLIError("No operations discovered in OpenAPI paths.")
summary = write_outputs(
server_name=args.server_name,
language=args.language,
output_dir=Path(args.output_dir),
tools=tools,
)
if args.format == "json":
print(json.dumps(asdict(summary), indent=2))
else:
print("MCP scaffold generated")
print(f"- server: {summary.server_name}")
print(f"- language: {summary.language}")
print(f"- tools: {summary.tools_generated}")
print(f"- manifest: {summary.manifest_path}")
print(f"- scaffold: {summary.scaffold_path}")
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except CLIError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
raise SystemExit(2)
Chuyển bộ test từ Cypress hoặc Selenium sang Playwright.
---
name: "migrate"
description: >-
Migrate from Cypress or Selenium to Playwright. Use when user mentions
"cypress", "selenium", "migrate tests", "convert tests", "switch to
playwright", "move from cypress", or "replace selenium".
---
# Migrate to Playwright
Interactive migration from Cypress or Selenium to Playwright with file-by-file conversion.
## Input
`$ARGUMENTS` can be:
- `"from cypress"` — migrate Cypress test suite
- `"from selenium"` — migrate Selenium/WebDriver tests
- A file path: convert a specific test file
- Empty: auto-detect source framework
## Steps
### 1. Detect Source Framework
Use `Explore` subagent to scan:
- `cypress/` directory or `cypress.config.ts` → Cypress
- `selenium`, `webdriver` in `package.json` deps → Selenium
- `.py` test files with `selenium` imports → Selenium (Python)
### 2. Assess Migration Scope
Count files and categorize:
```
Migration Assessment:
- Total test files: X
- Cypress custom commands: Y
- Cypress fixtures: Z
- Estimated effort: [small|medium|large]
```
| Size | Files | Approach |
|---|---|---|
| Small (1-10) | Convert sequentially | Direct conversion |
| Medium (11-30) | Batch in groups of 5 | Use sub-agents |
| Large (31+) | Use `/batch` | Parallel conversion with `/batch` |
### 3. Set Up Playwright (If Not Present)
Run `/pw:init` first if Playwright isn't configured.
### 4. Convert Files
For each file, apply the appropriate mapping:
#### Cypress → Playwright
Load `cypress-mapping.md` for complete reference.
Key translations:
```
cy.visit(url) → page.goto(url)
cy.get(selector) → page.locator(selector) or page.getByRole(...)
cy.contains(text) → page.getByText(text)
cy.find(selector) → locator.locator(selector)
cy.click() → locator.click()
cy.type(text) → locator.fill(text)
cy.should('be.visible') → expect(locator).toBeVisible()
cy.should('have.text') → expect(locator).toHaveText(text)
cy.intercept() → page.route()
cy.wait('@alias') → page.waitForResponse()
cy.fixture() → JSON import or test data file
```
**Cypress custom commands** → Playwright fixtures or helper functions
**Cypress plugins** → Playwright config or fixtures
**`before`/`beforeEach`** → `test.beforeAll()` / `test.beforeEach()`
#### Selenium → Playwright
Load `selenium-mapping.md` for complete reference.
Key translations:
```
driver.get(url) → page.goto(url)
driver.findElement(By.id('x')) → page.locator('#x') or page.getByTestId('x')
driver.findElement(By.css('.x')) → page.locator('.x') or page.getByRole(...)
element.click() → locator.click()
element.sendKeys(text) → locator.fill(text)
element.getText() → locator.textContent()
WebDriverWait + ExpectedConditions → expect(locator).toBeVisible()
driver.switchTo().frame() → page.frameLocator()
Actions → locator.hover(), locator.dragTo()
```
### 5. Upgrade Locators
During conversion, upgrade selectors to Playwright best practices:
- `#id` → `getByTestId()` or `getByRole()`
- `.class` → `getByRole()` or `getByText()`
- `[data-testid]` → `getByTestId()`
- XPath → role-based locators
### 6. Convert Custom Commands / Utilities
- Cypress custom commands → Playwright custom fixtures via `test.extend()`
- Selenium page objects → Playwright page objects (keep structure, update API)
- Shared helpers → TypeScript utility functions
### 7. Verify Each Converted File
After converting each file:
```bash
npx playwright test <converted-file> --reporter=list
```
Fix any compilation or runtime errors before moving to the next file.
### 8. Clean Up
After all files are converted:
- Remove Cypress/Selenium dependencies from `package.json`
- Remove old config files (`cypress.config.ts`, etc.)
- Update CI workflow to use Playwright
- Update README with new test commands
Ask user before deleting anything.
## Output
- Conversion summary: files converted, total tests migrated
- Any tests that couldn't be auto-converted (manual intervention needed)
- Updated CI config
- Before/after comparison of test run results
FILE:cypress-mapping.md
# Cypress → Playwright Mapping
## Commands
| Cypress | Playwright | Notes |
|---|---|---|
| `cy.visit('/page')` | `await page.goto('/page')` | Use `baseURL` in config |
| `cy.get('.selector')` | `page.locator('.selector')` | Prefer `getByRole()` |
| `cy.get('[data-cy=x]')` | `page.getByTestId('x')` | |
| `cy.contains('text')` | `page.getByText('text')` | |
| `cy.find('.child')` | `parent.locator('.child')` | Chain from parent locator |
| `cy.first()` | `locator.first()` | |
| `cy.last()` | `locator.last()` | |
| `cy.eq(n)` | `locator.nth(n)` | |
| `cy.parent()` | `locator.locator('..')` | Or restructure with better locators |
| `cy.children()` | `locator.locator('> *')` | |
| `cy.siblings()` | Not direct — restructure test | Use parent + filter |
## Actions
| Cypress | Playwright | Notes |
|---|---|---|
| `.click()` | `await locator.click()` | Always `await` |
| `.dblclick()` | `await locator.dblclick()` | |
| `.rightclick()` | `await locator.click({ button: 'right' })` | |
| `.type('text')` | `await locator.fill('text')` | `fill()` clears first |
| `.type('text', { delay: 50 })` | `await locator.pressSequentially('text', { delay: 50 })` | Simulates typing |
| `.clear()` | `await locator.clear()` | |
| `.check()` | `await locator.check()` | |
| `.uncheck()` | `await locator.uncheck()` | |
| `.select('value')` | `await locator.selectOption('value')` | |
| `.scrollTo()` | `await locator.scrollIntoViewIfNeeded()` | |
| `.trigger('event')` | `await locator.dispatchEvent('event')` | |
| `.focus()` | `await locator.focus()` | |
| `.blur()` | `await locator.blur()` | |
## Assertions
| Cypress | Playwright | Notes |
|---|---|---|
| `.should('be.visible')` | `await expect(locator).toBeVisible()` | Web-first, auto-retry |
| `.should('not.exist')` | `await expect(locator).not.toBeVisible()` | Or `.toHaveCount(0)` |
| `.should('have.text', 'x')` | `await expect(locator).toHaveText('x')` | |
| `.should('contain', 'x')` | `await expect(locator).toContainText('x')` | |
| `.should('have.value', 'x')` | `await expect(locator).toHaveValue('x')` | |
| `.should('have.attr', 'x', 'y')` | `await expect(locator).toHaveAttribute('x', 'y')` | |
| `.should('have.class', 'x')` | `await expect(locator).toHaveClass(/x/)` | |
| `.should('be.disabled')` | `await expect(locator).toBeDisabled()` | |
| `.should('be.checked')` | `await expect(locator).toBeChecked()` | |
| `.should('have.length', n)` | `await expect(locator).toHaveCount(n)` | |
| `cy.url().should('include', '/x')` | `await expect(page).toHaveURL(/\/x/)` | |
| `cy.title().should('eq', 'x')` | `await expect(page).toHaveTitle('x')` | |
## Network
| Cypress | Playwright |
|---|---|
| `cy.intercept('GET', '/api/*', { body: data })` | `await page.route('**/api/*', route => route.fulfill({ body: JSON.stringify(data) }))` |
| `cy.intercept('POST', '/api/*').as('save')` | `const savePromise = page.waitForResponse('**/api/*')` |
| `cy.wait('@save')` | `await savePromise` |
## Fixtures & Custom Commands
| Cypress | Playwright |
|---|---|
| `cy.fixture('data.json')` | `import data from './test-data/data.json'` |
| `Cypress.Commands.add('login', ...)` | `test.extend({ authenticatedPage: ... })` |
| `beforeEach(() => { ... })` | `test.beforeEach(async ({ page }) => { ... })` |
| `before(() => { ... })` | `test.beforeAll(async () => { ... })` |
## Config
| Cypress | Playwright |
|---|---|
| `baseUrl` in `cypress.config.ts` | `use.baseURL` in `playwright.config.ts` |
| `defaultCommandTimeout` | `expect.timeout` or `use.actionTimeout` |
| `video: true` | `use.video: 'on'` |
| `screenshotOnRunFailure` | `use.screenshot: 'only-on-failure'` |
| `retries: { runMode: 2 }` | `retries: 2` |
FILE:selenium-mapping.md
# Selenium → Playwright Mapping
## Driver Setup
| Selenium (JS) | Playwright |
|---|---|
| `new Builder().forBrowser('chrome').build()` | Handled by config — no driver setup |
| `driver.quit()` | Automatic — Playwright manages browser lifecycle |
| `driver.manage().setTimeouts(...)` | Config: `timeout`, `expect.timeout` |
## Navigation
| Selenium | Playwright | Notes |
|---|---|---|
| `driver.get(url)` | `await page.goto(url)` | Use `baseURL` |
| `driver.navigate().back()` | `await page.goBack()` | |
| `driver.navigate().forward()` | `await page.goForward()` | |
| `driver.navigate().refresh()` | `await page.reload()` | |
| `driver.getCurrentUrl()` | `page.url()` | |
| `driver.getTitle()` | `await page.title()` | |
## Element Location
| Selenium | Playwright | Preferred |
|---|---|---|
| `By.id('x')` | `page.locator('#x')` | `page.getByTestId('x')` |
| `By.css('.x')` | `page.locator('.x')` | `page.getByRole(...)` |
| `By.xpath('//div')` | `page.locator('xpath=//div')` | Avoid — use role-based |
| `By.name('x')` | `page.locator('[name=x]')` | `page.getByLabel(...)` |
| `By.linkText('x')` | `page.getByRole('link', { name: 'x' })` | ✅ Best practice |
| `By.partialLinkText('x')` | `page.getByRole('link', { name: /x/ })` | ✅ Best practice |
| `By.tagName('button')` | `page.getByRole('button')` | ✅ Best practice |
| `By.className('x')` | `page.locator('.x')` | `page.getByRole(...)` |
| `findElement()` | Returns first match | `locator.first()` |
| `findElements()` | `page.locator(selector)` | Use `.count()` or `.all()` |
## Actions
| Selenium | Playwright |
|---|---|
| `element.click()` | `await locator.click()` |
| `element.sendKeys('text')` | `await locator.fill('text')` |
| `element.sendKeys(Key.ENTER)` | `await locator.press('Enter')` |
| `element.clear()` | `await locator.clear()` |
| `element.submit()` | `await locator.press('Enter')` or click submit button |
| `element.getText()` | `await locator.textContent()` |
| `element.getAttribute('x')` | `await locator.getAttribute('x')` |
| `element.isDisplayed()` | `await locator.isVisible()` |
| `element.isEnabled()` | `await locator.isEnabled()` |
| `element.isSelected()` | `await locator.isChecked()` |
## Waits
| Selenium | Playwright | Notes |
|---|---|---|
| `WebDriverWait(driver, 10).until(EC.visibilityOf(el))` | `await expect(locator).toBeVisible()` | Auto-retries |
| `WebDriverWait(driver, 10).until(EC.elementToBeClickable(el))` | `await locator.click()` | Auto-waits for clickable |
| `WebDriverWait(driver, 10).until(EC.presenceOf(el))` | `await expect(locator).toBeAttached()` | |
| `WebDriverWait(driver, 10).until(EC.textToBe(el, 'x'))` | `await expect(locator).toHaveText('x')` | |
| `Thread.sleep(3000)` | ❌ Never use | Use assertions instead |
| `driver.manage().setTimeouts({ implicit: 10000 })` | Not needed | Playwright auto-waits |
## Advanced
| Selenium | Playwright |
|---|---|
| `Actions(driver).moveToElement(el).perform()` | `await locator.hover()` |
| `Actions(driver).dragAndDrop(src, tgt).perform()` | `await src.dragTo(tgt)` |
| `Actions(driver).doubleClick(el).perform()` | `await locator.dblclick()` |
| `Actions(driver).contextClick(el).perform()` | `await locator.click({ button: 'right' })` |
| `driver.switchTo().frame(el)` | `page.frameLocator('#frame')` |
| `driver.switchTo().defaultContent()` | Not needed — use `page` directly |
| `driver.switchTo().alert()` | `page.on('dialog', d => d.accept())` |
| `driver.switchTo().window(handle)` | `const popup = await page.waitForEvent('popup')` |
| `driver.executeScript(js)` | `await page.evaluate(js)` |
| `driver.takeScreenshot()` | `await page.screenshot({ path: 'x.png' })` |
## Test Structure
| Selenium (Jest/Mocha) | Playwright |
|---|---|
| `describe('Suite', () => { ... })` | `test.describe('Suite', () => { ... })` |
| `it('should...', () => { ... })` | `test('should...', async ({ page }) => { ... })` |
| `beforeAll(() => { ... })` | `test.beforeAll(async () => { ... })` |
| `beforeEach(() => { ... })` | `test.beforeEach(async ({ page }) => { ... })` |
| `afterEach(() => { ... })` | `test.afterEach(async ({ page }) => { ... })` |
## Key Differences
1. **No implicit waits** — Playwright auto-waits for actionability
2. **No driver management** — Playwright handles browser lifecycle
3. **Built-in assertions** — `expect(locator)` with auto-retry
4. **Parallel by default** — tests run in parallel, must be isolated
5. **Traces instead of screenshots** — richer debugging artifacts