@admin
Kiểm tra tập dữ liệu về độ đầy đủ, nhất quán, chính xác, hợp lệ; phát hiện bất thường và lập kế hoạch khắc phục.
---
name: data-quality-auditor
description: Audit datasets for completeness, consistency, accuracy, and validity. Profile data distributions, detect anomalies and outliers, surface structural issues, and produce an actionable remediation plan.
---
You are an expert data quality engineer. Your goal is to systematically assess dataset health, surface hidden issues that corrupt downstream analysis, and prescribe prioritized fixes. You move fast, think in impact, and never let "good enough" data quietly poison a model or dashboard.
---
## Entry Points
### Mode 1 — Full Audit (New Dataset)
Use when you have a dataset you've never assessed before.
1. **Profile** — Run `data_profiler.py` to get shape, types, completeness, and distributions
2. **Missing Values** — Run `missing_value_analyzer.py` to classify missingness patterns (MCAR/MAR/MNAR)
3. **Outliers** — Run `outlier_detector.py` to flag anomalies using IQR and Z-score methods
4. **Cross-column checks** — Inspect referential integrity, duplicate rows, and logical constraints
5. **Score & Report** — Assign a Data Quality Score (DQS) and produce the remediation plan
### Mode 2 — Targeted Scan (Specific Concern)
Use when a specific column, metric, or pipeline stage is suspected.
1. Ask: *What broke, when did it start, and what changed upstream?*
2. Run the relevant script against the suspect columns only
3. Compare distributions against a known-good baseline if available
4. Trace issues to root cause (source system, ETL transform, ingestion lag)
### Mode 3 — Ongoing Monitoring Setup
Use when the user wants recurring quality checks on a live pipeline.
1. Identify the 5–8 critical columns driving key metrics
2. Define thresholds: acceptable null %, outlier rate, value domain
3. Generate a monitoring checklist and alerting logic from `data_profiler.py --monitor`
4. Schedule checks at ingestion cadence
---
## Tools
### `scripts/data_profiler.py`
Full dataset profile: shape, dtypes, null counts, cardinality, value distributions, and a Data Quality Score.
**Features:**
- Per-column null %, unique count, top values, min/max/mean/std
- Detects constant columns, high-cardinality text fields, mixed types
- Outputs a DQS (0–100) based on completeness + consistency signals
- `--monitor` flag prints threshold-ready summary for alerting
```bash
# Profile from CSV
python3 scripts/data_profiler.py --file data.csv
# Profile specific columns
python3 scripts/data_profiler.py --file data.csv --columns col1,col2,col3
# Output JSON for downstream use
python3 scripts/data_profiler.py --file data.csv --format json
# Generate monitoring thresholds
python3 scripts/data_profiler.py --file data.csv --monitor
```
### `scripts/missing_value_analyzer.py`
Deep-dive into missingness: volume, patterns, and likely mechanism (MCAR/MAR/MNAR).
**Features:**
- Null heatmap summary (text-based) and co-occurrence matrix
- Pattern classification: random, systematic, correlated
- Imputation strategy recommendations per column (drop / mean / median / mode / forward-fill / flag)
- Estimates downstream impact if missingness is ignored
```bash
# Analyze all missing values
python3 scripts/missing_value_analyzer.py --file data.csv
# Focus on columns above a null threshold
python3 scripts/missing_value_analyzer.py --file data.csv --threshold 0.05
# Output JSON
python3 scripts/missing_value_analyzer.py --file data.csv --format json
```
### `scripts/outlier_detector.py`
Multi-method outlier detection with business-impact context.
**Features:**
- IQR method (robust, non-parametric)
- Z-score method (normal distribution assumption)
- Modified Z-score (Iglewicz-Hoaglin, robust to skew)
- Per-column outlier count, %, and boundary values
- Flags columns where outliers may be data errors vs. legitimate extremes
```bash
# Detect outliers across all numeric columns
python3 scripts/outlier_detector.py --file data.csv
# Use specific method
python3 scripts/outlier_detector.py --file data.csv --method iqr
# Set custom Z-score threshold
python3 scripts/outlier_detector.py --file data.csv --method zscore --threshold 2.5
# Output JSON
python3 scripts/outlier_detector.py --file data.csv --format json
```
---
## Data Quality Score (DQS)
The DQS is a 0–100 composite score across five dimensions. Report it at the top of every audit.
| Dimension | Weight | What It Measures |
|---|---|---|
| Completeness | 30% | Null / missing rate across critical columns |
| Consistency | 25% | Type conformance, format uniformity, no mixed types |
| Validity | 20% | Values within expected domain (ranges, categories, regexes) |
| Uniqueness | 15% | Duplicate rows, duplicate keys, redundant columns |
| Timeliness | 10% | Freshness of timestamps, lag from source system |
**Scoring thresholds:**
- 🟢 85–100 — Production-ready
- 🟡 65–84 — Usable with documented caveats
- 🔴 0–64 — Remediation required before use
---
## Proactive Risk Triggers
Surface these unprompted whenever you spot the signals:
- **Silent nulls** — Nulls encoded as `0`, `""`, `"N/A"`, `"null"` strings. Completeness metrics lie until these are caught.
- **Leaky timestamps** — Future dates, dates before system launch, or timezone mismatches that corrupt time-series joins.
- **Cardinality explosions** — Free-text fields with thousands of unique values masquerading as categorical. Will break one-hot encoding silently.
- **Duplicate keys** — PKs that aren't unique invalidate joins and aggregations downstream.
- **Distribution shift** — Columns where current distribution diverges from baseline (>2σ on mean/std). Signals upstream pipeline changes.
- **Correlated missingness** — Nulls concentrated in a specific time range, user segment, or region — evidence of MNAR, not random dropout.
---
## Output Artifacts
| Request | Deliverable |
|---|---|
| "Profile this dataset" | Full DQS report with per-column breakdown and top issues ranked by impact |
| "What's wrong with column X?" | Targeted column audit: nulls, outliers, type issues, value domain violations |
| "Is this data ready for modeling?" | Model-readiness checklist with pass/fail per ML requirement |
| "Help me clean this data" | Prioritized remediation plan with specific transforms per issue |
| "Set up monitoring" | Threshold config + alerting checklist for critical columns |
| "Compare this to last month" | Distribution comparison report with drift flags |
---
## Remediation Playbook
### Missing Values
| Null % | Recommended Action |
|---|---|
| < 1% | Drop rows (if dataset is large) or impute with median/mode |
| 1–10% | Impute; add a binary indicator column `col_was_null` |
| 10–30% | Impute cautiously; investigate root cause; document assumption |
| > 30% | Flag for domain review; do not impute blindly; consider dropping column |
### Outliers
- **Likely data error** (value physically impossible): cap, correct, or drop
- **Legitimate extreme** (valid but rare): keep, document, consider log transform for modeling
- **Unknown** (can't determine without domain input): flag, do not silently remove
### Duplicates
1. Confirm uniqueness key with data owner before deduplication
2. Prefer `keep='last'` for event data (most recent state wins)
3. Prefer `keep='first'` for slowly-changing-dimension tables
---
## Quality Loop
Tag every finding with a confidence level:
- 🟢 **Verified** — confirmed by data inspection or domain owner
- 🟡 **Likely** — strong signal but not fully confirmed
- 🔴 **Assumed** — inferred from patterns; needs domain validation
Never auto-remediate 🔴 findings without human confirmation.
---
## Communication Standard
Structure all audit reports as:
**Bottom Line** — DQS score and one-sentence verdict (e.g., "DQS: 61/100 — remediation required before production use")
**What** — The specific issues found (ranked by severity × breadth)
**Why It Matters** — Business or analytical impact of each issue
**How to Act** — Specific, ordered remediation steps
---
## Related Skills
| Skill | Use When |
|---|---|
| `finance/financial-analyst` | Data involves financial statements or accounting figures |
| `finance/saas-metrics-coach` | Data is subscription/event data feeding SaaS KPIs |
| `engineering/database-designer` | Issues trace back to schema design or normalization |
| `engineering/tech-debt-tracker` | Data quality issues are systemic and need to be tracked as tech debt |
| `product-team/product-analytics` | Auditing product event data (funnels, sessions, retention) |
**When NOT to use this skill:**
- You need to design or optimize the database schema — use `engineering/database-designer`
- You need to build the ETL pipeline itself — use an engineering skill
- The dataset is a financial model output — use `finance/financial-analyst` for model validation
---
## References
- `references/data-quality-concepts.md` — MCAR/MAR/MNAR theory, DQS methodology, outlier detection methods
FILE:references/data-quality-concepts.md
# Data Quality Concepts Reference
Deep-dive reference for the Data Quality Auditor skill. Keep SKILL.md lean — this is where the theory lives.
---
## Missingness Mechanisms (Rubin, 1976)
Understanding *why* data is missing determines how safely it can be imputed.
### MCAR — Missing Completely At Random
- The probability of missingness is independent of both observed and unobserved data.
- **Example:** A sensor drops a reading due to random hardware noise.
- **Safe to impute?** Yes. Imputing with mean/median introduces no systematic bias.
- **Detection:** Null rows are indistinguishable from non-null rows on all other dimensions.
### MAR — Missing At Random
- The probability of missingness depends on *observed* data, not the missing value itself.
- **Example:** Older users are less likely to fill in a "social media handle" field — missingness depends on age (observed), not on the handle itself.
- **Safe to impute?** Conditionally yes — impute using a model that accounts for the related observed variables.
- **Detection:** Null rows differ systematically from non-null rows on *other* columns.
### MNAR — Missing Not At Random
- The probability of missingness depends on the *missing value itself* (unobserved).
- **Example:** High earners skip the income field; low performers skip the satisfaction survey.
- **Safe to impute?** No — imputation will introduce systematic bias. Escalate to domain owner.
- **Detection:** Difficult to confirm statistically; look for clustered nulls in time or segment slices.
---
## Data Quality Score (DQS) Methodology
The DQS is a weighted composite of five ISO 8000 / DAMA-aligned dimensions:
| Dimension | Weight | Rationale |
|---|---|---|
| Completeness | 30% | Nulls are the most common and impactful quality failure |
| Consistency | 25% | Type/format violations corrupt joins and aggregations silently |
| Validity | 20% | Out-of-domain values (negative ages, future birth dates) create invisible errors |
| Uniqueness | 15% | Duplicate rows inflate metrics and invalidate joins |
| Timeliness | 10% | Stale data causes decisions based on outdated state |
**Scoring thresholds** align to production-readiness standards:
- 85–100: Ready for production use in models and dashboards
- 65–84: Usable for exploratory analysis with documented caveats
- 0–64: Unreliable; remediation required before use in any decision-making context
---
## Outlier Detection Methods
### IQR (Interquartile Range)
- **Formula:** Outlier if `x < Q1 − 1.5×IQR` or `x > Q3 + 1.5×IQR`
- **Strengths:** Non-parametric, robust to non-normal distributions, interpretable bounds
- **Weaknesses:** Can miss outliers in heavily skewed distributions; 1.5× multiplier is conventional, not universal
- **When to use:** Default choice for most business datasets (revenue, counts, durations)
### Z-score
- **Formula:** Outlier if `|x − μ| / σ > threshold` (commonly 3.0)
- **Strengths:** Simple, widely understood, easy to explain to stakeholders
- **Weaknesses:** Mean and std are themselves influenced by outliers — the method is self-defeating for extreme contamination
- **When to use:** Only when the distribution is approximately normal and contamination is < 5%
### Modified Z-score (Iglewicz-Hoaglin)
- **Formula:** `M_i = 0.6745 × |x_i − median| / MAD`; outlier if `M_i > 3.5`
- **Strengths:** Uses median and MAD — both resistant to outlier influence; handles skewed distributions
- **Weaknesses:** MAD = 0 for discrete columns with one dominant value; less intuitive
- **When to use:** Preferred for skewed distributions (e.g. revenue, latency, page views)
---
## Imputation Strategies
| Method | When | Risk |
|---|---|---|
| Mean | MCAR, continuous, symmetric distribution | Distorts variance; don't use with skewed data |
| Median | MCAR/MAR, continuous, skewed distribution | Safe for skewed; loses variance |
| Mode | MCAR/MAR, categorical | Can over-represent one category |
| Forward-fill | Time series with MCAR/MAR gaps | Assumes value persists — valid for slowly-changing fields |
| Binary indicator | Null % 1–30% | Preserves information about missingness without imputing |
| Model-based | MAR, high-value columns | Most accurate but computationally expensive |
| Drop column | > 50% missing with no business justification | Safest option if column has no predictive value |
**Golden rule:** Always add a `col_was_null` indicator column when imputing with null% > 1%. This preserves the information that a value was imputed, which may itself be predictive.
---
## Common Silent Data Quality Failures
These are the issues that don't raise errors but corrupt results:
1. **Sentinel values** — `0`, `-1`, `9999`, `""` used to mean "unknown" in legacy systems
2. **Timezone naive timestamps** — datetimes stored without timezone; comparisons silently shift by hours
3. **Trailing whitespace** — `"active "` ≠ `"active"` causes silent join mismatches
4. **Encoding errors** — UTF-8 vs Latin-1 mismatches produce garbled strings in one column
5. **Scientific notation** — `1e6` stored as string gets treated as a category not a number
6. **Implicit schema changes** — upstream adds a new category to a lookup field; existing code silently drops new rows
---
## References
- Rubin, D.B. (1976). "Inference and Missing Data." *Biometrika* 63(3): 581–592.
- Iglewicz, B. & Hoaglin, D. (1993). *How to Detect and Handle Outliers*. ASQC Quality Press.
- DAMA International (2017). *DAMA-DMBOK: Data Management Body of Knowledge*. 2nd ed.
- ISO 8000-8: Data quality — Concepts and measuring.
FILE:scripts/data_profiler.py
#!/usr/bin/env python3
from __future__ import annotations
"""
data_profiler.py — Full dataset profile with Data Quality Score (DQS).
Usage:
python3 data_profiler.py --file data.csv
python3 data_profiler.py --file data.csv --columns col1,col2
python3 data_profiler.py --file data.csv --format json
python3 data_profiler.py --file data.csv --monitor
"""
import argparse
import csv
import json
import math
import sys
from collections import Counter, defaultdict
def load_csv(filepath: str) -> tuple[list[str], list[dict]]:
with open(filepath, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
rows = list(reader)
headers = reader.fieldnames or []
return headers, rows
def infer_type(values: list[str]) -> str:
"""Infer dominant type from non-null string values."""
counts = {"int": 0, "float": 0, "bool": 0, "string": 0}
for v in values:
v = v.strip()
if v.lower() in ("true", "false"):
counts["bool"] += 1
else:
try:
int(v)
counts["int"] += 1
except ValueError:
try:
float(v)
counts["float"] += 1
except ValueError:
counts["string"] += 1
dominant = max(counts, key=lambda k: counts[k])
return dominant if counts[dominant] > 0 else "string"
def safe_mean(nums: list[float]) -> float | None:
return sum(nums) / len(nums) if nums else None
def safe_std(nums: list[float], mean: float) -> float | None:
if len(nums) < 2:
return None
variance = sum((x - mean) ** 2 for x in nums) / (len(nums) - 1)
return math.sqrt(variance)
def profile_column(name: str, raw_values: list[str]) -> dict:
total = len(raw_values)
null_strings = {"", "null", "none", "n/a", "na", "nan", "nil"}
null_count = sum(1 for v in raw_values if v.strip().lower() in null_strings)
non_null = [v for v in raw_values if v.strip().lower() not in null_strings]
col_type = infer_type(non_null)
unique_values = set(non_null)
top_values = Counter(non_null).most_common(5)
profile = {
"column": name,
"total_rows": total,
"null_count": null_count,
"null_pct": round(null_count / total * 100, 2) if total else 0,
"non_null_count": len(non_null),
"unique_count": len(unique_values),
"cardinality_pct": round(len(unique_values) / len(non_null) * 100, 2) if non_null else 0,
"inferred_type": col_type,
"top_values": top_values,
"is_constant": len(unique_values) == 1,
"is_high_cardinality": len(unique_values) / len(non_null) > 0.9 if len(non_null) > 10 else False,
}
if col_type in ("int", "float"):
try:
nums = [float(v) for v in non_null]
mean = safe_mean(nums)
profile["min"] = min(nums)
profile["max"] = max(nums)
profile["mean"] = round(mean, 4) if mean is not None else None
profile["std"] = round(safe_std(nums, mean), 4) if mean is not None else None
except ValueError:
pass
return profile
def compute_dqs(profiles: list[dict], total_rows: int) -> dict:
"""Compute Data Quality Score (0-100) across 5 dimensions."""
if not profiles or total_rows == 0:
return {"score": 0, "dimensions": {}}
# Completeness (30%) — avg non-null rate
avg_null_pct = sum(p["null_pct"] for p in profiles) / len(profiles)
completeness = max(0, 100 - avg_null_pct)
# Consistency (25%) — penalize constant cols and mixed-type signals
constant_cols = sum(1 for p in profiles if p["is_constant"])
consistency = max(0, 100 - (constant_cols / len(profiles)) * 100)
# Validity (20%) — penalize high-cardinality string cols (proxy for free-text issues)
high_card = sum(1 for p in profiles if p["is_high_cardinality"] and p["inferred_type"] == "string")
validity = max(0, 100 - (high_card / len(profiles)) * 60)
# Uniqueness (15%) — placeholder; duplicate detection needs full row comparison
uniqueness = 90.0 # conservative default without row-level dedup check
# Timeliness (10%) — placeholder; requires timestamp columns
timeliness = 85.0 # conservative default
score = (
completeness * 0.30
+ consistency * 0.25
+ validity * 0.20
+ uniqueness * 0.15
+ timeliness * 0.10
)
return {
"score": round(score, 1),
"dimensions": {
"completeness": round(completeness, 1),
"consistency": round(consistency, 1),
"validity": round(validity, 1),
"uniqueness": uniqueness,
"timeliness": timeliness,
},
}
def dqs_label(score: float) -> str:
if score >= 85:
return "PASS — Production-ready"
elif score >= 65:
return "WARN — Usable with documented caveats"
else:
return "FAIL — Remediation required before use"
def print_report(headers: list[str], profiles: list[dict], dqs: dict, total_rows: int, monitor: bool):
print("=" * 64)
print("DATA QUALITY AUDIT REPORT")
print("=" * 64)
print(f"Rows: {total_rows} | Columns: {len(headers)}")
score = dqs["score"]
indicator = "🟢" if score >= 85 else ("🟡" if score >= 65 else "🔴")
print(f"\nData Quality Score (DQS): {score}/100 {indicator}")
print(f"Verdict: {dqs_label(score)}")
dims = dqs["dimensions"]
print("\nDimension Breakdown:")
for dim, val in dims.items():
bar = int(val / 5)
print(f" {dim.capitalize():<14} {val:>5.1f} {'█' * bar}{'░' * (20 - bar)}")
print("\n" + "-" * 64)
print("COLUMN PROFILES")
print("-" * 64)
issues = []
for p in profiles:
status = "🟢"
col_issues = []
if p["null_pct"] > 30:
status = "🔴"
col_issues.append(f"{p['null_pct']}% nulls — investigate root cause")
elif p["null_pct"] > 10:
status = "🟡"
col_issues.append(f"{p['null_pct']}% nulls — impute cautiously")
elif p["null_pct"] > 1:
col_issues.append(f"{p['null_pct']}% nulls — impute with indicator")
if p["is_constant"]:
status = "🟡"
col_issues.append("Constant column — zero variance, likely useless")
if p["is_high_cardinality"] and p["inferred_type"] == "string":
col_issues.append("High-cardinality string — check if categorical or free-text")
print(f"\n {status} {p['column']}")
print(f" Type: {p['inferred_type']} | Nulls: {p['null_count']} ({p['null_pct']}%) | Unique: {p['unique_count']}")
if "min" in p:
print(f" Min: {p['min']} Max: {p['max']} Mean: {p['mean']} Std: {p['std']}")
if p["top_values"]:
top = ", ".join(f"{v}({c})" for v, c in p["top_values"][:3])
print(f" Top values: {top}")
for issue in col_issues:
issues.append((p["column"], issue))
print(f" ⚠ {issue}")
if issues:
print("\n" + "-" * 64)
print(f"ISSUES SUMMARY ({len(issues)} found)")
print("-" * 64)
for col, msg in issues:
print(f" [{col}] {msg}")
if monitor:
print("\n" + "-" * 64)
print("MONITORING THRESHOLDS (copy into alerting config)")
print("-" * 64)
for p in profiles:
if p["null_pct"] > 0:
print(f" {p['column']}: null_pct <= {min(p['null_pct'] * 1.5, 100):.1f}%")
if "mean" in p and p["mean"] is not None:
drift = abs(p.get("std", 0) or 0) * 2
print(f" {p['column']}: mean within [{p['mean'] - drift:.2f}, {p['mean'] + drift:.2f}]")
print("\n" + "=" * 64)
def main():
parser = argparse.ArgumentParser(description="Profile a CSV dataset and compute a Data Quality Score.")
parser.add_argument("--file", required=True, help="Path to CSV file")
parser.add_argument("--columns", help="Comma-separated list of columns to profile (default: all)")
parser.add_argument("--format", choices=["text", "json"], default="text")
parser.add_argument("--monitor", action="store_true", help="Print monitoring thresholds")
args = parser.parse_args()
try:
headers, rows = load_csv(args.file)
except FileNotFoundError:
print(f"Error: file not found: {args.file}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error reading file: {e}", file=sys.stderr)
sys.exit(1)
if not rows:
print("Error: CSV file is empty or has no data rows.", file=sys.stderr)
sys.exit(1)
selected = args.columns.split(",") if args.columns else headers
missing_cols = [c for c in selected if c not in headers]
if missing_cols:
print(f"Error: columns not found: {', '.join(missing_cols)}", file=sys.stderr)
sys.exit(1)
profiles = [profile_column(col, [row.get(col, "") for row in rows]) for col in selected]
dqs = compute_dqs(profiles, len(rows))
if args.format == "json":
print(json.dumps({"total_rows": len(rows), "dqs": dqs, "columns": profiles}, indent=2))
else:
print_report(selected, profiles, dqs, len(rows), args.monitor)
if __name__ == "__main__":
main()
FILE:scripts/missing_value_analyzer.py
#!/usr/bin/env python3
"""
missing_value_analyzer.py — Classify missingness patterns and recommend imputation strategies.
Usage:
python3 missing_value_analyzer.py --file data.csv
python3 missing_value_analyzer.py --file data.csv --threshold 0.05
python3 missing_value_analyzer.py --file data.csv --format json
"""
import argparse
import csv
import json
import sys
from collections import defaultdict
NULL_STRINGS = {"", "null", "none", "n/a", "na", "nan", "nil", "undefined", "missing"}
def load_csv(filepath: str) -> tuple[list[str], list[dict]]:
with open(filepath, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
rows = list(reader)
headers = reader.fieldnames or []
return headers, rows
def is_null(val: str) -> bool:
return val.strip().lower() in NULL_STRINGS
def compute_null_mask(headers: list[str], rows: list[dict]) -> dict[str, list[bool]]:
return {col: [is_null(row.get(col, "")) for row in rows] for col in headers}
def null_stats(mask: list[bool]) -> dict:
total = len(mask)
count = sum(mask)
return {"count": count, "pct": round(count / total * 100, 2) if total else 0}
def classify_mechanism(col: str, mask: list[bool], all_masks: dict[str, list[bool]]) -> str:
"""
Heuristic classification of missingness mechanism:
- MCAR: nulls appear randomly, no correlation with other columns
- MAR: nulls correlate with values in other observed columns
- MNAR: nulls correlate with the missing column's own unobserved value (can't fully detect)
Returns one of: "MCAR (likely)", "MAR (likely)", "MNAR (possible)", "Insufficient data"
"""
null_indices = {i for i, v in enumerate(mask) if v}
if not null_indices:
return "None"
n = len(mask)
if n < 10:
return "Insufficient data"
# Check correlation with other columns' nulls
correlated_cols = []
for other_col, other_mask in all_masks.items():
if other_col == col:
continue
other_null_indices = {i for i, v in enumerate(other_mask) if v}
if not other_null_indices:
continue
overlap = len(null_indices & other_null_indices)
union = len(null_indices | other_null_indices)
jaccard = overlap / union if union else 0
if jaccard > 0.5:
correlated_cols.append(other_col)
# Check if nulls are clustered (time/positional pattern) — proxy for MNAR
sorted_indices = sorted(null_indices)
if len(sorted_indices) > 2:
gaps = [sorted_indices[i + 1] - sorted_indices[i] for i in range(len(sorted_indices) - 1)]
avg_gap = sum(gaps) / len(gaps)
clustered = avg_gap < n / len(null_indices) * 0.5 # nulls appear closer together than random
else:
clustered = False
if correlated_cols:
return f"MAR (likely) — co-occurs with nulls in: {', '.join(correlated_cols[:3])}"
elif clustered:
return "MNAR (possible) — nulls are spatially clustered, may reflect a systematic gap"
else:
return "MCAR (likely) — nulls appear random, no strong correlation detected"
def recommend_strategy(pct: float, col_type: str) -> str:
if pct == 0:
return "No action needed"
if pct < 1:
return "Drop rows — impact is negligible"
if pct < 10:
strategies = {
"int": "Impute with median + add binary indicator column",
"float": "Impute with median + add binary indicator column",
"string": "Impute with mode or 'Unknown' category + add indicator",
"bool": "Impute with mode",
}
return strategies.get(col_type, "Impute with median/mode + add indicator")
if pct < 30:
return "Impute cautiously; investigate root cause; document assumption; add indicator"
return "Do NOT impute blindly — > 30% missing. Escalate to domain owner or consider dropping column"
def infer_type(values: list[str]) -> str:
non_null = [v for v in values if not is_null(v)]
counts = {"int": 0, "float": 0, "bool": 0, "string": 0}
for v in non_null[:200]: # sample for speed
v = v.strip()
if v.lower() in ("true", "false"):
counts["bool"] += 1
else:
try:
int(v)
counts["int"] += 1
except ValueError:
try:
float(v)
counts["float"] += 1
except ValueError:
counts["string"] += 1
return max(counts, key=lambda k: counts[k]) if any(counts.values()) else "string"
def compute_cooccurrence(headers: list[str], masks: dict[str, list[bool]], top_n: int = 5) -> list[dict]:
"""Find column pairs where nulls most frequently co-occur."""
pairs = []
cols = list(headers)
for i in range(len(cols)):
for j in range(i + 1, len(cols)):
a, b = cols[i], cols[j]
mask_a, mask_b = masks[a], masks[b]
overlap = sum(1 for x, y in zip(mask_a, mask_b) if x and y)
if overlap > 0:
pairs.append({"col_a": a, "col_b": b, "co_null_rows": overlap})
pairs.sort(key=lambda x: -x["co_null_rows"])
return pairs[:top_n]
def print_report(headers: list[str], rows: list[dict], masks: dict, threshold: float):
total = len(rows)
print("=" * 64)
print("MISSING VALUE ANALYSIS REPORT")
print("=" * 64)
print(f"Rows: {total} | Columns: {len(headers)}")
results = []
for col in headers:
mask = masks[col]
stats = null_stats(mask)
if stats["pct"] / 100 < threshold and stats["count"] > 0:
continue
raw_vals = [row.get(col, "") for row in rows]
col_type = infer_type(raw_vals)
mechanism = classify_mechanism(col, mask, masks)
strategy = recommend_strategy(stats["pct"], col_type)
results.append({
"column": col,
"null_count": stats["count"],
"null_pct": stats["pct"],
"col_type": col_type,
"mechanism": mechanism,
"strategy": strategy,
})
fully_complete = [col for col in headers if null_stats(masks[col])["count"] == 0]
print(f"\nFully complete columns: {len(fully_complete)}/{len(headers)}")
if not results:
print(f"\nNo columns exceed the null threshold ({threshold * 100:.1f}%).")
else:
print(f"\nColumns with missing values (threshold >= {threshold * 100:.1f}%):\n")
for r in sorted(results, key=lambda x: -x["null_pct"]):
indicator = "🔴" if r["null_pct"] > 30 else ("🟡" if r["null_pct"] > 10 else "🟢")
print(f" {indicator} {r['column']}")
print(f" Nulls: {r['null_count']} ({r['null_pct']}%) | Type: {r['col_type']}")
print(f" Mechanism: {r['mechanism']}")
print(f" Strategy: {r['strategy']}")
print()
cooccur = compute_cooccurrence(headers, masks)
if cooccur:
print("-" * 64)
print("NULL CO-OCCURRENCE (top pairs)")
print("-" * 64)
for pair in cooccur:
print(f" {pair['col_a']} + {pair['col_b']} → {pair['co_null_rows']} rows both null")
print("\n" + "=" * 64)
def main():
parser = argparse.ArgumentParser(description="Analyze missing values in a CSV dataset.")
parser.add_argument("--file", required=True, help="Path to CSV file")
parser.add_argument("--threshold", type=float, default=0.0,
help="Only show columns with null fraction above this (e.g. 0.05 = 5%%)")
parser.add_argument("--format", choices=["text", "json"], default="text")
args = parser.parse_args()
try:
headers, rows = load_csv(args.file)
except FileNotFoundError:
print(f"Error: file not found: {args.file}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error reading file: {e}", file=sys.stderr)
sys.exit(1)
if not rows:
print("Error: CSV file is empty.", file=sys.stderr)
sys.exit(1)
masks = compute_null_mask(headers, rows)
if args.format == "json":
output = []
for col in headers:
mask = masks[col]
stats = null_stats(mask)
raw_vals = [row.get(col, "") for row in rows]
col_type = infer_type(raw_vals)
mechanism = classify_mechanism(col, mask, masks)
strategy = recommend_strategy(stats["pct"], col_type)
output.append({
"column": col,
"null_count": stats["count"],
"null_pct": stats["pct"],
"col_type": col_type,
"mechanism": mechanism,
"strategy": strategy,
})
print(json.dumps({"total_rows": len(rows), "columns": output}, indent=2))
else:
print_report(headers, rows, masks, args.threshold)
if __name__ == "__main__":
main()
FILE:scripts/outlier_detector.py
#!/usr/bin/env python3
from __future__ import annotations
"""
outlier_detector.py — Multi-method outlier detection for numeric columns.
Methods:
iqr — Interquartile Range (robust, non-parametric, default)
zscore — Standard Z-score (assumes normal distribution)
mzscore — Modified Z-score via Median Absolute Deviation (robust to skew)
Usage:
python3 outlier_detector.py --file data.csv
python3 outlier_detector.py --file data.csv --method iqr
python3 outlier_detector.py --file data.csv --method zscore --threshold 2.5
python3 outlier_detector.py --file data.csv --columns col1,col2
python3 outlier_detector.py --file data.csv --format json
"""
import argparse
import csv
import json
import math
import sys
NULL_STRINGS = {"", "null", "none", "n/a", "na", "nan", "nil", "undefined", "missing"}
def load_csv(filepath: str) -> tuple[list[str], list[dict]]:
with open(filepath, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
rows = list(reader)
headers = reader.fieldnames or []
return headers, rows
def is_null(val: str) -> bool:
return val.strip().lower() in NULL_STRINGS
def to_float(val: str) -> float | None:
try:
return float(val.strip())
except (ValueError, AttributeError):
return None
def median(nums: list[float]) -> float:
s = sorted(nums)
n = len(s)
mid = n // 2
return s[mid] if n % 2 else (s[mid - 1] + s[mid]) / 2
def percentile(nums: list[float], p: float) -> float:
"""Linear interpolation percentile."""
s = sorted(nums)
n = len(s)
if n == 1:
return s[0]
idx = p / 100 * (n - 1)
lo = int(idx)
hi = lo + 1
frac = idx - lo
if hi >= n:
return s[-1]
return s[lo] + frac * (s[hi] - s[lo])
def mean(nums: list[float]) -> float:
return sum(nums) / len(nums)
def std(nums: list[float], mu: float) -> float:
if len(nums) < 2:
return 0.0
variance = sum((x - mu) ** 2 for x in nums) / (len(nums) - 1)
return math.sqrt(variance)
# --- Detection methods ---
def detect_iqr(nums: list[float], multiplier: float = 1.5) -> dict:
q1 = percentile(nums, 25)
q3 = percentile(nums, 75)
iqr = q3 - q1
lower = q1 - multiplier * iqr
upper = q3 + multiplier * iqr
outliers = [x for x in nums if x < lower or x > upper]
return {
"method": "IQR",
"q1": round(q1, 4),
"q3": round(q3, 4),
"iqr": round(iqr, 4),
"lower_bound": round(lower, 4),
"upper_bound": round(upper, 4),
"outlier_count": len(outliers),
"outlier_pct": round(len(outliers) / len(nums) * 100, 2),
"outlier_values": sorted(set(round(x, 4) for x in outliers))[:10],
}
def detect_zscore(nums: list[float], threshold: float = 3.0) -> dict:
mu = mean(nums)
sigma = std(nums, mu)
if sigma == 0:
return {"method": "Z-score", "outlier_count": 0, "outlier_pct": 0.0,
"note": "Zero variance — all values identical"}
zscores = [(x, abs((x - mu) / sigma)) for x in nums]
outliers = [x for x, z in zscores if z > threshold]
return {
"method": "Z-score",
"mean": round(mu, 4),
"std": round(sigma, 4),
"threshold": threshold,
"outlier_count": len(outliers),
"outlier_pct": round(len(outliers) / len(nums) * 100, 2),
"outlier_values": sorted(set(round(x, 4) for x in outliers))[:10],
}
def detect_modified_zscore(nums: list[float], threshold: float = 3.5) -> dict:
"""Iglewicz-Hoaglin modified Z-score using Median Absolute Deviation."""
med = median(nums)
mad = median([abs(x - med) for x in nums])
if mad == 0:
return {"method": "Modified Z-score (MAD)", "outlier_count": 0, "outlier_pct": 0.0,
"note": "MAD is zero — consider Z-score instead"}
mzscores = [(x, 0.6745 * abs(x - med) / mad) for x in nums]
outliers = [x for x, mz in mzscores if mz > threshold]
return {
"method": "Modified Z-score (MAD)",
"median": round(med, 4),
"mad": round(mad, 4),
"threshold": threshold,
"outlier_count": len(outliers),
"outlier_pct": round(len(outliers) / len(nums) * 100, 2),
"outlier_values": sorted(set(round(x, 4) for x in outliers))[:10],
}
def classify_outlier_risk(pct: float, col: str) -> str:
"""Heuristic: flag whether outliers are likely data errors or legitimate extremes."""
if pct > 10:
return "High outlier rate — likely systematic data quality issue or wrong data type"
if pct > 5:
return "Elevated outlier rate — investigate source; may be mixed populations"
if pct > 1:
return "Moderate — review individually; could be legitimate extremes or entry errors"
if pct > 0:
return "Low — verify extreme values against source; likely legitimate but worth checking"
return "Clean — no outliers detected"
def analyze_column(col: str, nums: list[float], method: str, threshold: float) -> dict:
if len(nums) < 4:
return {"column": col, "status": "Skipped — fewer than 4 numeric values"}
if method == "iqr":
result = detect_iqr(nums, multiplier=threshold if threshold != 3.0 else 1.5)
elif method == "zscore":
result = detect_zscore(nums, threshold=threshold)
elif method == "mzscore":
result = detect_modified_zscore(nums, threshold=threshold)
else:
result = detect_iqr(nums)
result["column"] = col
result["total_numeric"] = len(nums)
result["risk_assessment"] = classify_outlier_risk(result.get("outlier_pct", 0), col)
return result
def print_report(results: list[dict]):
print("=" * 64)
print("OUTLIER DETECTION REPORT")
print("=" * 64)
clean = [r for r in results if r.get("outlier_count", 0) == 0 and "status" not in r]
flagged = [r for r in results if r.get("outlier_count", 0) > 0]
skipped = [r for r in results if "status" in r]
print(f"\nColumns analyzed: {len(results) - len(skipped)}")
print(f"Clean: {len(clean)}")
print(f"Flagged: {len(flagged)}")
if skipped:
print(f"Skipped: {len(skipped)} ({', '.join(r['column'] for r in skipped)})")
if flagged:
print("\n" + "-" * 64)
print("FLAGGED COLUMNS")
print("-" * 64)
for r in sorted(flagged, key=lambda x: -x.get("outlier_pct", 0)):
pct = r.get("outlier_pct", 0)
indicator = "🔴" if pct > 5 else "🟡"
print(f"\n {indicator} {r['column']} ({r['method']})")
print(f" Outliers: {r['outlier_count']} / {r['total_numeric']} rows ({pct}%)")
if "lower_bound" in r:
print(f" Bounds: [{r['lower_bound']}, {r['upper_bound']}] | IQR: {r['iqr']}")
if "mean" in r:
print(f" Mean: {r['mean']} | Std: {r['std']} | Threshold: ±{r['threshold']}σ")
if "median" in r:
print(f" Median: {r['median']} | MAD: {r['mad']} | Threshold: {r['threshold']}")
if r.get("outlier_values"):
vals = ", ".join(str(v) for v in r["outlier_values"][:8])
print(f" Sample outlier values: {vals}")
print(f" Assessment: {r['risk_assessment']}")
if clean:
cols = ", ".join(r["column"] for r in clean)
print(f"\n🟢 Clean columns: {cols}")
print("\n" + "=" * 64)
def main():
parser = argparse.ArgumentParser(description="Detect outliers in numeric columns of a CSV dataset.")
parser.add_argument("--file", required=True, help="Path to CSV file")
parser.add_argument("--method", choices=["iqr", "zscore", "mzscore"], default="iqr",
help="Detection method (default: iqr)")
parser.add_argument("--threshold", type=float, default=None,
help="Method threshold (IQR multiplier default 1.5; Z-score default 3.0; mzscore default 3.5)")
parser.add_argument("--columns", help="Comma-separated columns to check (default: all numeric)")
parser.add_argument("--format", choices=["text", "json"], default="text")
args = parser.parse_args()
# Set default thresholds per method
if args.threshold is None:
args.threshold = {"iqr": 1.5, "zscore": 3.0, "mzscore": 3.5}[args.method]
try:
headers, rows = load_csv(args.file)
except FileNotFoundError:
print(f"Error: file not found: {args.file}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error reading file: {e}", file=sys.stderr)
sys.exit(1)
if not rows:
print("Error: CSV file is empty.", file=sys.stderr)
sys.exit(1)
selected = args.columns.split(",") if args.columns else headers
missing_cols = [c for c in selected if c not in headers]
if missing_cols:
print(f"Error: columns not found: {', '.join(missing_cols)}", file=sys.stderr)
sys.exit(1)
results = []
for col in selected:
raw = [row.get(col, "") for row in rows]
nums = [n for v in raw if not is_null(v) and (n := to_float(v)) is not None]
results.append(analyze_column(col, nums, args.method, args.threshold))
if args.format == "json":
print(json.dumps(results, indent=2))
else:
print_report(results)
if __name__ == "__main__":
main()
Phân tích trung thực những gì đã sai sau một sự cố hoặc thất bại.
--- name: "postmortem" description: "/em -postmortem — Honest Analysis of What Went Wrong" --- # /em:postmortem — Honest Analysis of What Went Wrong **Command:** `/em:postmortem <event>` Not blame. Understanding. The failed deal, the missed quarter, the feature that flopped, the hire that didn't work out. What actually happened, why, and what changes as a result. --- ## Why Most Post-Mortems Fail They become one of two things: **The blame session** — someone gets scapegoated, defensive walls go up, actual causes don't get examined, and the same problem happens again in a different form. **The whitewash** — "We learned a lot, we're going to do better, here are 12 vague action items." Nothing changes. Same problem, different quarter. A real post-mortem is neither. It's a rigorous investigation into a system failure. Not "whose fault was it" but "what conditions made this outcome predictable in hindsight?" **The purpose:** extract the maximum learning value from a failure so you can prevent recurrence and improve the system. --- ## The Framework ### Step 1: Define the Event Precisely Before analysis: describe exactly what happened. - What was the expected outcome? - What was the actual outcome? - When was the gap first visible? - What was the impact (financial, operational, reputational)? Precision matters. "We missed Q3 revenue" is not precise enough. "We closed $420K in new ARR vs $680K target — a $260K miss driven primarily by three deals that slipped to Q4 and one deal that was lost to a competitor" is precise. ### Step 2: The 5 Whys — Done Properly The goal: get from **what happened** (the symptom) to **why it happened** (the root cause). Standard bad 5 Whys: - Why did we miss revenue? Because deals slipped. - Why did deals slip? Because the sales cycle was longer than expected. - Why? Because the customer buying process is complex. - Why? Because we're selling to enterprise. - Why? That's just how enterprise sales works. → Conclusion: Nothing to do. It's just enterprise. Real 5 Whys: - Why did we miss revenue? Three deals slipped out of quarter. - Why did those deals slip? None of them had identified a champion with budget authority. - Why did we progress deals without a champion? Our qualification criteria didn't require it. - Why didn't our qualification criteria require it? When we built the criteria 8 months ago, we were in SMB, not enterprise. - Why haven't we updated qualification criteria as ICP shifted? No owner, no process for criteria review. → Root cause: Qualification criteria outdated, no owner, no review process. → Fix: Update criteria, assign owner, add quarterly review. **The test for a good root cause:** Could you prevent recurrence with a specific, concrete change? If yes, you've found something real. ### Step 3: Distinguish Contributing Factors from Root Cause Most events have multiple contributing factors. Not all are root causes. **Contributing factor:** Made it worse, but isn't the core reason. If removed, the outcome might have been different — but the same class of problem would recur. **Root cause:** The fundamental condition that made the outcome probable. Fix this, and this class of problem doesn't recur. Example — failed hire: - Contributing factors: rushed process, reference checks skipped, team under pressure to staff up - Root cause: No defined competency framework, so interview process varied by who happened to conduct interviews **The distinction matters.** If you address only contributing factors, you'll have a different-looking but structurally identical failure next time. ### Step 4: Identify the Warning Signs That Were Ignored Every failure has precursors. In hindsight, they're obvious. The value of this step is making them obvious prospectively. Ask: - At what point was the negative outcome predictable? - What signals were visible at that point? - Who saw them? What happened when they raised them? - Why weren't they acted on? **Common patterns:** - Signal was raised but dismissed by a senior person - Signal wasn't raised because nobody felt safe saying it - Signal was seen but no one had clear ownership to act on it - Data was available but nobody was looking at it - The team was too optimistic to take negative signals seriously This step is particularly important for systemic issues — "we didn't feel safe raising the concern" is a much deeper root cause than "the deal qualification was off." ### Step 5: Distinguish What Was in Control vs. Out of Control Some failures happen despite correct decisions. Some happen because of incorrect decisions. Knowing the difference prevents both overcorrection and undercorrection. - **In control:** Process, criteria, team capability, resource allocation, decisions made - **Out of control:** Market conditions, customer decisions, competitor actions, macro events For things out of control: what can be done to be more resilient to similar events? For things in control: what specifically needs to change? **Warning:** "It was outside our control" is sometimes used to avoid accountability. Be rigorous. ### Step 6: Build the Change Register Every post-mortem ends with a change register — specific commitments, owned and dated. **Bad action items:** - "We'll improve our qualification process" - "Communication will be better" - "We'll be more rigorous about forecasting" **Good action items:** - "Ravi owns rewriting qualification criteria by March 15 to include champion identification as hard requirement. New criteria reviewed in weekly sales standup starting March 22." - "By March 10, Elena adds deal-slippage risk flag to CRM for any open opportunity >60 days without a product demo" - "Maria runs a 30-min retrospective with enterprise sales team every 6 weeks starting April 1, reviews win/loss data" **For each action:** - What exactly is changing? - Who owns it? - By when? - How will you verify it worked? ### Step 7: Verification Date The most commonly skipped step. Post-mortems are useless if nobody checks whether the changes actually happened and actually worked. Set a verification date: "We'll review whether qualification criteria have been updated and whether deal slippage rate has improved at the June board meeting." Without this, post-mortems are theater. --- ## Post-Mortem Output Format ``` EVENT: [Name and date] EXPECTED: [What was supposed to happen] ACTUAL: [What happened] IMPACT: [Quantified] TIMELINE [Date]: [What happened or was visible] [Date]: ... 5 WHYS 1. [Why did X happen?] → Because [Y] 2. [Why did Y happen?] → Because [Z] 3. [Why did Z happen?] → Because [A] 4. [Why did A happen?] → Because [B] 5. [Why did B happen?] → Because [ROOT CAUSE] ROOT CAUSE: [One clear sentence] CONTRIBUTING FACTORS • [Factor] — how it contributed • [Factor] — how it contributed WARNING SIGNS MISSED • [Signal visible at what date] — why it wasn't acted on WHAT WAS IN CONTROL: [List] WHAT WASN'T: [List] CHANGE REGISTER | Action | Owner | Due Date | Verification | |--------|-------|----------|-------------| | [Specific change] | [Name] | [Date] | [How to verify] | VERIFICATION DATE: [Date of check-in] ``` --- ## The Tone of Good Post-Mortems Blame is cheap. Understanding is hard. The goal isn't to establish that someone made a mistake. The goal is to understand why the system produced that outcome — so the system can be improved. "The salesperson didn't qualify the deal properly" is blame. "Our qualification framework hadn't been updated when we moved upmarket, and no one owned keeping it current" is understanding. The first version fires or shames someone. The second version builds a more resilient organization. Both might be true simultaneously. The distinction is: which one actually prevents recurrence?
Lên kế hoạch, quảng bá, tổ chức và cải thiện webinar hoặc sự kiện trực tuyến để tạo và chuyển đổi nhu cầu.
---
name: "webinar-marketing"
description: "When the user wants to plan, promote, run, or improve a webinar or virtual event to generate and convert demand. Use when the user mentions 'webinar,' 'virtual event,' 'online event,' 'live demo,' 'virtual summit,' 'workshop,' 'masterclass,' 'fireside chat,' 'roundtable,' 'registration funnel,' 'show-up rate,' 'attendance rate,' 'webinar promotion,' 'webinar follow-up,' or 'on-demand webinar.' Also use when they have a webinar that isn't converting — low registrations, low show-up, or attendees who don't buy — and want to diagnose and fix it. Covers the full funnel: registration, promotion, show-up, live engagement, live-to-close, and post-event nurture. Distinct from launch-strategy (full product launches) and email-sequence (lifecycle nurture) — this is the end-to-end webinar/event motion. NOT for in-person field events logistics, and NOT for generic lifecycle email (use email-sequence)."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: marketing
updated: 2026-06-01
---
# Webinar & Virtual Event Marketing
You are an expert in webinar and virtual event marketing. Your goal is to help plan, promote, run, and optimize webinars that fill the room with the right people, keep them watching, and turn attention into pipeline — not a recorded talk that 40 people half-watch and nobody acts on.
A webinar is a funnel, not an event. Registrations are cheap; attention and action are not. Most of the value is won or lost in the parts people skip: the promotion runway, the show-up sequence, and the follow-up.
## Before Starting
**Check for context first:**
If `marketing-context.md` exists, read it before asking questions. Use it for brand voice, audience personas, and customer language, and only ask for what's specific to this event.
Gather this context (ask conversationally, one section at a time — don't dump every question at once):
### 1. The Event
- What's the topic, and what's the single promise to the attendee? (What will they be able to do after?)
- Format: live training, product demo, expert panel, customer story, fireside chat, multi-session summit?
- Date, length, and platform (Zoom Webinars, Livestorm, Demio, Goldcast, etc.)?
- Live, on-demand/evergreen, or live-then-evergreen?
### 2. The Audience & Goal
- Who is this for? (Role, stage of awareness — cold prospects vs. existing pipeline vs. customers)
- What's the business goal: net-new leads, pipeline acceleration, product adoption, retention/expansion, or brand/authority?
- What's the conversion action after the webinar? (Book a demo, start a trial, upgrade, attend a follow-up call)
### 3. Reality Check
- How big is the reachable list / audience, and what channels can promote it? (Email list, paid, partners, social, sales)
- How long is the runway before the event date?
- Is there budget for paid promotion, or is this organic-only?
---
## How This Skill Works
This skill supports three modes.
### Mode 1: Plan From Scratch
When there's no webinar yet — design the whole motion.
1. Lock the promise and pick the format that fits the goal (see `references/webinar-formats.md`)
2. Map the funnel targets backward from the business goal (see funnel math below)
3. Build the promotion plan across the runway (see `references/promotion-playbook.md`)
4. Design the show-up sequence and the live-to-close moment
5. Plan the segmented follow-up for attendees vs. no-shows
6. Deliver: a full webinar plan (use `templates/webinar-plan-template.md`), promo calendar, and email/copy drafts
### Mode 2: Optimize / Rescue
When a webinar exists or recently ran and the numbers disappoint. Diagnose where the funnel breaks before rewriting anything.
1. Get the actual numbers: invited → registered → showed up → engaged → converted
2. Score the funnel with `scripts/webinar_funnel_scorer.py` to find the weakest stage
3. Fix the stage that's actually broken — don't rewrite the landing page when the problem is show-up rate
4. Deliver: diagnosis (where it breaks + why) + targeted fixes ranked by impact
### Mode 3: Evergreen / On-Demand
When turning a one-time webinar into an always-on lead engine.
1. Identify the segment of the webinar with the strongest live-to-close moment
2. Set up the on-demand registration → watch → follow-up automation
3. Decide live vs. "just-in-time" simulated-live framing (be honest with the audience — fake-live that's obviously fake erodes trust)
4. Deliver: evergreen funnel map + automated follow-up sequence
---
## The Funnel Math (Plan Backward)
Always size the webinar from the business goal backward, using realistic conversion rates. This stops people from celebrating 800 registrations and ignoring that 6 people bought.
```
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 plan must drive ~1,425 qualified visits
```
If the math says you need 5,000 visits and your list is 2,000 people, the plan is broken before it starts — fix the goal, the format, or the promotion budget now, not after. See `references/benchmarks.md` for stage-by-stage benchmarks by audience type.
---
## The Five Stages
### 1. Registration
The landing page has one job: make the value obvious and registering frictionless.
- Lead with the *outcome and the takeaway*, not the agenda. "Leave with a 90-day pipeline plan" beats "Join us to learn about pipeline."
- Name and face of the host/speaker — people register for people.
- Ask for the minimum fields you'll actually use. Every extra field costs registrations.
- Show the date/time in the visitor's timezone, and make the time commitment explicit.
- If B2B and gating matters, you can ask for company/role — but know it lowers CVR.
### 2. Show-Up
This is where most webinars quietly fail. A registration is a promise people forget. The show-up sequence is non-negotiable.
- Confirmation email immediately, with a one-click calendar add (this alone lifts attendance materially).
- Reminders: 1 week, 1 day, 1 hour, and "we're live now." The 1-hour and live reminders drive the most attendance.
- Give a reason to show up *live* vs. watch the recording: live Q&A, a live-only resource, a giveaway, or a tool they build during the session.
- Pre-event engagement (a question, a poll, a "what do you want covered?") increases commitment.
### 3. Live Engagement
Attention is the currency. Every 10 minutes without interaction, you lose people.
- Hook in the first 2-3 minutes: state the promise and the agenda, then deliver a quick win fast.
- Interaction every 5-10 minutes: polls, chat prompts, Q&A, live examples.
- Teach something genuinely useful even if no one buys — earned trust converts later.
- Watch the drop-off curve; the point where people leave tells you where the content sags.
### 4. Live-to-Close (The Transition)
The pitch is a moment, not the whole webinar. Done right it feels like a natural next step, not a bait-and-switch.
- Earn the right first: deliver real value before any offer.
- Transition explicitly and confidently: "Here's how to go further / do this with us."
- Make the offer time-bound to the event (attendee-only bonus, deadline) to create a reason to act now.
- One clear CTA. Repeat it; don't bury it.
### 5. Follow-Up
The follow-up converts more than the live event for most B2B webinars. Segment it.
| Segment | Message |
|---------|---------|
| Attended + engaged with offer | Strike now — direct path to the CTA, attendee bonus, easy booking |
| Attended, no action | Recap + the one key takeaway + soft CTA |
| Registered, no-show | "Sorry we missed you" + recording + same offer (this segment is often the biggest) |
| Watched recording later | Recap + CTA matched to on-demand intent |
Send the recording within 24 hours while it's fresh. See `references/promotion-playbook.md` for the full follow-up sequence.
---
## What to Avoid
| ❌ Avoid | Why It Fails |
|----------|-------------|
| Promoting the *topic* instead of the *takeaway* | People register for outcomes, not agendas |
| One reminder email | Show-up rate craters; you need a sequence, not a nudge |
| No reason to attend live | Everyone "watches the recording later" (they don't) |
| 45 minutes of teaching, then a hard pitch with no transition | Feels like a bait-and-switch; tanks trust and conversion |
| Treating no-shows as lost | No-shows are often your largest convertible segment |
| Optimizing registrations as the headline metric | 800 regs and 6 buyers is a failure dressed as a win |
| Sending the recording a week later | Intent has evaporated; send within 24h |
| Asking for 8 form fields | Every field beyond the essentials costs registrations |
---
## Proactive Triggers
Surface these without being asked when you see them:
- **Show-up sequence has fewer than 3 reminders** → attendance will suffer. Flag and propose the full 1-week/1-day/1-hour/live cadence.
- **No live-only incentive** → registrations will watch "later" and never convert. Recommend a live Q&A, attendee bonus, or build-along.
- **Registration page leads with agenda/logistics** → reframe around the attendee outcome and takeaway.
- **No follow-up plan for no-shows** → flag that the largest convertible segment is being ignored.
- **Funnel goal implies more traffic than the reachable audience can supply** → the plan is mathematically broken; flag before execution and adjust goal/format/budget.
- **The offer/CTA appears with no value delivered first** → it will read as a bait-and-switch; recommend earning the transition.
- **Webinar length over ~60 min for a cold audience** → drop-off risk; recommend tightening or splitting.
---
## Output Artifacts
| When you ask for... | You get... |
|---------------------|------------|
| Plan a webinar | Full webinar plan: promise, format, funnel targets (backward math), promotion calendar, show-up sequence, live structure, and follow-up plan |
| Promote my webinar | Channel-by-channel promotion calendar across the runway + ready-to-send invite, reminder, and social copy |
| Fix my low attendance | Show-up diagnosis + a complete reminder sequence with timing and copy |
| Why isn't it converting? | Funnel diagnosis (stage-by-stage with the weakest link identified) + ranked fixes |
| Write the follow-up | Segmented post-event sequence (engaged / attended / no-show / on-demand) with copy |
| Score my funnel | 0-100 funnel scorecard from your numbers (via `scripts/webinar_funnel_scorer.py`) with the bottleneck stage called out |
---
## Communication
All output follows the structured communication standard:
- **Bottom line first** — answer before explanation
- **What + Why + How** — every finding has all three
- **Actions have owners and deadlines** — no "we should consider"
- **Confidence tagging** — 🟢 verified / 🟡 medium / 🔴 assumed
For webinar numbers, always tag whether a conversion rate is measured from their data (🟢) or an industry benchmark assumption (🟡), so they know what's real vs. estimated.
---
## Related Skills
- **launch-strategy**: For full product/feature launches where a webinar is one tactic among many. Use webinar-marketing for the webinar motion itself; use launch-strategy for the broader launch plan.
- **email-sequence**: For lifecycle and nurture emails to opted-in subscribers. The webinar follow-up borrows its mechanics, but the show-up and follow-up sequences here are event-specific. Use email-sequence for ongoing drips, not event flows.
- **paid-ads**: For paid registration-driving campaigns. Use it to build the promotion traffic this skill's funnel math calls for. NOT for the funnel design itself.
- **page-cro**: For optimizing the registration landing page conversion rate specifically. Pair it with this skill when the bottleneck is landing-page CVR.
- **content-creator**: For producing the on-demand assets, recap posts, and clips that extend a webinar's life. Good follow-up reuses webinar content.
- **campaign-analytics**: For measuring webinar performance against pipeline and revenue. Use it to close the loop on whether the webinar actually drove business outcomes.
- **social-content**: For the organic social promotion of the event across the runway.
FILE:evals/evals.json
{
"skill_name": "webinar-marketing",
"evals": [
{
"id": 0,
"name": "plan-first-webinar-from-scratch",
"prompt": "We're a Series A B2B SaaS selling an AP automation tool to finance teams. I want to run our first webinar next month to generate leads. I've got an email list of about 8,000 finance and ops people. Can you help me plan the whole thing? I honestly have no idea where to start.",
"expected_output": "A full webinar plan: clear attendee promise, format recommendation, funnel targets worked backward from a lead goal, a promotion calendar across the runway, a show-up/reminder sequence, live structure with a live-to-close, and a segmented follow-up plan (incl. no-shows).",
"files": []
},
{
"id": 1,
"name": "rescue-low-attendance",
"prompt": "Our last webinar flopped on attendance. We had 540 registrations but only 95 people showed up live, and barely anyone watched the recording afterward. The content itself was good — it's the turnout that's killing us. What should we do differently next time?",
"expected_output": "Diagnosis that correctly identifies registration->attendance (show-up) as the broken stage, then specific fixes: a full reminder sequence with timing, one-click calendar add, a live-only incentive, and shortening the registration-to-event gap. Should NOT just rewrite the landing page or content.",
"files": []
},
{
"id": 2,
"name": "write-segmented-followup",
"prompt": "I need follow-up emails for a webinar we ran yesterday on 'Cutting cloud costs in 2026.' 220 people registered and 80 attended live. Can you write the follow-up emails? I want to push everyone toward booking a demo.",
"expected_output": "A segmented follow-up sequence: attendees who engaged, attendees who didn't act, and no-shows (the ~140 who registered but didn't attend) each get tailored copy. Recording sent within 24h, one clear demo CTA per email, with the no-show segment treated as warm.",
"files": []
}
]
}
FILE:references/benchmarks.md
# Webinar Benchmarks
Use these as planning assumptions, not promises. Always tag numbers derived from these as 🟡 (benchmark) vs. 🟢 (the user's measured data). Ranges vary widely by industry, audience temperature, and list quality.
## Table of Contents
- [Funnel Stage Benchmarks](#funnel-stage-benchmarks)
- [By Audience Temperature](#by-audience-temperature)
- [Show-Up Rate Factors](#show-up-rate-factors)
- [Conversion Benchmarks](#conversion-benchmarks)
---
## Funnel Stage Benchmarks
| Stage | Typical Range | Notes |
|-------|---------------|-------|
| Landing page registration CVR | 20-50% | Higher for warm/owned traffic, lower for cold/paid |
| Registration → live attendance | 25-45% | The most variable and most neglected stage |
| Registration → eventual viewing (incl. on-demand) | 50-65% | Counts recording watchers |
| Live attendee average watch time | 50-70% of runtime | Drop-off accelerates after ~30-40 min |
| Attendee → offer click/CTA | 15-30% | Depends heavily on the live-to-close |
| Attendee → SQO/opportunity (B2B) | 5-15% | For sales-led motions |
| Attendee → trial/signup (PLG) | 10-25% | For self-serve motions |
---
## By Audience Temperature
| Audience | Reg CVR | Show-Up | Convert | Implication |
|----------|---------|---------|---------|-------------|
| Existing customers | 30-50% | 40-60% | High | Best for expansion/adoption webinars |
| Warm pipeline / engaged leads | 25-45% | 35-50% | Medium-high | Best for pipeline acceleration |
| Owned cold list | 15-30% | 25-40% | Medium | Needs strong takeaway + show-up sequence |
| Paid / net-new cold | 10-25% | 20-35% | Lower | Volume play; expect heavier follow-up reliance |
The colder the audience, the more the value must be obvious up front and the more the follow-up carries the conversion.
---
## Show-Up Rate Factors
What moves registration → live attendance, roughly in order of impact:
1. **One-click calendar add** in the confirmation — large lift.
2. **The 1-hour and "we're live" reminders** — the biggest single-email contributors.
3. **A live-only incentive** (Q&A, bonus, build-along) — gives a reason not to "watch later."
4. **Short gap between registration and event** — the longer the wait, the more forget; same-week registrants show up at higher rates.
5. **Time of day / day of week** — mid-week, mid-morning in the audience's primary timezone tends to perform; always confirm against your own data.
A webinar with a single reminder email typically sees show-up in the low 20s%; a full sequence with a calendar add and live incentive can push it to 40%+.
---
## Conversion Benchmarks
"Good" depends entirely on the goal. Pick the metric that matches the business objective and ignore vanity:
- **Lead-gen webinar:** cost per qualified registration and registration→SQO rate matter more than raw registration count.
- **Pipeline acceleration:** influenced/accelerated opportunity value, not attendance.
- **Adoption/retention:** feature adoption lift or churn reduction among attendees.
- **Brand/authority:** attendance quality, engagement, and downstream brand lift — accept that direct conversion will be lower and that's fine if it's the goal.
Always reconcile the webinar back to revenue or its true objective. 800 registrations and zero pipeline is not a success.
FILE:references/promotion-playbook.md
# Webinar Promotion & Follow-Up Playbook
The runway and the follow-up are where webinars are won. This is the channel-by-channel cadence plus copy patterns. Adjust the timeline to your runway — compress proportionally if you have less than three weeks.
## Table of Contents
- [Promotion Timeline (3-Week Runway)](#promotion-timeline-3-week-runway)
- [Channel Playbook](#channel-playbook)
- [The Show-Up Sequence](#the-show-up-sequence)
- [The Follow-Up Sequence](#the-follow-up-sequence)
- [Copy Patterns](#copy-patterns)
---
## Promotion Timeline (3-Week Runway)
| When | Action | Channel |
|------|--------|---------|
| Day -21 | Announce + open registration | Email (full list), landing page live |
| Day -21 → -1 | Always-on promotion | Paid social/search, organic social |
| Day -14 | Speaker/topic spotlight | Email (non-registrants), social |
| Day -10 | Partner/affiliate co-promotion | Partner emails, co-marketing |
| Day -7 | "1 week to go" + agenda reveal | Email (non-registrants), social |
| Day -3 | Social proof push (who's attending) | Social, email |
| Day -2 | Last-call to the list | Email (non-registrants) |
| Day -1 | Reminder to registrants begins | (see show-up sequence) |
| Day 0 | Live + "we're live" blast | Email + social |
| Day 0 → +7 | Recording + follow-up | Email (segmented), social clips |
Promote to non-registrants and registrants on **separate tracks** — never re-pitch registration to someone who already registered.
---
## Channel Playbook
**Owned email** — your highest-converting channel. Segment: full list (announce), engaged non-registrants (repeat with new angle), customers (if relevant). 3-5 sends across the runway is normal, not spammy, when each has a fresh angle.
**Organic social** — speaker quote cards, a teaser clip, a "what you'll learn" carousel, countdown posts. The speaker/host resharing to their own network typically outperforms the brand account.
**Paid** — retargeting site visitors and lookalikes of your customer list converts best. Drive to the registration page, optimize for registrations, and cap frequency so you don't burn the audience before the event.
**Partners / co-marketing** — a partner with an overlapping audience is the fastest way to expand reach. Give them swipe copy and a tracked link.
**Sales outreach** — for pipeline-acceleration webinars, reps personally inviting their open opportunities drives the highest-quality attendance.
**Communities & newsletters** — relevant Slack/Discord communities and niche newsletters reach intent-rich audiences. Lead with value, follow community norms.
---
## The Show-Up Sequence
Registration is a promise people forget. This sequence is the single biggest lever on attendance.
| Timing | Email | Job |
|--------|-------|-----|
| Immediately on register | Confirmation | Confirm + one-click calendar add + set expectations |
| Day -7 (if reg'd early) | Value reminder | Re-sell the takeaway; add a pre-event question/poll |
| Day -1 | "Tomorrow" | Time (their timezone), how to join, what to bring |
| Day 0, -1 hour | "Starting soon" | Highest-impact reminder; one-click join link |
| Day 0, live | "We're live" | Join now; mention the live-only incentive |
Rules:
- One-click calendar add in the confirmation is the highest-ROI single tactic for attendance.
- The 1-hour and live reminders drive the most show-ups — never skip them.
- Always restate the *live-only* reason (Q&A, bonus, build-along) in the final two reminders.
---
## The Follow-Up Sequence
For most B2B webinars, follow-up converts more than the live event. Send the recording within 24 hours. Segment by behavior.
| Day | Segment | Message |
|-----|---------|---------|
| +0 (within 24h) | All attendees | Thank-you + recording + the one key takeaway + clear CTA |
| +0 (within 24h) | No-shows | "Sorry we missed you" + recording + same offer/CTA |
| +2 | Engaged with offer | Direct, personal nudge to the CTA + attendee-only bonus/deadline |
| +4 | Attended, no action | A second angle (case study, FAQ, objection handled) + CTA |
| +7 | Still unconverted | Last call on the attendee bonus; then move to standard nurture |
No-shows are frequently the **largest** convertible segment — treat them as warm, not lost.
---
## Copy Patterns
**Registration headline** — outcome-first:
> ✅ "Build a 90-day pipeline plan you can run on Monday"
> ❌ "Join our webinar on pipeline planning"
**Invite email opener** — lead with their problem and the takeaway, not the logistics:
> "If your pipeline reviews feel like guesswork, this session gives you a repeatable model. Live [date], you'll leave with the exact framework — and a template to run it."
**Live-only incentive line:**
> "Attend live for the Q&A and a copy of the planning template — it's not in the recording."
**No-show follow-up opener:**
> "Missed you yesterday — no worries, here's the full recording. The part most people flagged as useful starts around [timestamp]."
**Live-to-close transition:**
> "That's the framework — you can run it yourself from today. If you'd rather have us set it up with you, here's how that works…"
FILE:references/webinar-formats.md
# Webinar Formats
Pick the format that matches the goal and the audience's stage of awareness. The format dictates the structure, the promotion angle, and the live-to-close.
## Table of Contents
- [Format Selector](#format-selector)
- [Format Details](#format-details)
- [Structure Templates](#structure-templates)
---
## Format Selector
| Goal | Best Format(s) | Why |
|------|----------------|-----|
| Net-new lead gen (cold) | Educational training, masterclass | Value-first builds trust with people who don't know you |
| Pipeline acceleration | Product demo, customer story | Shows the solution working for people already evaluating |
| Product adoption / retention | Live training, office hours | Helps existing users get more value |
| Authority / brand | Expert panel, fireside chat | Borrowed credibility, shareable, low-pitch |
| Demand at scale | Virtual summit (multi-session) | Many speakers = many promoters = reach |
---
## Format Details
**Educational training / masterclass** — You teach a genuinely useful skill or framework. The offer is "do this faster/better with us." Highest trust, works for cold audiences. Risk: teaching so much there's no reason to buy — leave a clear "do-it-with-us" gap.
**Product demo** — Show the product solving a real problem, ideally with a realistic scenario rather than a feature tour. Best for warm pipeline. Risk: feature-dumping; anchor every feature to a pain.
**Customer story / case study** — A customer tells how they got a result. Extremely persuasive for evaluators because it's peer proof, not vendor claims. Risk: too much backstory, not enough transferable insight.
**Expert panel** — 3-4 voices on a timely topic. Great reach (panelists promote) and authority. Lower direct conversion; pair with a strong follow-up. Risk: meandering — a firm moderator is essential.
**Fireside chat** — One notable guest, conversational. Draws registrations on the guest's name. Low-pitch, brand-building. Risk: no clear next step — design the CTA deliberately.
**Office hours / AMA** — Recurring, live Q&A. Excellent for adoption and community. Low production cost. Risk: dead air if no questions — seed a few.
**Virtual summit** — Multi-session, multi-speaker event over hours or days. Maximum reach and list growth. High production effort. Risk: low per-session attendance — design for asynchronous/on-demand viewing.
---
## Structure Templates
### Standard 45-minute educational webinar
1. **0-3 min — Hook:** the promise, who it's for, agenda, and a quick credibility marker.
2. **3-8 min — Frame the problem:** make them feel the cost of the status quo.
3. **8-30 min — Teach the core content:** 3 main points, an interaction every 5-10 min, at least one quick win they can use immediately.
4. **30-38 min — Live-to-close:** recap the transformation, transition to "how to go further," present one time-bound offer.
5. **38-45 min — Q&A:** answer live (handle objections in the open), restate the CTA at the end.
### Product demo (30 min)
1. 0-3 min — the problem and who has it.
2. 3-20 min — the product solving it in a realistic scenario; anchor each capability to a pain.
3. 20-25 min — proof (a customer result) + the offer/next step.
4. 25-30 min — Q&A + CTA.
### Panel (45-60 min)
1. 0-5 min — moderator frames the topic and introduces panelists.
2. 5-45 min — 4-6 prepared questions, audience questions woven in.
3. Final 10 min — each panelist's one takeaway + the host's single CTA.
Keep cold-audience webinars at or under ~60 minutes. Attention and drop-off get punishing beyond that.
FILE:scripts/webinar_funnel_scorer.py
#!/usr/bin/env python3
"""Score a webinar funnel 0-100 and identify the weakest stage.
Stdlib-only. Reads funnel numbers from a JSON file arg or stdin, compares each
stage's conversion rate against industry benchmarks, scores the funnel, and
names the bottleneck so you fix the stage that's actually broken.
Input JSON (all optional except where noted):
{
"invited": 5000, # optional (audience reached / list size)
"page_visits": 1800, # optional
"registrations": 620, # required
"attended_live": 180, # required
"cta_clicks": 40, # optional
"conversions": 14, # optional (SQOs, trials, demos booked...)
"audience": "owned_cold", # one of: customers, warm, owned_cold, paid_cold
"runtime_min": 45, # optional
"avg_watch_min": 26 # optional
}
Usage:
python webinar_funnel_scorer.py data.json # score a JSON file
cat data.json | python webinar_funnel_scorer.py - # read JSON from stdin
python webinar_funnel_scorer.py # runs on embedded sample data
"""
import json
import sys
# Benchmark "good" conversion rates per stage, by audience temperature.
# Each value is the rate considered solid; we score relative to it.
BENCHMARKS = {
"customers": {"page_cvr": 0.40, "show_up": 0.50, "cta": 0.25, "convert": 0.12},
"warm": {"page_cvr": 0.35, "show_up": 0.42, "cta": 0.22, "convert": 0.10},
"owned_cold": {"page_cvr": 0.25, "show_up": 0.35, "cta": 0.18, "convert": 0.07},
"paid_cold": {"page_cvr": 0.18, "show_up": 0.28, "cta": 0.15, "convert": 0.05},
}
STAGE_LABELS = {
"page_cvr": "Landing page -> registration",
"show_up": "Registration -> live attendance",
"watch": "Attendee watch-time",
"cta": "Attendee -> CTA click",
"convert": "Attendee -> conversion",
}
SAMPLE = {
"invited": 5000,
"page_visits": 1800,
"registrations": 620,
"attended_live": 150,
"cta_clicks": 33,
"conversions": 9,
"audience": "owned_cold",
"runtime_min": 45,
"avg_watch_min": 24,
}
def safe_div(a, b):
return (a / b) if b else None
def stage_score(actual, benchmark):
"""Score a single stage 0-100: 100 if at/above benchmark, scaled below."""
if actual is None or benchmark in (None, 0):
return None
return max(0, min(100, round((actual / benchmark) * 100)))
def analyze(d):
audience = d.get("audience", "owned_cold")
bm = BENCHMARKS.get(audience, BENCHMARKS["owned_cold"])
regs = d.get("registrations")
att = d.get("attended_live")
if regs is None or att is None:
raise ValueError("registrations and attended_live are required")
rates = {
"page_cvr": safe_div(regs, d.get("page_visits")),
"show_up": safe_div(att, regs),
"watch": safe_div(d.get("avg_watch_min"), d.get("runtime_min")),
"cta": safe_div(d.get("cta_clicks"), att),
"convert": safe_div(d.get("conversions"), att),
}
# Watch-time benchmark is a flat 0.6 of runtime (good engagement).
bm_full = dict(bm)
bm_full["watch"] = 0.60
scores = {}
for stage, actual in rates.items():
scores[stage] = stage_score(actual, bm_full.get(stage))
scored = {k: v for k, v in scores.items() if v is not None}
overall = round(sum(scored.values()) / len(scored)) if scored else 0
# Weakest scored stage = the bottleneck.
bottleneck = min(scored, key=scored.get) if scored else None
return {
"audience": audience,
"overall_score": overall,
"stage_rates": {k: (round(v, 3) if v is not None else None)
for k, v in rates.items()},
"stage_scores": scores,
"benchmarks": bm_full,
"bottleneck": bottleneck,
"bottleneck_label": STAGE_LABELS.get(bottleneck) if bottleneck else None,
}
def fmt_pct(x):
return f"{x*100:.0f}%" if isinstance(x, (int, float)) else "n/a"
def print_summary(r):
print("=" * 56)
print(f"WEBINAR FUNNEL SCORE: {r['overall_score']}/100 "
f"(audience: {r['audience']})")
print("=" * 56)
print(f"{'Stage':<34}{'Rate':>8}{'Bench':>8}{'Score':>7}")
print("-" * 56)
for stage in ["page_cvr", "show_up", "watch", "cta", "convert"]:
rate = r["stage_rates"].get(stage)
bench = r["benchmarks"].get(stage)
score = r["stage_scores"].get(stage)
label = STAGE_LABELS.get(stage, stage)
score_s = f"{score}" if score is not None else " -"
flag = " <-- weakest" if stage == r["bottleneck"] else ""
print(f"{label:<34}{fmt_pct(rate):>8}{fmt_pct(bench):>8}{score_s:>7}{flag}")
print("-" * 56)
if r["bottleneck"]:
print(f"BOTTLENECK: {r['bottleneck_label']}")
print("Fix this stage first — it's dragging the funnel most.")
print()
def main():
arg = sys.argv[1] if len(sys.argv) > 1 else None
if arg == "-":
# Explicit stdin. Only read here so we never block when no input exists.
raw = sys.stdin.read().strip()
data = json.loads(raw) if raw else SAMPLE
elif arg:
with open(arg) as f:
data = json.load(f)
else:
data = SAMPLE
print("(no input given — running on embedded sample data)\n")
result = analyze(data)
print_summary(result)
print("JSON:")
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
FILE:templates/webinar-plan-template.md
# Webinar Plan — [Webinar Title]
> Fill the bracketed placeholders. Delete guidance notes in _italics_ once filled.
## 1. The Promise
- **Topic:** [topic]
- **One-line promise (attendee outcome):** [After this, attendees will be able to ___]
- **Format:** [educational / demo / customer story / panel / fireside / summit]
- **Date & time:** [date, time, timezone]
- **Length:** [minutes]
- **Live / on-demand / both:** [choice]
- **Platform:** [Zoom Webinars / Livestorm / Demio / Goldcast / other]
## 2. Audience & Goal
- **Primary audience:** [role, segment, awareness stage]
- **Business goal:** [lead gen / pipeline acceleration / adoption / retention / brand]
- **Post-webinar conversion action:** [book demo / start trial / upgrade / book call]
## 3. Funnel Targets (work backward from the goal)
| Stage | Target | Assumed rate | Source |
|-------|--------|--------------|--------|
| Conversions (goal) | [N] | — | 🟢/🟡 |
| Engaged attendees | [N] | [attendee→convert %] | 🟢/🟡 |
| Live registrations | [N] | [register→attend %] | 🟢/🟡 |
| Landing-page visits | [N] | [page CVR %] | 🟢/🟡 |
- **Reachable audience / channels:** [list size + channels] — _does the math fit? If not, fix goal/format/budget._
## 4. Promotion Plan
| When | Action | Channel | Owner | Status |
|------|--------|---------|-------|--------|
| Day -[X] | [announce / spotlight / last call] | [channel] | [owner] | [ ] |
- **Paid budget:** [amount or none]
- **Partners / co-promo:** [who]
## 5. Show-Up Sequence
| Timing | Email | Key message | Owner |
|--------|-------|-------------|-------|
| On register | Confirmation + calendar add | [ ] | [owner] |
| Day -1 | "Tomorrow" | [ ] | [owner] |
| -1 hour | "Starting soon" | [ ] | [owner] |
| Live | "We're live" | [ ] | [owner] |
- **Live-only incentive:** [Q&A / bonus / build-along / giveaway]
## 6. Live Structure
- **Hook (0-3 min):** [ ]
- **Core content points:** [1] / [2] / [3]
- **Interactions (every 5-10 min):** [polls / chat / Q&A / live example]
- **Live-to-close transition (when + how):** [ ]
- **Offer / CTA:** [one offer, time-bound]
## 7. Follow-Up Plan
| Segment | Timing | Message | CTA | Owner |
|---------|--------|---------|-----|-------|
| Attended + engaged | +0 / +2 | [ ] | [ ] | [owner] |
| Attended, no action | +0 / +4 | [ ] | [ ] | [owner] |
| No-show | +0 | [ ] | [ ] | [owner] |
| On-demand viewer | on watch | [ ] | [ ] | [owner] |
- **Recording sent within 24h:** [ ]
## 8. Measurement
- **Primary metric:** [the one number that defines success — tied to the business goal]
- **Secondary metrics:** [registrations, show-up %, watch time, CTA clicks]
- **How conversions are attributed back to revenue:** [ ]
Phân tích bản ghi và transcript cuộc họp để tìm mẫu hành vi, thói quen giao tiếp chưa tốt và đưa ra phản hồi huấn luyện cụ thể.
---
name: meeting-analyzer
description: Analyzes meeting transcripts and recordings to surface behavioral patterns, communication anti-patterns, and actionable coaching feedback. Use this skill whenever the user uploads or points to meeting transcripts (.txt, .md, .vtt, .srt, .docx), asks about their communication habits, wants feedback on how they run meetings, requests speaking ratio analysis, mentions filler words or conflict avoidance, or wants to compare their communication across time periods. Also trigger when users mention tools like Granola, Otter, Fireflies, or Zoom transcripts. Even if the user just says "look at my meetings" or "how do I come across in meetings" — use this skill.
---
# Meeting Insights Analyzer
> Originally contributed by [maximcoding](https://github.com/maximcoding) — enhanced and integrated by the claude-skills team.
Transform meeting transcripts into concrete, evidence-backed feedback on communication patterns, leadership behaviors, and interpersonal dynamics.
## Core Workflow
### 1. Ingest & Inventory
Scan the target directory for transcript files (`.txt`, `.md`, `.vtt`, `.srt`, `.docx`, `.json`).
For each file:
- Extract meeting date from filename or content (expect `YYYY-MM-DD` prefix or embedded timestamps)
- Identify speaker labels — look for patterns like `Speaker 1:`, `[John]:`, `John Smith 00:14:32`, VTT/SRT cue formatting
- Detect the user's identity: ask if ambiguous, otherwise infer from the most frequent speaker or filename hints
- Log: filename, date, duration (from timestamps), participant count, word count
Print a brief inventory table so the user confirms scope before heavy analysis begins.
### 2. Normalize Transcripts
Different tools produce wildly different formats. Normalize everything into a common internal structure before analysis:
```
{ speaker: string, timestamp_sec: number | null, text: string }[]
```
Handling per format:
- **VTT/SRT**: Parse cue timestamps + text. Speaker labels may be inline (`<v Speaker>`) or prefixed.
- **Plain text**: Look for `Name:` or `[Name]` prefixes per line. If no speaker labels exist, warn the user that per-speaker analysis is limited.
- **Markdown**: Strip formatting, then treat as plain text.
- **DOCX**: Extract text content, then treat as plain text.
- **JSON**: Expect an array of objects with `speaker`/`text` fields (common Otter/Fireflies export).
If timestamps are missing, degrade gracefully — skip timing-dependent metrics (speaking pace, pause analysis) but still run text-based analysis.
### 3. Analyze
Run all applicable analysis modules below. Each module is independent — skip any that don't apply (e.g., skip speaking ratios if there are no speaker labels).
---
#### Module: Speaking Dynamics
Calculate per-speaker:
- **Word count & percentage** of total meeting words
- **Turn count** — how many times each person spoke
- **Average turn length** — words per uninterrupted speaking turn
- **Longest monologue** — flag turns exceeding 60 seconds or 200 words
- **Interruption detection** — a turn that starts within 2 seconds of the previous speaker's last timestamp, or mid-sentence breaks
Produce a per-meeting summary and a cross-meeting average if multiple transcripts exist.
Red flags to surface:
- User speaks > 60% in a 1:many meeting (dominating)
- User speaks < 15% in a meeting they're facilitating (disengaged or over-delegating)
- One participant never speaks (excluded voice)
- Interruption ratio > 2:1 (user interrupts others twice as often as they're interrupted)
---
#### Module: Conflict & Directness
Scan the user's speech for hedging and avoidance markers:
**Hedging language** (score per-instance, aggregate per meeting):
- Qualifiers: "maybe", "kind of", "sort of", "I guess", "potentially", "arguably"
- Permission-seeking: "if that's okay", "would it be alright if", "I don't know if this is right but"
- Deflection: "whatever you think", "up to you", "I'm flexible"
- Softeners before disagreement: "I don't want to push back but", "this might be a dumb question"
**Conflict avoidance patterns** (requires more context, flag with confidence level):
- Topic changes after tension (speaker A raises problem → user pivots to logistics)
- Agreement-without-commitment: "yeah totally" followed by no action or follow-up
- Reframing others' concerns as smaller than stated: "it's probably not that big a deal"
- Absent feedback in 1:1s where performance topics would be expected
For each flagged instance, extract:
- The full quote (with surrounding context — 2 turns before and after)
- A severity tag: `low` (single hedge word), `medium` (pattern of hedging in one exchange), `high` (clearly avoided a necessary conversation)
- A rewrite suggestion: what a more direct version would sound like
---
#### Module: Filler Words & Verbal Habits
Count occurrences of: "um", "uh", "like" (non-comparative), "you know", "actually", "basically", "literally", "right?" (tag question), "so yeah", "I mean"
Report:
- Total count per meeting
- Rate per 100 words spoken (normalizes across meeting lengths)
- Breakdown by filler type
- Contextual spikes — do fillers increase in specific situations? (e.g., when responding to a senior stakeholder, when giving negative feedback, when asked a question cold)
Only flag this as an issue if the rate exceeds ~3 per 100 words. Below that, it's normal speech.
---
#### Module: Question Quality & Listening
Classify the user's questions:
- **Closed** (yes/no): "Did you finish the report?"
- **Leading** (answer embedded): "Don't you think we should ship sooner?"
- **Open genuine**: "What's blocking you on this?"
- **Clarifying** (references prior speaker): "When you said X, did you mean Y?"
- **Building** (extends another's idea): "That's interesting — what if we also Z?"
Good listening indicators:
- Clarifying and building questions (shows active processing)
- Paraphrasing: "So what I'm hearing is..."
- Referencing a point someone made earlier in the meeting
- Asking quieter participants for input
Poor listening indicators:
- Asking a question that was already answered
- Restating own point without acknowledging the response
- Responding to a question with an unrelated topic
Report the ratio of open/clarifying/building vs. closed/leading questions.
---
#### Module: Facilitation & Decision-Making
Only apply when the user is the meeting organizer or facilitator.
Evaluate:
- **Agenda adherence**: Did the meeting follow a structure or drift?
- **Time management**: How long did each topic take vs. expected?
- **Inclusion**: Did the facilitator actively draw in quiet participants?
- **Decision clarity**: Were decisions explicitly stated? ("So we're going with option B — Sarah owns the follow-up by Friday.")
- **Action items**: Were they assigned with owners and deadlines, or left vague?
- **Parking lot discipline**: Were off-topic items acknowledged and deferred, or did they derail?
---
#### Module: Sentiment & Energy
Track the emotional arc of the user's language across the meeting:
- **Positive markers**: enthusiastic agreement, encouragement, humor, praise
- **Negative markers**: frustration, dismissiveness, sarcasm, curt responses
- **Neutral/flat**: low-energy responses, monosyllabic answers
Flag energy drops — moments where the user's engagement visibly decreases (shorter turns, less substantive responses). These often correlate with discomfort, boredom, or avoidance.
---
### 4. Output the Report
Structure the final output as a single cohesive report. Use this skeleton — omit any section where data was insufficient:
```markdown
# Meeting Insights Report
**Period**: [earliest date] – [latest date]
**Meetings analyzed**: [count]
**Total transcript words**: [count]
**Your speaking share (avg)**: [X%]
---
## Top 3 Findings
[Rank by impact. Each finding gets 2-3 sentences + one concrete example with a direct quote and timestamp.]
## Detailed Analysis
### Speaking Dynamics
[Stats table + narrative interpretation + flagged red flags]
### Directness & Conflict Patterns
[Flagged instances grouped by pattern type, with quotes and rewrites]
### Verbal Habits
[Filler word stats, contextual spikes, only if rate > 3/100 words]
### Listening & Questions
[Question type breakdown, listening indicators, specific examples]
### Facilitation
[Only if applicable — agenda, decisions, action items]
### Energy & Sentiment
[Arc summary, flagged drops]
## Strengths
[3 specific things the user does well, with evidence]
## Growth Opportunities
[3 ranked by impact, each with: what to change, why it matters, a concrete "try this next time" action]
## Comparison to Previous Period
[Only if prior analysis exists — delta on key metrics]
```
### 5. Follow-Up Options
After delivering the report, offer:
- Deep dive into any specific meeting or pattern
- A 1-page "communication cheat sheet" with the user's top 3 habits to change
- Tracking setup — save current metrics as a baseline for future comparison
- Export as markdown or structured JSON for use in performance reviews
---
## Edge Cases
- **No speaker labels**: Warn the user upfront. Run text-level analysis (filler words, question types on the full transcript) but skip per-speaker metrics. Suggest re-exporting with speaker diarization enabled.
- **Very short meetings** (< 5 minutes or < 500 words): Analyze but caveat that patterns from short meetings may not be representative.
- **Non-English transcripts**: The filler word and hedging dictionaries are English-centric. For other languages, note the limitation and focus on structural analysis (speaking ratios, turn-taking, question counts).
- **Single meeting vs. corpus**: If only one transcript, skip trend/comparison language. Focus findings on that meeting alone.
- **User not identified**: If you can't determine which speaker is the user after scanning, ask before proceeding. Don't guess.
## Transcript Source Tips
Include this section in output only if the user seems unsure about how to get transcripts:
- **Zoom**: Settings → Recording → enable "Audio transcript". Download `.vtt` from cloud recordings.
- **Google Meet**: Auto-transcription saves to Google Docs in the calendar event's Drive folder.
- **Granola**: Exports to markdown. Best speaker label quality of consumer tools.
- **Otter.ai**: Export as `.txt` or `.json` from the web dashboard.
- **Fireflies.ai**: Export as `.docx` or `.json` — both work.
- **Microsoft Teams**: Transcripts appear in the meeting chat. Download as `.vtt`.
Recommend `YYYY-MM-DD - Meeting Name.ext` naming convention for easy chronological analysis.
---
## Anti-Patterns
| Anti-Pattern | Why It Fails | Better Approach |
|---|---|---|
| Analyzing without speaker labels | Per-person metrics impossible — results are generic word clouds | Ask user to re-export with speaker identification enabled |
| Running all modules on a 5-minute standup | Overkill — filler word and conflict analysis need 20+ min meetings | Auto-detect meeting length and skip irrelevant modules |
| Presenting raw metrics without context | "You said 'um' 47 times" is demoralizing without benchmarks | Always compare to norms and show trajectory over time |
| Analyzing a single meeting in isolation | One meeting is a snapshot, not a pattern — conclusions are unreliable | Require 3+ meetings minimum for trend-based coaching |
| Treating speaking time equality as the goal | A facilitator SHOULD talk less; a presenter SHOULD talk more | Weight speaking ratios by meeting type and role |
| Flagging every hedge word as negative | "I think" and "maybe" are appropriate in brainstorming | Distinguish between decision meetings (hedges are bad) and ideation (hedges are fine) |
---
## Related Skills
| Skill | Relationship |
|-------|-------------|
| `project-management/senior-pm` | Broader PM scope — use for project planning, risk, stakeholders |
| `project-management/scrum-master` | Agile ceremonies — pairs with meeting-analyzer for retro quality |
| `project-management/confluence-expert` | Store meeting analysis outputs as Confluence pages |
| `c-level-advisor/executive-mentor` | Executive communication coaching — complementary perspective |Gợi ý ý tưởng, chiến lược và chiến thuật marketing, tăng trưởng cho sản phẩm SaaS hoặc phần mềm.
---
name: marketing-ideas
description: "When the user needs marketing ideas, inspiration, or strategies for their SaaS or software product. Also use when the user asks for 'marketing ideas,' 'growth ideas,' 'how to market,' 'marketing strategies,' 'marketing tactics,' 'ways to promote,' 'ideas to grow,' 'what else can I try,' 'I don't know how to market this,' 'brainstorm marketing,' or 'what marketing should I do.' Use this as a starting point whenever someone is stuck or looking for inspiration on how to grow. For specific channel execution, see the relevant skill (ads, social, emails, etc.)."
metadata:
version: 2.0.1
---
# Marketing Ideas for SaaS
You are a marketing strategist with a library of 139 proven marketing ideas. Your goal is to help users find the right marketing strategies for their specific situation, stage, and resources.
## How to Use This Skill
**Check for product marketing context first:**
If `.agents/product-marketing.md` exists (or `.claude/product-marketing.md`, or the legacy `product-marketing-context.md` filename, in older setups), read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
When asked for marketing ideas:
1. Ask about their product, audience, and current stage if not clear
2. Suggest 3-5 most relevant ideas based on their context
3. Provide details on implementation for chosen ideas
4. Consider their resources (time, budget, team size)
---
## Ideas by Category (Quick Reference)
| Category | Ideas | Examples |
|----------|-------|----------|
| Content & SEO | 1-10 | Programmatic SEO, Glossary marketing, Content repurposing |
| Competitor | 11-13 | Comparison pages, Marketing jiu-jitsu |
| Free Tools | 14-22 | Calculators, Generators, Chrome extensions |
| Paid Ads | 23-34 | LinkedIn, Google, Retargeting, Podcast ads |
| Social & Community | 35-44 | LinkedIn audience, Reddit marketing, Short-form video |
| Email | 45-53 | Founder emails, Onboarding sequences, Win-back |
| Partnerships | 54-64 | Affiliate programs, Integration marketing, Newsletter swaps |
| Events | 65-72 | Webinars, Conference speaking, Virtual summits |
| PR & Media | 73-76 | Press coverage, Documentaries |
| Launches | 77-86 | Product Hunt, Lifetime deals, Giveaways |
| Product-Led | 87-96 | Viral loops, Powered-by marketing, Free migrations |
| Content Formats | 97-109 | Podcasts, Courses, Annual reports, Year wraps |
| Unconventional | 110-122 | Awards, Challenges, [Guerrilla marketing](references/guerrilla-marketing.md) |
| Platforms | 123-130 | App marketplaces, Review sites, YouTube |
| International | 131-132 | Expansion, Price localization |
| Developer | 133-136 | DevRel, Certifications |
| Audience-Specific | 137-139 | Referrals, Podcast tours, Customer language |
**For the complete list with descriptions**: See [references/ideas-by-category.md](references/ideas-by-category.md)
**Deep dives** (full framework + case library for a single idea):
- **Guerrilla marketing (#121)**: [references/guerrilla-marketing.md](references/guerrilla-marketing.md) — the direct-mail 3-rule framework (relevance / relationship-building / precision targeting), ROI discipline, "think in stories, not campaigns," "test small before going big," and a named case library (WePay, Xero, Red Bull, Antimetal, Arrows, ProfitWell, Buzzsprout, Wistia).
---
## Implementation Tips
### By Stage
**Pre-launch:**
- Waitlist referrals (#79)
- Early access pricing (#81)
- Product Hunt prep (#78)
**Early stage:**
- Content & SEO (#1-10)
- Community (#35)
- Founder-led sales (#47)
**Growth stage:**
- Paid acquisition (#23-34)
- Partnerships (#54-64)
- Events (#65-72)
**Scale:**
- Brand campaigns
- International (#131-132)
- Media acquisitions (#73)
### By Budget
**Free:**
- Content & SEO
- Community building
- Social media
- Comment marketing
**Low budget:**
- Targeted ads
- Sponsorships
- Free tools
**Medium budget:**
- Events
- Partnerships
- PR
**High budget:**
- Acquisitions
- Conferences
- Brand campaigns
### By Timeline
**Quick wins:**
- Ads, email, social posts
**Medium-term:**
- Content, SEO, community
**Long-term:**
- Brand, thought leadership, platform effects
---
## Top Ideas by Use Case
### Need Leads Fast
- Google Ads (#31) - High-intent search
- LinkedIn Ads (#28) - B2B targeting
- Engineering as Marketing (#15) - Free tool lead gen
### Building Authority
- Conference Speaking (#70)
- Book Marketing (#104)
- Podcasts (#107)
### Low Budget Growth
- Easy Keyword Ranking (#1)
- Reddit Marketing (#38)
- Comment Marketing (#44)
### Product-Led Growth
- Viral Loops (#93)
- Powered By Marketing (#87)
- In-App Upsells (#91)
### Enterprise Sales
- Investor Marketing (#133)
- Expert Networks (#57)
- Conference Sponsorship (#72)
---
## Output Format
When recommending ideas, provide for each:
- **Idea name**: One-line description
- **Why it fits**: Connection to their situation
- **How to start**: First 2-3 implementation steps
- **Expected outcome**: What success looks like
- **Resources needed**: Time, budget, skills required
---
## Task-Specific Questions
1. What's your current stage and main growth goal?
2. What's your marketing budget and team size?
3. What have you already tried that worked or didn't?
4. What competitor tactics do you admire?
---
## Related Skills
- **marketing-plan**: When the user wants a comprehensive plan instead of standalone ideas. Section 12 of the plan cross-references all 139 ideas here against AARRR stages and client-specific status.
- **programmatic-seo**: For scaling SEO content (#4)
- **competitors**: For comparison pages (#11)
- **emails**: For email marketing tactics
- **free-tools**: For engineering as marketing (#15)
- **referrals**: For viral growth (#93)
FILE:evals/evals.json
{
"skill_name": "marketing-ideas",
"evals": [
{
"id": 1,
"prompt": "I need marketing ideas for my SaaS product. We're a bootstrapped team of 3, sell a $49/month analytics tool for e-commerce, and have about 200 customers. Budget is tight — maybe $500/month for marketing.",
"expected_output": "Should check for product-marketing.md first. Should filter ideas by low budget and early-stage constraints. Should pull relevant ideas from the 139 marketing ideas organized by category. Should provide ideas appropriate for bootstrapped SaaS: content marketing, community building, SEO, partnerships, referral programs, social media, Product Hunt, and others that don't require large budgets. Output should follow the format: idea name, why it fits, how to start, expected outcome, resources needed. Should prioritize by likely impact given their stage.",
"assertions": [
"Checks for product-marketing.md",
"Filters ideas by low budget constraint",
"Provides ideas from the 139 marketing ideas catalog",
"Ideas are appropriate for bootstrapped SaaS stage",
"Output follows structured format per idea",
"Includes why it fits, how to start, expected outcome",
"Prioritizes by likely impact",
"Includes resources needed per idea"
],
"files": []
},
{
"id": 2,
"prompt": "What's the fastest way to get more leads? We sell enterprise security software and have a $10k/month marketing budget.",
"expected_output": "Should apply the 'top ideas by use case' — specifically 'leads fast' recommendations. Should recommend paid channels (Google Ads, LinkedIn Ads for enterprise), outbound (cold email, LinkedIn outreach), and content-based lead magnets. Should filter for enterprise-appropriate tactics. Should provide the structured output with why each idea fits, how to start, expected timeline, and resources needed. Should note that 'fast leads' typically means paid or outbound channels.",
"assertions": [
"Applies 'leads fast' use case filter",
"Recommends paid channels appropriate for enterprise",
"Recommends outbound tactics",
"Filters for enterprise-appropriate tactics",
"Provides structured output per idea",
"Notes that fast leads means paid or outbound",
"Includes timeline expectations"
],
"files": []
},
{
"id": 3,
"prompt": "how do we grow without spending money on ads? we're a PLG (product-led growth) company with a freemium model",
"expected_output": "Should trigger on casual phrasing. Should apply the 'PLG' use case filter from top ideas. Should recommend PLG-specific tactics: product virality features, referral programs, community building, content marketing, SEO, free tools/calculators, open-source contributions, social proof loops. Should avoid ad-dependent ideas given the constraint. Should provide structured output with implementation guidance.",
"assertions": [
"Triggers on casual phrasing",
"Applies PLG use case filter",
"Recommends PLG-specific tactics",
"Avoids ad-dependent ideas",
"Includes virality and referral tactics",
"Provides structured output with implementation guidance"
],
"files": []
},
{
"id": 4,
"prompt": "We want to build authority and thought leadership in the HR tech space. We're a newer company and nobody knows who we are yet.",
"expected_output": "Should apply the 'authority building' use case filter. Should recommend thought leadership tactics: original research/surveys, guest posting, podcast appearances, speaking engagements, LinkedIn content, industry report publishing, expert roundups. Should note that authority building is a longer-term play. Should provide structured output with how to start each idea, expected outcomes, and timeline.",
"assertions": [
"Applies 'authority building' use case filter",
"Recommends thought leadership tactics",
"Includes original research and content",
"Includes community and media appearances",
"Notes authority building is longer-term",
"Provides structured output with timelines"
],
"files": []
},
{
"id": 5,
"prompt": "Give me 20 marketing ideas. We sell project management software.",
"expected_output": "Should provide a curated list of ~20 ideas from the catalog. Should organize them by category or by effort/impact. Should provide brief implementation context for each. Should vary the ideas across categories (content, community, partnerships, product, paid, etc.) for a well-rounded set. Output should follow the structured format with at least idea name and brief description for each.",
"assertions": [
"Provides approximately 20 ideas",
"Ideas span multiple categories",
"Organizes by category or effort/impact",
"Provides brief implementation context per idea",
"Output follows structured format",
"Ideas are relevant to project management software"
],
"files": []
},
{
"id": 6,
"prompt": "We want to set up a referral program. How should we structure it?",
"expected_output": "Should recognize this is specifically a referral program design request. Should defer to or cross-reference the referrals skill, which provides detailed guidance on referral loop design, incentive structures, implementation, and optimization. May briefly mention referral programs as a marketing idea but should make clear that referrals is the right skill for detailed program design.",
"assertions": [
"Recognizes this as a referral program design request",
"References or defers to referrals skill",
"Does not attempt detailed referral program design",
"May briefly mention as a marketing idea"
],
"files": []
},
{
"id": 7,
"prompt": "We sell a $30k/year data platform and can't get marketing VPs at target accounts to reply to email or take calls. We have budget for something creative. What guerrilla tactics could break through?",
"expected_output": "Should route to the guerrilla-marketing deep-dive reference. Should surface the direct-mail 3-rule framework (relevance, relationship-building, precision targeting) and the ROI discipline that a $50-200 package to an unqualified lead is malpractice — qualify the named accounts first, test small before scaling. Should note the deal size ($30k/year) justifies high-cost mailers. Should draw on the case library for inspiration (e.g. Corey's Unicorn Floatie Campaign for hard-to-reach marketing leaders, Antimetal's $15k pizza campaign converting ~75 customers and ~$1M revenue). Should apply the operating principles: think in stories not campaigns, and test small before going big.",
"assertions": [
"Routes to the guerrilla-marketing reference",
"Surfaces the direct-mail 3-rule framework (relevance, relationship-building, precision targeting)",
"Applies the ROI discipline (qualify before spending on high-cost packages)",
"Recommends testing small before scaling",
"Notes deal size justifies high-cost direct mail",
"Pulls named cases as inspiration (e.g. Unicorn Floatie, Antimetal pizza)",
"Applies 'think in stories, not campaigns' principle"
],
"files": []
}
]
}
FILE:references/guerrilla-marketing.md
# Guerrilla Marketing (#121)
Unconventional stunts that earn attention in unexpected places — then get turned into repeatable strategy. The point isn't the one-off spectacle. It's finding a memorable, low-cost way to break through, proving it works small, and running it again.
Two operating principles govern everything below:
- **Think in stories, not campaigns.** People don't share your funnel. They share a story worth retelling. Design the moment so the person who receives it wants to tell someone else.
- **Test small before going big.** A guerrilla idea is a hypothesis. Prove it on a handful of targets before you spend real money scaling it.
---
## Direct Mail: The 3-Rule Framework
Physical mail is the most under-used guerrilla channel in SaaS because most people do it lazily — blasting cheap swag at a bought list. Done right, it cuts through inbox noise precisely because nobody else shows up in the mailbox. Every direct-mail play must satisfy all three rules:
1. **Relevance** — The item connects to your product, your message, or something the recipient specifically cares about. A random branded mug is noise. A gift that lands a joke about their exact problem is a story.
2. **Relationship-building** — The mailer opens or deepens a relationship. It's a gift, not a pitch. If the recipient feels sold to, you've spent money to annoy someone.
3. **Precision targeting** — You know exactly who is receiving it and why. Direct mail is expensive per unit, so it only works when aimed at a short, hand-qualified list.
### The ROI discipline
A $50–200 package sent to an unqualified lead is malpractice. The math only works when the target is qualified enough that a single closed deal pays for dozens of packages.
- **Qualify before you spend.** Reserve high-cost mailers for named accounts and hard-to-reach decision-makers where the deal size justifies the cost.
- **Test small before scaling.** Send to a handful first. Measure reply and meeting rates. Only scale the version that actually earns responses.
### Corey's Unicorn Floatie Campaign
To reach marketing leaders who ignore cold email and screen their calls, Corey mailed **giant inflatable unicorn pool floaties** to a short list of hard-to-reach targets. The floatie is absurd, oversized, and impossible to ignore — it lands as a story, not a pitch (relevance + relationship), aimed at a hand-picked list where each closed deal dwarfs the package cost (precision + ROI discipline). Recipients remembered it, mentioned it, and replied.
---
## Case Library (inspiration fuel)
Steal the *pattern*, not the prop. Each of these turned a small, unconventional act into outsized attention or revenue.
- **WePay's 600-lb ice block** — At PayPal's developer conference, WePay dropped a 600-pound block of ice with money frozen inside and a sign reading **"PayPal Freezes Your Accounts."** A pointed, physical jab at a competitor's real weakness, staged exactly where the audience was.
- **Xero skywriting** — Xero paid for **skywriting over TechCrunch Disrupt**, hijacking a competitor-heavy event's attention from above without buying a booth.
- **Red Bull's Felix Baumgartner space jump** — A supersonic freefall from the edge of space drew **8 million concurrent live viewers** — a story so big it dwarfed any ad buy.
- **Antimetal's $15K pizza campaign** — Antimetal spent roughly **$15,000 sending ~1,000 pizzas** to target accounts, converting **~75 customers** and driving **~$1M in revenue**. Precision-targeted, story-worthy, and measured — the direct-mail framework at scale.
- **Arrows personalized memos** — Personalized, hand-crafted memos sent to specific prospects, treating each as an audience of one.
- **ProfitWell trading cards** — Custom trading cards that turned the team and community into collectible characters, making the brand fun to share.
- **Buzzsprout GIPHY library** — A branded library of GIFs on GIPHY, so the brand rode along inside other people's conversations for free.
- **Wistia "Gear Squad vs Dr. Boring"** — Wistia produced an original, over-the-top branded film — entertainment first, marketing second — because a story people actually want to watch travels further than an ad.
---
## How to Use This With a Client
1. **Pick the story.** What's the one memorable thing a target would retell? Start from the story, then reverse-engineer the tactic.
2. **Qualify the list.** For any high-cost play (direct mail especially), name the exact accounts and confirm the deal size justifies the spend.
3. **Run the 3-rule check** on physical mail: relevance, relationship-building, precision targeting. If it fails any one, redesign it.
4. **Test small.** Ship to a handful, measure replies/meetings/coverage, and only scale what earns a response.
5. **Turn the stunt into a system.** If it works, make it repeatable — a recurring mailer program, a series of films, an ongoing library — rather than a one-time spike.
*Source: Corey Haines, Founding Marketing, ch. 13.*
FILE:references/ideas-by-category.md
# The 139 Marketing Ideas
Complete list of proven marketing approaches organized by category.
## Contents
- Content & SEO (1-10)
- Competitor & Comparison (11-13)
- Free Tools & Engineering (14-22)
- Paid Advertising (23-34)
- Social Media & Community (35-44)
- Email Marketing (45-53)
- Partnerships & Programs (54-64)
- Events & Speaking (65-72)
- PR & Media (73-76)
- Launches & Promotions (77-86)
- Product-Led Growth (87-96)
- Content Formats (97-109)
- Unconventional & Creative (110-122)
- Platforms & Marketplaces (123-130)
- International & Localization (131-132)
- Developer & Technical (133-136)
- Audience-Specific (137-139)
## Content & SEO (1-10)
1. **Easy Keyword Ranking** - Target low-competition keywords where you can rank quickly. Find terms competitors overlook—niche variations, long-tail queries, emerging topics.
2. **SEO Audit** - Conduct comprehensive technical SEO audits of your own site and share findings publicly. Document fixes and improvements to build authority.
3. **Glossary Marketing** - Create comprehensive glossaries defining industry terms. Each term becomes an SEO-optimized page targeting "what is X" searches.
4. **Programmatic SEO** - Build template-driven pages at scale targeting keyword patterns. Location pages, comparison pages, integration pages—any pattern with search volume.
5. **Content Repurposing** - Transform one piece of content into multiple formats. Blog post becomes Twitter thread, YouTube video, podcast episode, infographic.
6. **Proprietary Data Content** - Leverage unique data from your product to create original research and reports. Data competitors can't replicate creates linkable assets.
7. **Internal Linking** - Strategic internal linking distributes authority and improves crawlability. Build topical clusters connecting related content.
8. **Content Refreshing** - Regularly update existing content with fresh data, examples, and insights. Refreshed content often outperforms new content.
9. **Knowledge Base SEO** - Optimize help documentation for search. Support articles targeting problem-solution queries capture users actively seeking solutions.
10. **Parasite SEO** - Publish content on high-authority platforms (Medium, LinkedIn, Substack) that rank faster than your own domain.
---
## Competitor & Comparison (11-13)
11. **Competitor Comparison Pages** - Create detailed comparison pages positioning your product against competitors. "[Your Product] vs [Competitor]" pages capture high-intent searchers.
12. **Marketing Jiu-Jitsu** - Turn competitor weaknesses into your strengths. When competitors raise prices, launch affordability campaigns.
13. **Competitive Ad Research** - Study competitor advertising through tools like SpyFu or Facebook Ad Library. Learn what messaging resonates.
---
## Free Tools & Engineering (14-22)
14. **Side Projects as Marketing** - Build small, useful tools related to your main product. Side projects attract users who may later convert.
15. **Engineering as Marketing** - Build free tools that solve real problems. Calculators, analyzers, generators—useful utilities that naturally lead to your paid product.
16. **Importers as Marketing** - Build import tools for competitor data. "Import from [Competitor]" reduces switching friction.
17. **Quiz Marketing** - Create interactive quizzes that engage users while qualifying leads. Personality quizzes, assessments, and diagnostic tools generate shares.
18. **Calculator Marketing** - Build calculators solving real problems—ROI calculators, pricing estimators, savings tools. Calculators attract links and rank well.
19. **Chrome Extensions** - Create browser extensions providing standalone value. Chrome Web Store becomes another distribution channel.
20. **Microsites** - Build focused microsites for specific campaigns, products, or audiences. Dedicated domains can rank faster.
21. **Scanners** - Build free scanning tools that audit or analyze something. Website scanners, security checkers, performance analyzers.
22. **Public APIs** - Open APIs enable developers to build on your platform, creating an ecosystem.
---
## Paid Advertising (23-34)
23. **Podcast Advertising** - Sponsor relevant podcasts to reach engaged audiences. Host-read ads perform especially well.
24. **Pre-targeting Ads** - Show awareness ads before launching direct response campaigns. Warm audiences convert better.
25. **Facebook Ads** - Meta's detailed targeting reaches specific audiences. Test creative variations and leverage retargeting.
26. **Instagram Ads** - Visual-first advertising for products with strong imagery. Stories and Reels ads capture attention.
27. **Twitter Ads** - Reach engaged professionals discussing industry topics. Promoted tweets and follower campaigns.
28. **LinkedIn Ads** - Target by job title, company size, and industry. Premium CPMs justified by B2B purchase intent.
29. **Reddit Ads** - Reach passionate communities with authentic messaging. Transparency wins on Reddit.
30. **Quora Ads** - Target users actively asking questions your product answers. Intent-rich environment.
31. **Google Ads** - Capture high-intent search queries. Brand terms, competitor terms, and category terms.
32. **YouTube Ads** - Video ads with detailed targeting. Pre-roll and discovery ads reach users consuming related content.
33. **Cross-Platform Retargeting** - Follow users across platforms with consistent messaging.
34. **Click-to-Messenger Ads** - Ads that open direct conversations rather than landing pages.
---
## Social Media & Community (35-44)
35. **Community Marketing** - Build and nurture communities around your product. Slack groups, Discord servers, Facebook groups.
36. **Quora Marketing** - Answer relevant questions with genuine expertise. Include product mentions where naturally appropriate.
37. **Reddit Keyword Research** - Mine Reddit for real language your audience uses. Discover pain points and desires.
38. **Reddit Marketing** - Participate authentically in relevant subreddits. Provide value first.
39. **LinkedIn Audience** - Build personal brands on LinkedIn for B2B reach. Thought leadership builds authority.
40. **Instagram Audience** - Visual storytelling for products with strong aesthetics. Behind-the-scenes and user stories.
41. **X Audience** - Build presence on X/Twitter through consistent value. Threads and insights grow followings.
42. **Short Form Video** - TikTok, Reels, and Shorts reach new audiences with snackable content.
43. **Engagement Pods** - Coordinate with peers to boost each other's content engagement.
44. **Comment Marketing** - Thoughtful comments on relevant content build visibility.
---
## Email Marketing (45-53)
45. **Mistake Email Marketing** - Send "oops" emails when something genuinely goes wrong. Authenticity generates engagement.
46. **Reactivation Emails** - Win back churned or inactive users with targeted campaigns.
47. **Founder Welcome Email** - Personal welcome emails from founders create connection.
48. **Dynamic Email Capture** - Smart email capture that adapts to user behavior. Exit intent, scroll depth triggers.
49. **Monthly Newsletters** - Consistent newsletters keep your brand top-of-mind.
50. **Inbox Placement** - Technical email optimization for deliverability. Authentication and list hygiene.
51. **Onboarding Emails** - Guide new users to activation with targeted sequences.
52. **Win-back Emails** - Re-engage churned users with compelling reasons to return.
53. **Trial Reactivation** - Expired trials aren't lost causes. Targeted campaigns can recover them.
---
## Partnerships & Programs (54-64)
54. **Affiliate Discovery Through Backlinks** - Find potential affiliates by analyzing who links to competitors.
55. **Influencer Whitelisting** - Run ads through influencer accounts for authentic reach.
56. **Reseller Programs** - Enable agencies to resell your product. White-label options create distribution partners.
57. **Expert Networks** - Build networks of certified experts who implement your product.
58. **Newsletter Swaps** - Exchange promotional mentions with complementary newsletters.
59. **Article Quotes** - Contribute expert quotes to journalists. HARO connects experts with writers.
60. **Pixel Sharing** - Partner with complementary companies to share remarketing audiences.
61. **Shared Slack Channels** - Create shared channels with partners and customers.
62. **Affiliate Program** - Structured commission programs for referrers.
63. **Integration Marketing** - Joint marketing with integration partners.
64. **Community Sponsorship** - Sponsor relevant communities, newsletters, or publications.
---
## Events & Speaking (65-72)
65. **Live Webinars** - Educational webinars demonstrate expertise while generating leads.
66. **Virtual Summits** - Multi-speaker online events attract audiences through varied perspectives.
67. **Roadshows** - Take your product on the road to meet customers directly.
68. **Local Meetups** - Host or attend local meetups in key markets.
69. **Meetup Sponsorship** - Sponsor relevant meetups to reach engaged local audiences.
70. **Conference Speaking** - Speak at industry conferences to reach engaged audiences.
71. **Conferences** - Host your own conference to become the center of your industry.
72. **Conference Sponsorship** - Sponsor relevant conferences for brand visibility.
---
## PR & Media (73-76)
73. **Media Acquisitions as Marketing** - Acquire newsletters, podcasts, or publications in your space.
74. **Press Coverage** - Pitch newsworthy stories to relevant publications.
75. **Fundraising PR** - Leverage funding announcements for press coverage.
76. **Documentaries** - Create documentary content exploring your industry or customers.
---
## Launches & Promotions (77-86)
77. **Black Friday Promotions** - Annual deals create urgency and acquisition spikes.
78. **Product Hunt Launch** - Structured Product Hunt launches reach early adopters.
79. **Early-Access Referrals** - Reward referrals with earlier access during launches.
80. **New Year Promotions** - New Year brings fresh budgets and goal-setting energy.
81. **Early Access Pricing** - Launch with discounted early access tiers.
82. **Product Hunt Alternatives** - Launch on BetaList, Launching Next, AlternativeTo.
83. **Twitter Giveaways** - Engagement-boosting giveaways that require follows or retweets.
84. **Giveaways** - Strategic giveaways attract attention and capture leads.
85. **Vacation Giveaways** - Grand prize giveaways generate massive engagement.
86. **Lifetime Deals** - One-time payment deals generate cash and users.
---
## Product-Led Growth (87-96)
87. **Powered By Marketing** - "Powered by [Your Product]" badges create free impressions.
88. **Free Migrations** - Offer free migration services from competitors.
89. **Contract Buyouts** - Pay to exit competitor contracts.
90. **One-Click Registration** - Minimize signup friction with OAuth options.
91. **In-App Upsells** - Strategic upgrade prompts within the product experience.
92. **Newsletter Referrals** - Built-in referral programs for newsletters.
93. **Viral Loops** - Product mechanics that naturally encourage sharing.
94. **Offboarding Flows** - Optimize cancellation flows to retain or learn.
95. **Concierge Setup** - White-glove onboarding for high-value accounts.
96. **Onboarding Optimization** - Continuous improvement of new user experience.
---
## Content Formats (97-109)
97. **Playlists as Marketing** - Create Spotify playlists for your audience.
98. **Template Marketing** - Offer free templates users can immediately use.
99. **Graphic Novel Marketing** - Transform complex stories into visual narratives.
100. **Promo Videos** - High-quality promotional videos showcase your product.
101. **Industry Interviews** - Interview customers, experts, and thought leaders.
102. **Social Screenshots** - Design shareable screenshot templates for social proof.
103. **Online Courses** - Educational courses establish authority while generating leads.
104. **Book Marketing** - Author a book establishing expertise in your domain.
105. **Annual Reports** - Publish annual reports showcasing industry data and trends.
106. **End of Year Wraps** - Personalized year-end summaries users want to share.
107. **Podcasts** - Launch a podcast reaching audiences during commutes.
108. **Changelogs** - Public changelogs showcase product momentum.
109. **Public Demos** - Live product demonstrations showing real usage.
---
## Unconventional & Creative (110-122)
110. **Awards as Marketing** - Create industry awards positioning your brand as tastemaker.
111. **Challenges as Marketing** - Launch viral challenges that spread organically.
112. **Reality TV Marketing** - Create reality-show style content following real customers.
113. **Controversy as Marketing** - Strategic positioning against industry norms.
114. **Moneyball Marketing** - Data-driven marketing finding undervalued channels.
115. **Curation as Marketing** - Curate valuable resources for your audience.
116. **Grants as Marketing** - Offer grants to customers or community members.
117. **Product Competitions** - Sponsor competitions using your product.
118. **Cameo Marketing** - Use Cameo celebrities for personalized messages.
119. **OOH Advertising** - Out-of-home advertising—billboards, transit ads.
120. **Marketing Stunts** - Bold, attention-grabbing marketing moments.
121. **Guerrilla Marketing** - Unconventional, low-cost marketing in unexpected places. Deep dive: direct-mail 3-rule framework (relevance / relationship / precision), ROI discipline, and a named case library in [guerrilla-marketing.md](guerrilla-marketing.md).
122. **Humor Marketing** - Use humor to stand out and create memorability.
---
## Platforms & Marketplaces (123-130)
123. **Open Source as Marketing** - Open-source components or tools build developer goodwill.
124. **App Store Optimization** - Optimize app store listings for discoverability.
125. **App Marketplaces** - List in Salesforce AppExchange, Shopify App Store, etc.
126. **YouTube Reviews** - Get YouTubers to review your product.
127. **YouTube Channel** - Build a YouTube presence with tutorials and thought leadership.
128. **Source Platforms** - Submit to G2, Capterra, GetApp, and similar directories.
129. **Review Sites** - Actively manage presence on review platforms.
130. **Live Audio** - Host Twitter Spaces, Clubhouse, or LinkedIn Audio discussions.
---
## International & Localization (131-132)
131. **International Expansion** - Expand to new geographic markets with localization.
132. **Price Localization** - Adjust pricing for local purchasing power.
---
## Developer & Technical (133-136)
133. **Investor Marketing** - Market to investors for portfolio introductions.
134. **Certifications** - Create certification programs validating expertise.
135. **Support as Marketing** - Exceptional support creates stories customers share.
136. **Developer Relations** - Build relationships with developer communities.
---
## Audience-Specific (137-139)
137. **Two-Sided Referrals** - Reward both referrer and referred.
138. **Podcast Tours** - Guest on multiple podcasts reaching your target audience.
139. **Customer Language** - Use the exact words your customers use in marketing.
Quản trị, tìm kiếm và kích hoạt nhanh Skill, Agent, Command, Tool; tạo skill/agent mới và kiểm tra tính toàn vẹn workspace.
--- name: workspace-manager description: Quản trị, điều hướng, tìm kiếm và kích hoạt nhanh các Skill, Agent, Command và Tool trong workspace. Hỗ trợ scaffolding tạo skill/agent mới, liên kết đa tác nhân và kiểm tra tính toàn vẹn của hệ thống. Dùng khi nói "workspace", "tìm skill", "gợi ý agent", "tạo skill mới", "hướng dẫn workspace". --- # Workspace Manager & Navigator (Điều Phối Workspace) Bạn là chuyên gia điều phối và quản trị hệ thống AI Agent & Skills Workspace. Mục tiêu của bạn là giúp người dùng khai thác tối đa sức mạnh của hơn 250+ Skills, 39+ Agents, 41 Commands và 51 CLI Tools trong kho tài nguyên này. --- ## 1. Bản đồ điều hướng nhanh theo nhu cầu (Intent Routing Map) Khi người dùng đưa ra một vấn đề, hãy tự động nhận diện và kích hoạt đúng Skill / Agent theo bảng sau: | Nhu cầu của người dùng | Skill đề xuất | Agent đề xuất | File tài liệu | |---|---|---|---| | **Lên kế hoạch, quản lý thời gian, việc bị quá tải** | `lap-ke-hoach` | `planner` | [`agents/vietnamese/planner.md`](../../agents/vietnamese/planner.md) | | **Kiểm tra chất lượng bài viết, kế hoạch, tính khả thi** | `qa-reviewer` | `qa-reviewer` | [`agents/vietnamese/qa-reviewer.md`](../../agents/vietnamese/qa-reviewer.md) | | **Vận hành Shopee, TikTok Shop, Web, Facebook** | `van-hanh-tmdt-da-kenh` | `growth-strategist` | [`skills/van-hanh-tmdt-da-kenh/SKILL.md`](../van-hanh-tmdt-da-kenh/SKILL.md) | | **Xây kênh TikTok, làm thương hiệu cá nhân** | `xay-dung-thuong-hieu-ca-nhan` | `content-creator` | [`skills/xay-dung-thuong-hieu-ca-nhan/SKILL.md`](../xay-dung-thuong-hieu-ca-nhan/SKILL.md) | | **Phân tích quy trình, cơ cấu tổ chức, KPI/OKR** | `phan-tich-nghiep-vu-quan-tri-doanh-nghiep` | `product-strategist` | [`skills/phan-tich-nghiep-vu-quan-tri-doanh-nghiep/SKILL.md`](../phan-tich-nghiep-vu-quan-tri-doanh-nghiep/SKILL.md) | | **Đọc hiểu tài liệu dài, học kiến thức mới** | `hoc-tap-nghien-cuu` | (Feynman Tutor) | [`skills/hoc-tap-nghien-cuu/SKILL.md`](../hoc-tap-nghien-cuu/SKILL.md) | | **Nghiên cứu nhanh một công nghệ hoặc thị trường** | `research-nhanh` | `research-summarizer` | [`skills/research-nhanh/SKILL.md`](../research-nhanh/SKILL.md) | | **Quản lý thu chi, lập ngân sách cá nhân** | `tai-chinh-ca-nhan` | `financial-analyst` | [`skills/tai-chinh-ca-nhan/SKILL.md`](../tai-chinh-ca-nhan/SKILL.md) | | **Viết code backend, thiết kế API, cơ sở dữ liệu** | `senior-backend` | `cs-backend-engineer` | [`engineering-team/skills/senior-backend/SKILL.md`](../../engineering-team/skills/senior-backend/SKILL.md) | | **Viết code frontend, UI/UX hiện đại** | `senior-frontend` | `cs-frontend-engineer` | [`engineering-team/skills/senior-frontend/SKILL.md`](../../engineering-team/skills/senior-frontend/SKILL.md) | | **Rà soát code tối giản, loại bỏ over-engineering** | `karpathy-coder` | `cs-karpathy-reviewer` | [`engineering/karpathy-coder/skills/karpathy-coder/SKILL.md`](../../engineering/karpathy-coder/skills/karpathy-coder/SKILL.md) | | **Viết PRD, phân tích User Stories** | `code-to-prd` | `cs-agile-product-owner` | [`product-team/skills/code-to-prd/SKILL.md`](../../product-team/skills/code-to-prd/SKILL.md) | | **Kiểm toán SEO, tối ưu thứ hạng website** | `seo-audit` | `cs-aeo` | [`marketing-skill/skills/seo-audit/SKILL.md`](../../marketing-skill/skills/seo-audit/SKILL.md) | --- ## 2. Quy trình điều phối Đa tác nhân (Multi-Agent Coordination) Khi xử lý bài toán lớn, hãy tuân theo quy tắc 3 bước: 1. **Persona Selection**: Chọn đúng vai trò người tư duy (`agents/personas/` hoặc `agents/vietnamese/`). 2. **Skill Chaining**: Xâu chuỗi các skill thực thi theo thứ tự logic (ví dụ: `research-nhanh` ➡️ `copywriting` ➡️ `seo-audit`). 3. **Quality Gate**: Luôn yêu cầu kiểm định đầu ra theo tiêu chuẩn của `qa-reviewer` (Logic, Bối cảnh, Khả thi, Giả định). --- ## 3. Hướng dẫn Scaffolding tạo Skill hoặc Agent mới ### A. Mẫu tạo Skill mới (`skills/<ten-skill>/SKILL.md`): ```markdown --- name: ten-skill-kebab-case description: Mô tả ngắn gọn (1-2 câu) nêu rõ kỹ năng làm gì và từ khóa kích hoạt. --- # Tên Kỹ Năng ## Mục tiêu [Mục tiêu cụ thể giúp người dùng đạt được kết quả gì] ## Khi nào dùng - [Tình huống 1] - [Tình huống 2] ## Đầu vào cần cung cấp - [Thông tin đầu vào 1] - [Thông tin đầu vào 2] ## Quy trình xử lý 1. [Bước 1] 2. [Bước 2] 3. [Bước 3] ## Tiêu chuẩn đầu ra - [Định dạng và chất lượng kết quả] ## Tránh (Anti-patterns) - [Những sai lầm cần tránh] ``` ### B. Mẫu tạo Agent mới (`agents/<category>/cs-<ten-agent>.md`): ```markdown # [Tên Agent] ## Vai trò [Định vị chuyên gia, phong cách và trách nhiệm chính] ## Nhiệm vụ cốt lõi - [Nhiệm vụ 1] - [Nhiệm vụ 2] ## Đầu vào & Đầu ra - Đầu vào: [Thông tin cần nhận] - Đầu ra: [Sản phẩm giao nộp] ## Phối hợp & Tiêu chí đánh giá - Phối hợp với: [Các Agent / Skill liên quan] - Tiêu chí chất lượng: [Chuẩn đánh giá] ``` --- ## 4. Tài liệu tham khảo & Mục lục tra cứu - 📖 [Cẩm nang toàn diện Master Handbook](../../HANDBOOK.md) - 📂 [Danh mục 50 Core Skills](../README.md) - 🤖 [Danh mục 39+ Agents](../../agents/README.md) - ⚡ [Danh mục 41 Slash Commands](../../commands/README.md) - 🛠️ [Danh bạ 51 Tools & Integrations](../../tools/README.md)
Chạy kiểm thử trên BrowserStack: kiểm thử đa trình duyệt, đám mây và tương thích trình duyệt.
---
name: "browserstack"
description: >-
Run tests on BrowserStack. Use when user mentions "browserstack",
"cross-browser", "cloud testing", "browser matrix", "test on safari",
"test on firefox", or "browser compatibility".
---
# BrowserStack Integration
Run Playwright tests on BrowserStack's cloud grid for cross-browser and cross-device testing.
## Prerequisites
Environment variables must be set:
- `BROWSERSTACK_USERNAME` — your BrowserStack username
- `BROWSERSTACK_ACCESS_KEY` — your access key
If not set, inform the user how to get them from [browserstack.com/accounts/settings](https://www.browserstack.com/accounts/settings) and stop.
## Capabilities
### 1. Configure for BrowserStack
```
/pw:browserstack setup
```
Steps:
1. Check current `playwright.config.ts`
2. Add BrowserStack connect options:
```typescript
// Add to playwright.config.ts
import { defineConfig } from '@playwright/test';
const isBS = !!process.env.BROWSERSTACK_USERNAME;
export default defineConfig({
// ... existing config
projects: isBS ? [
{
name: "chromelatestwindows-11",
use: {
connectOptions: {
wsEndpoint: `wss://cdp.browserstack.com/playwright?caps='chrome',
'browser_version': 'latest',
'os': 'Windows',
'os_version': '11',
'browserstack.username': process.env.BROWSERSTACK_USERNAME,
'browserstack.accessKey': process.env.BROWSERSTACK_ACCESS_KEY,))}`,
},
},
},
{
name: "firefoxlatestwindows-11",
use: {
connectOptions: {
wsEndpoint: `wss://cdp.browserstack.com/playwright?caps='playwright-firefox',
'browser_version': 'latest',
'os': 'Windows',
'os_version': '11',
'browserstack.username': process.env.BROWSERSTACK_USERNAME,
'browserstack.accessKey': process.env.BROWSERSTACK_ACCESS_KEY,))}`,
},
},
},
{
name: "webkitlatestos-x-ventura",
use: {
connectOptions: {
wsEndpoint: `wss://cdp.browserstack.com/playwright?caps='playwright-webkit',
'browser_version': 'latest',
'os': 'OS X',
'os_version': 'Ventura',
'browserstack.username': process.env.BROWSERSTACK_USERNAME,
'browserstack.accessKey': process.env.BROWSERSTACK_ACCESS_KEY,))}`,
},
},
},
] : [
// ... local projects fallback
],
});
```
3. Add npm script: `"test:e2e:cloud": "npx playwright test --project='chrome@*' --project='firefox@*' --project='webkit@*'"`
### 2. Run Tests on BrowserStack
```
/pw:browserstack run
```
Steps:
1. Verify credentials are set
2. Run tests with BrowserStack projects:
```bash
BROWSERSTACK_USERNAME=$BROWSERSTACK_USERNAME \
BROWSERSTACK_ACCESS_KEY=$BROWSERSTACK_ACCESS_KEY \
npx playwright test --project='chrome@*' --project='firefox@*'
```
3. Monitor execution
4. Report results per browser
### 3. Get Build Results
```
/pw:browserstack results
```
Steps:
1. Call `browserstack_get_builds` MCP tool
2. Get latest build's sessions
3. For each session:
- Status (pass/fail)
- Browser and OS
- Duration
- Video URL
- Log URLs
4. Format as summary table
### 4. Check Available Browsers
```
/pw:browserstack browsers
```
Steps:
1. Call `browserstack_get_browsers` MCP tool
2. Filter for Playwright-compatible browsers
3. Display available browser/OS combinations
### 5. Local Testing
```
/pw:browserstack local
```
For testing localhost or staging behind firewall:
1. Install BrowserStack Local: `npm install -D browserstack-local`
2. Add local tunnel to config
3. Provide setup instructions
## MCP Tools Used
| Tool | When |
|---|---|
| `browserstack_get_plan` | Check account limits |
| `browserstack_get_browsers` | List available browsers |
| `browserstack_get_builds` | List recent builds |
| `browserstack_get_sessions` | Get sessions in a build |
| `browserstack_get_session` | Get session details (video, logs) |
| `browserstack_update_session` | Mark pass/fail |
| `browserstack_get_logs` | Get text/network logs |
## Output
- Cross-browser test results table
- Per-browser pass/fail status
- Links to BrowserStack dashboard for video/screenshots
- Any browser-specific failures highlighted
Lập kế hoạch, sơ đồ và tái cấu trúc phân cấp trang, điều hướng, cấu trúc URL và liên kết nội bộ cho website.
---
name: site-architecture
description: When the user wants to plan, map, or restructure their website's page hierarchy, navigation, URL structure, or internal linking. Also use when the user mentions "sitemap," "site map," "visual sitemap," "site structure," "page hierarchy," "information architecture," "IA," "navigation design," "URL structure," "breadcrumbs," "internal linking strategy," "website planning," "what pages do I need," "how should I organize my site," or "site navigation." Use this whenever someone is planning what pages a website should have and how they connect. NOT for XML sitemaps (that's technical SEO — see seo-audit). For SEO audits, see seo-audit. For structured data, see schema.
metadata:
version: 2.0.0
---
# Site Architecture
You are an information architecture expert. Your goal is to help plan website structure — page hierarchy, navigation, URL patterns, and internal linking — so the site is intuitive for users and optimized for search engines.
## Before Planning
**Check for product marketing context first:**
If `.agents/product-marketing.md` exists (or `.claude/product-marketing.md`, or the legacy `product-marketing-context.md` filename, in older setups), read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Gather this context (ask if not provided):
### 1. Business Context
- What does the company do?
- Who are the primary audiences?
- What are the top 3 goals for the site? (conversions, SEO traffic, education, support)
### 2. Current State
- New site or restructuring an existing one?
- If restructuring: what's broken? (high bounce, poor SEO, users can't find things)
- Existing URLs that must be preserved (for redirects)?
### 3. Site Type
- SaaS marketing site
- Content/blog site
- E-commerce
- Documentation
- Hybrid (SaaS + content)
- Small business / local
### 4. Content Inventory
- How many pages exist or are planned?
- What are the most important pages? (by traffic, conversions, or business value)
- Any planned sections or expansions?
---
## Site Types and Starting Points
| Site Type | Typical Depth | Key Sections | URL Pattern |
|-----------|--------------|--------------|-------------|
| SaaS marketing | 2-3 levels | Home, Features, Pricing, Blog, Docs | `/features/name`, `/blog/slug` |
| Content/blog | 2-3 levels | Home, Blog, Categories, About | `/blog/slug`, `/category/slug` |
| E-commerce | 3-4 levels | Home, Categories, Products, Cart | `/category/subcategory/product` |
| Documentation | 3-4 levels | Home, Guides, API Reference | `/docs/section/page` |
| Hybrid SaaS+content | 3-4 levels | Home, Product, Blog, Resources, Docs | `/product/feature`, `/blog/slug` |
| Small business | 1-2 levels | Home, Services, About, Contact | `/services/name` |
**For full page hierarchy templates**: See [references/site-type-templates.md](references/site-type-templates.md)
---
## Page Hierarchy Design
### The 3-Click Rule
Users should reach any important page within 3 clicks from the homepage. This isn't absolute, but if critical pages are buried 4+ levels deep, something is wrong.
### Flat vs Deep
| Approach | Best For | Tradeoff |
|----------|----------|----------|
| Flat (2 levels) | Small sites, portfolios | Simple but doesn't scale |
| Moderate (3 levels) | Most SaaS, content sites | Good balance of depth and findability |
| Deep (4+ levels) | E-commerce, large docs | Scales but risks burying content |
**Rule of thumb**: Go as flat as possible while keeping navigation clean. If a nav dropdown has 20+ items, add a level of hierarchy.
### Hierarchy Levels
| Level | What It Is | Example |
|-------|-----------|---------|
| L0 | Homepage | `/` |
| L1 | Primary sections | `/features`, `/blog`, `/pricing` |
| L2 | Section pages | `/features/analytics`, `/blog/seo-guide` |
| L3+ | Detail pages | `/docs/api/authentication` |
### ASCII Tree Format
Use this format for page hierarchies:
```
Homepage (/)
├── Features (/features)
│ ├── Analytics (/features/analytics)
│ ├── Automation (/features/automation)
│ └── Integrations (/features/integrations)
├── Pricing (/pricing)
├── Blog (/blog)
│ ├── [Category: SEO] (/blog/category/seo)
│ └── [Category: CRO] (/blog/category/cro)
├── Resources (/resources)
│ ├── Case Studies (/resources/case-studies)
│ └── Templates (/resources/templates)
├── Docs (/docs)
│ ├── Getting Started (/docs/getting-started)
│ └── API Reference (/docs/api)
├── About (/about)
│ └── Careers (/about/careers)
└── Contact (/contact)
```
**When to use ASCII vs Mermaid**:
- ASCII: quick hierarchy drafts, text-only contexts, simple structures
- Mermaid: visual presentations, complex relationships, showing nav zones or linking patterns
---
## Navigation Design
### Navigation Types
| Nav Type | Purpose | Placement |
|----------|---------|-----------|
| Header nav | Primary navigation, always visible | Top of every page |
| Dropdown menus | Organize sub-pages under parent | Expands from header items |
| Footer nav | Secondary links, legal, sitemap | Bottom of every page |
| Sidebar nav | Section navigation (docs, blog) | Left side within a section |
| Breadcrumbs | Show current location in hierarchy | Below header, above content |
| Contextual links | Related content, next steps | Within page content |
### Header Navigation Rules
- **4-7 items max** in the primary nav (more causes decision paralysis)
- **CTA button** goes rightmost (e.g., "Start Free Trial," "Get Started")
- **Logo** links to homepage (left side)
- **Order by priority**: most important/visited pages first
- If you have a mega menu, limit to 3-4 columns
### Footer Organization
Group footer links into columns:
- **Product**: Features, Pricing, Integrations, Changelog
- **Resources**: Blog, Case Studies, Templates, Docs
- **Company**: About, Careers, Contact, Press
- **Legal**: Privacy, Terms, Security
### Breadcrumb Format
```
Home > Features > Analytics
Home > Blog > SEO Category > Post Title
```
Breadcrumbs should mirror the URL hierarchy. Every breadcrumb segment should be a clickable link except the current page.
**For detailed navigation patterns**: See [references/navigation-patterns.md](references/navigation-patterns.md)
---
## URL Structure
### Design Principles
1. **Readable by humans** — `/features/analytics` not `/f/a123`
2. **Hyphens, not underscores** — `/blog/seo-guide` not `/blog/seo_guide`
3. **Reflect the hierarchy** — URL path should match site structure
4. **Consistent trailing slash policy** — pick one (with or without) and enforce it
5. **Lowercase always** — `/About` should redirect to `/about`
6. **Short but descriptive** — `/blog/how-to-improve-landing-page-conversion-rates` is too long; `/blog/landing-page-conversions` is better
### URL Patterns by Page Type
| Page Type | Pattern | Example |
|-----------|---------|---------|
| Homepage | `/` | `example.com` |
| Feature page | `/features/{name}` | `/features/analytics` |
| Pricing | `/pricing` | `/pricing` |
| Blog post | `/blog/{slug}` | `/blog/seo-guide` |
| Blog category | `/blog/category/{slug}` | `/blog/category/seo` |
| Case study | `/customers/{slug}` | `/customers/acme-corp` |
| Documentation | `/docs/{section}/{page}` | `/docs/api/authentication` |
| Legal | `/{page}` | `/privacy`, `/terms` |
| Landing page | `/{slug}` or `/lp/{slug}` | `/free-trial`, `/lp/webinar` |
| Comparison | `/compare/{competitor}` or `/vs/{competitor}` | `/compare/competitor-name` |
| Integration | `/integrations/{name}` | `/integrations/slack` |
| Template | `/templates/{slug}` | `/templates/marketing-plan` |
### Common Mistakes
- **Dates in blog URLs** — `/blog/2024/01/15/post-title` adds no value and makes URLs long. Use `/blog/post-title`.
- **Over-nesting** — `/products/category/subcategory/item/detail` is too deep. Flatten where possible.
- **Changing URLs without redirects** — Every old URL needs a 301 redirect to its new URL. Without them, you lose backlink equity and create broken pages for anyone with the old URL bookmarked or linked.
- **IDs in URLs** — `/product/12345` is not human-readable. Use slugs.
- **Query parameters for content** — `/blog?id=123` should be `/blog/post-title`.
- **Inconsistent patterns** — Don't mix `/features/analytics` and `/product/automation`. Pick one parent.
### Breadcrumb-URL Alignment
The breadcrumb trail should mirror the URL path:
| URL | Breadcrumb |
|-----|-----------|
| `/features/analytics` | Home > Features > Analytics |
| `/blog/seo-guide` | Home > Blog > SEO Guide |
| `/docs/api/auth` | Home > Docs > API > Authentication |
---
## Visual Sitemap Output (Mermaid)
Use Mermaid `graph TD` for visual sitemaps. This makes hierarchy relationships clear and can annotate navigation zones.
### Basic Hierarchy
```mermaid
graph TD
HOME[Homepage] --> FEAT[Features]
HOME --> PRICE[Pricing]
HOME --> BLOG[Blog]
HOME --> ABOUT[About]
FEAT --> F1[Analytics]
FEAT --> F2[Automation]
FEAT --> F3[Integrations]
BLOG --> B1[Post 1]
BLOG --> B2[Post 2]
```
### With Navigation Zones
```mermaid
graph TD
subgraph Header Nav
HOME[Homepage]
FEAT[Features]
PRICE[Pricing]
BLOG[Blog]
CTA[Get Started]
end
subgraph Footer Nav
ABOUT[About]
CAREERS[Careers]
CONTACT[Contact]
PRIVACY[Privacy]
end
HOME --> FEAT
HOME --> PRICE
HOME --> BLOG
HOME --> ABOUT
FEAT --> F1[Analytics]
FEAT --> F2[Automation]
```
**For more Mermaid templates**: See [references/mermaid-templates.md](references/mermaid-templates.md)
---
## Internal Linking Strategy
### Link Types
| Type | Purpose | Example |
|------|---------|---------|
| Navigational | Move between sections | Header, footer, sidebar links |
| Contextual | Related content within text | "Learn more about [analytics](/features/analytics)" |
| Hub-and-spoke | Connect cluster content to hub | Blog posts linking to pillar page |
| Cross-section | Connect related pages across sections | Feature page linking to related case study |
### Internal Linking Rules
1. **No orphan pages** — every page must have at least one internal link pointing to it
2. **Descriptive anchor text** — "our analytics features" not "click here"
3. **5-10 internal links per 1000 words** of content (approximate guideline)
4. **Link to important pages more often** — homepage, key feature pages, pricing
5. **Use breadcrumbs** — free internal links on every page
6. **Related content sections** — "Related Posts" or "You might also like" at page bottom
### Hub-and-Spoke Model
For content-heavy sites, organize around hub pages:
```
Hub: /blog/seo-guide (comprehensive overview)
├── Spoke: /blog/keyword-research (links back to hub)
├── Spoke: /blog/on-page-seo (links back to hub)
├── Spoke: /blog/technical-seo (links back to hub)
└── Spoke: /blog/link-building (links back to hub)
```
Each spoke links back to the hub. The hub links to all spokes. Spokes link to each other where relevant.
### Link Audit Checklist
- [ ] Every page has at least one inbound internal link
- [ ] No broken internal links (404s)
- [ ] Anchor text is descriptive (not "click here" or "read more")
- [ ] Important pages have the most inbound internal links
- [ ] Breadcrumbs are implemented on all pages
- [ ] Related content links exist on blog posts
- [ ] Cross-section links connect features to case studies, blog to product pages
---
## Output Format
When creating a site architecture plan, provide these deliverables:
### 1. Page Hierarchy (ASCII Tree)
Full site structure with URLs at each node. Use the ASCII tree format from the Page Hierarchy Design section.
### 2. Visual Sitemap (Mermaid)
Mermaid diagram showing page relationships and navigation zones. Use `graph TD` with subgraphs for nav zones where helpful.
### 3. URL Map Table
| Page | URL | Parent | Nav Location | Priority |
|------|-----|--------|-------------|----------|
| Homepage | `/` | — | Header | High |
| Features | `/features` | Homepage | Header | High |
| Analytics | `/features/analytics` | Features | Header dropdown | Medium |
| Pricing | `/pricing` | Homepage | Header | High |
| Blog | `/blog` | Homepage | Header | Medium |
### 4. Navigation Spec
- Header nav items (ordered, with CTA)
- Footer sections and links
- Sidebar nav (if applicable)
- Breadcrumb implementation notes
### 5. Internal Linking Plan
- Hub pages and their spokes
- Cross-section link opportunities
- Orphan page audit (if restructuring)
- Recommended links per key page
---
## Task-Specific Questions
1. Is this a new site or are you restructuring an existing one?
2. What type of site is it? (SaaS, content, e-commerce, docs, hybrid, small business)
3. How many pages exist or are planned?
4. What are the 5 most important pages on the site?
5. Are there existing URLs that need to be preserved or redirected?
6. Who are the primary audiences, and what are they trying to accomplish on the site?
---
## Related Skills
- **content-strategy**: For planning what content to create and topic clusters
- **programmatic-seo**: For building SEO pages at scale with templates and data
- **seo-audit**: For technical SEO, on-page optimization, and indexation issues
- **cro**: For optimizing individual pages for conversion
- **schema**: For implementing breadcrumb and site navigation structured data
- **competitors**: For comparison page frameworks and URL patterns
FILE:evals/evals.json
{
"skill_name": "site-architecture",
"evals": [
{
"id": 1,
"prompt": "Help me plan the site architecture for our new SaaS marketing website. We have a homepage, product page, pricing page, about page, blog, and want to add competitor comparison pages and integration pages.",
"expected_output": "Should check for product-marketing.md first. Should apply the page hierarchy design principles (3-click rule, flat vs deep). Should create an ASCII tree showing the full site structure. Should organize pages logically: main nav (Home, Product, Pricing, About, Blog), comparison pages section, integrations hub. Should recommend URL structure patterns for each section. Should provide navigation design recommendations (4-7 header items). Should include internal linking strategy (hub-and-spoke for comparisons and integrations). Should provide the full deliverable set: hierarchy, URL map, nav spec.",
"assertions": [
"Checks for product-marketing.md",
"Applies 3-click rule and flat vs deep principles",
"Creates ASCII tree for site structure",
"Organizes pages logically",
"Recommends URL structure for each section",
"Provides navigation design (4-7 header items)",
"Includes internal linking strategy",
"Provides hierarchy, URL map, and nav spec"
],
"files": []
},
{
"id": 2,
"prompt": "Our website has grown organically and the navigation is a mess. We have 50+ pages and users can't find anything. Help us reorganize.",
"expected_output": "Should treat this as a site architecture audit and redesign. Should recommend starting with a content inventory of all 50+ pages. Should apply the page hierarchy design to reorganize: group related pages, establish clear parent-child relationships, apply the 3-click rule. Should redesign the navigation (reduce header items, use mega-menu or dropdowns for deeper pages). Should provide before/after ASCII tree structure. Should address URL redirects for any pages that move. Should include a visual sitemap (Mermaid).",
"assertions": [
"Recommends content inventory first",
"Groups related pages logically",
"Applies 3-click rule",
"Redesigns navigation structure",
"Provides ASCII tree or visual sitemap",
"Addresses URL redirects for moved pages",
"Reduces header navigation items"
],
"files": []
},
{
"id": 3,
"prompt": "what should our url structure look like? we keep debating between /blog/post-name vs /resources/blog/post-name and /product/feature vs /features/feature-name",
"expected_output": "Should trigger on casual phrasing. Should apply the URL structure patterns guidance. Should recommend clean, descriptive URLs: prefer shorter paths (/blog/post-name over /resources/blog/post-name), use consistent patterns, avoid unnecessary nesting. Should provide URL structure recommendations for each section type (blog, features, comparisons, integrations). Should address SEO implications of URL structure. Should provide a complete URL map as a reference.",
"assertions": [
"Triggers on casual phrasing",
"Applies URL structure patterns",
"Recommends shorter, cleaner paths",
"Provides recommendations for each section type",
"Addresses SEO implications",
"Provides URL map reference"
],
"files": []
},
{
"id": 4,
"prompt": "We're adding programmatic SEO pages — 200 integration pages and 50 comparison pages. How should these fit into our site architecture?",
"expected_output": "Should address how to integrate scaled content into the site architecture. Should recommend hub pages for both sections (/integrations and /compare or /vs). Should apply the hub-and-spoke internal linking model. Should address navigation: these shouldn't clutter the main nav, but should be accessible via hub pages. Should provide URL structure for both sections. Should address crawl budget considerations for 250 new pages. Should cross-reference programmatic-seo for the content strategy.",
"assertions": [
"Recommends hub pages for each section",
"Applies hub-and-spoke internal linking",
"Keeps programmatic pages out of main nav",
"Provides URL structure for both sections",
"Addresses crawl budget for 250 pages",
"Cross-references programmatic-seo skill"
],
"files": []
},
{
"id": 5,
"prompt": "Can you create a visual sitemap for our site? We want something we can share with our design team.",
"expected_output": "Should provide a visual sitemap using Mermaid diagram format. Should organize the sitemap hierarchically showing page relationships. Should use the Mermaid graph syntax that can be rendered by most tools. Should include all major sections and key pages. Should be clear enough for a design team to use as a reference for navigation and wireframing.",
"assertions": [
"Provides visual sitemap in Mermaid format",
"Shows hierarchical page relationships",
"Includes all major sections",
"Uses clear, readable format",
"Suitable for sharing with design team"
],
"files": []
},
{
"id": 6,
"prompt": "Our XML sitemap hasn't been updated in 6 months and we have crawl errors in Search Console. Can you fix our technical SEO?",
"expected_output": "Should recognize this is a technical SEO audit task, not a site architecture design task. Should defer to or cross-reference the seo-audit skill, which handles XML sitemaps, crawl errors, and technical SEO issues. Site-architecture focuses on page hierarchy, navigation, and URL structure design — not technical SEO troubleshooting.",
"assertions": [
"Recognizes this as technical SEO, not site architecture",
"References or defers to seo-audit skill",
"Explains site-architecture covers design, not technical SEO"
],
"files": []
}
]
}
FILE:references/mermaid-templates.md
# Mermaid Diagram Templates
Copy-paste-ready Mermaid diagrams for visual sitemaps. Customize node labels and connections for your site.
---
## Basic Hierarchy
Simple top-down page hierarchy.
```mermaid
graph TD
HOME["Homepage<br/>/"] --> FEAT["Features<br/>/features"]
HOME --> PRICE["Pricing<br/>/pricing"]
HOME --> BLOG["Blog<br/>/blog"]
HOME --> ABOUT["About<br/>/about"]
FEAT --> F1["Analytics<br/>/features/analytics"]
FEAT --> F2["Automation<br/>/features/automation"]
FEAT --> F3["Integrations<br/>/features/integrations"]
BLOG --> B1["Post: SEO Guide<br/>/blog/seo-guide"]
BLOG --> B2["Post: CRO Tips<br/>/blog/cro-tips"]
```
---
## Hierarchy with Navigation Zones
Uses subgraphs to show which pages appear in which navigation area.
```mermaid
graph TD
subgraph "Header Nav"
HOME["Homepage"]
FEAT["Features"]
PRICE["Pricing"]
BLOG["Blog"]
CTA["Get Started ★"]
end
subgraph "Feature Pages"
F1["Analytics"]
F2["Automation"]
F3["Integrations"]
end
subgraph "Footer Nav"
ABOUT["About"]
CAREERS["Careers"]
CONTACT["Contact"]
PRIVACY["Privacy"]
TERMS["Terms"]
end
HOME --> FEAT
HOME --> PRICE
HOME --> BLOG
FEAT --> F1
FEAT --> F2
FEAT --> F3
HOME --> ABOUT
ABOUT --> CAREERS
HOME --> CONTACT
```
---
## Hierarchy with URL Labels
Each node shows the page name and URL path.
```mermaid
graph TD
HOME["Homepage<br/><small>/</small>"] --> PROD["Product<br/><small>/product</small>"]
HOME --> PRICE["Pricing<br/><small>/pricing</small>"]
HOME --> BLOG["Blog<br/><small>/blog</small>"]
HOME --> DOCS["Docs<br/><small>/docs</small>"]
HOME --> ABOUT["About<br/><small>/about</small>"]
PROD --> P1["Analytics<br/><small>/product/analytics</small>"]
PROD --> P2["Reports<br/><small>/product/reports</small>"]
DOCS --> D1["Getting Started<br/><small>/docs/getting-started</small>"]
DOCS --> D2["API Reference<br/><small>/docs/api</small>"]
```
---
## Hub-and-Spoke Content Model
Shows a hub page connected to spoke articles, with spokes linking to each other.
```mermaid
graph TD
HUB["SEO Guide<br/>(Hub Page)"]
HUB --> S1["Keyword Research"]
HUB --> S2["On-Page SEO"]
HUB --> S3["Technical SEO"]
HUB --> S4["Link Building"]
S1 -.-> S2
S2 -.-> S3
S3 -.-> S4
style HUB fill:#f9f,stroke:#333,stroke-width:2px
```
Legend:
- Solid lines = primary hub-spoke links
- Dashed lines = cross-links between spokes
---
## Internal Linking Flow
Shows how different site sections link to each other.
```mermaid
graph LR
subgraph "Marketing"
HOME["Homepage"]
FEAT["Features"]
PRICE["Pricing"]
end
subgraph "Content"
BLOG["Blog"]
GUIDE["Guides"]
CASE["Case Studies"]
end
subgraph "Product"
DOCS["Docs"]
API["API Ref"]
CHANGE["Changelog"]
end
BLOG --> FEAT
BLOG --> CASE
CASE --> FEAT
CASE --> PRICE
FEAT --> DOCS
GUIDE --> BLOG
GUIDE --> DOCS
HOME --> FEAT
HOME --> BLOG
HOME --> CASE
```
---
## Before/After Restructuring
Compare current and proposed site structures side by side.
```mermaid
graph TD
subgraph "Before"
B_HOME["Homepage"] --> B_P1["Page 1"]
B_HOME --> B_P2["Page 2"]
B_HOME --> B_P3["Page 3"]
B_HOME --> B_P4["Page 4"]
B_HOME --> B_P5["Page 5"]
B_HOME --> B_P6["Page 6"]
B_HOME --> B_P7["Page 7"]
B_HOME --> B_P8["Page 8"]
end
subgraph "After"
A_HOME["Homepage"] --> A_S1["Features"]
A_HOME --> A_S2["Resources"]
A_HOME --> A_S3["Company"]
A_S1 --> A_P1["Feature A"]
A_S1 --> A_P2["Feature B"]
A_S2 --> A_P3["Blog"]
A_S2 --> A_P4["Guides"]
A_S3 --> A_P5["About"]
A_S3 --> A_P6["Contact"]
end
```
---
## Color-Coding Conventions
Use styles to highlight page status, priority, or type.
```mermaid
graph TD
HOME["Homepage"] --> FEAT["Features"]
HOME --> PRICE["Pricing"]
HOME --> BLOG["Blog"]
HOME --> NEW["New Section"]
HOME --> REMOVE["Deprecated Page"]
FEAT --> F1["Existing Feature"]
FEAT --> F2["New Feature"]
style HOME fill:#4CAF50,color:#fff
style PRICE fill:#4CAF50,color:#fff
style FEAT fill:#4CAF50,color:#fff
style BLOG fill:#4CAF50,color:#fff
style F1 fill:#4CAF50,color:#fff
style NEW fill:#2196F3,color:#fff
style F2 fill:#2196F3,color:#fff
style REMOVE fill:#f44336,color:#fff
```
Color key:
- **Green** (`#4CAF50`): Existing pages (no changes)
- **Blue** (`#2196F3`): New pages to create
- **Red** (`#f44336`): Pages to remove or redirect
- **Yellow** (`#FFC107`): Pages to restructure or move
- **Purple** (`#9C27B0`): High-priority / CTA pages
FILE:references/navigation-patterns.md
# Navigation Patterns
Detailed navigation patterns for different site types and contexts.
---
## Header Navigation
### Simple Header (4-6 items)
Best for: small businesses, simple SaaS, portfolios.
```
[Logo] Features Pricing Blog About [CTA Button]
```
Rules:
- Logo always links to homepage
- CTA button is rightmost, visually distinct (filled button, contrasting color)
- Items ordered by priority (most visited first)
- Active page gets visual indicator (underline, bold, color)
### Mega Menu Header
Best for: SaaS with many features, e-commerce with categories, large content sites.
```
[Logo] Product ▾ Solutions ▾ Resources ▾ Pricing Docs [CTA]
```
When "Product" is hovered/clicked:
```
┌─────────────────────────────────────────────────┐
│ Features Platform Integrations │
│ ───────── ───────── ──────────── │
│ Analytics Security Slack │
│ Automation API HubSpot │
│ Reporting Compliance Salesforce │
│ Dashboards Zapier │
│ │
│ [See all features →] │
└─────────────────────────────────────────────────┘
```
Mega menu rules:
- 2-4 columns max
- Group items logically (by feature area, use case, or audience)
- Include a "See all" link at the bottom
- Don't nest dropdowns inside mega menus
- Show descriptions for items when labels alone aren't clear
### Split Navigation
Best for: apps with both marketing and product nav.
```
[Logo] Features Pricing Blog [Login] [Sign Up]
├── Marketing nav (left) ──────┘ └── Auth nav (right) ──┤
```
Right side handles authentication actions. Left side handles page navigation.
---
## Footer Navigation
### Column-Based Footer (Standard)
Best for: most sites. Organize links into 3-5 themed columns.
```
┌──────────────────────────────────────────────────────────┐
│ │
│ Product Resources Company Legal │
│ ───────── ────────── ───────── ───── │
│ Features Blog About Privacy │
│ Pricing Guides Careers Terms │
│ Integrations Templates Contact GDPR │
│ Changelog Case Studies Press │
│ Security Webinars Partners │
│ │
│ [Logo] © 2026 Company Name │
│ Social: [Twitter] [LinkedIn] [GitHub] │
│ │
└──────────────────────────────────────────────────────────┘
```
### Minimal Footer
Best for: simple sites, landing pages.
```
┌──────────────────────────────────────────────────────────┐
│ [Logo] │
│ © 2026 Company · Privacy · Terms · Contact │
└──────────────────────────────────────────────────────────┘
```
### Expanded Footer
Best for: sites using footer for SEO (comparison pages, location pages, resource links).
```
┌──────────────────────────────────────────────────────────┐
│ Product Resources Compare Use Cases │
│ Features Blog vs Competitor A For Startups │
│ Pricing Guides vs Competitor B For Enterprise│
│ API Templates vs Competitor C For Agencies │
│ │
│ Integrations Popular Posts │
│ Slack Zapier How to Do X │
│ HubSpot Salesforce Guide to Y │
│ Template: Z │
│ │
│ [Logo] © 2026 · Privacy · Terms · Security │
└──────────────────────────────────────────────────────────┘
```
---
## Sidebar Navigation
### Documentation Sidebar
Persistent left sidebar with collapsible sections.
```
Getting Started
├── Installation
├── Quick Start
└── Configuration
Guides
├── Authentication
├── Data Models
└── Deployment
API Reference
├── REST API
│ ├── Users
│ ├── Projects
│ └── Webhooks
└── GraphQL
Examples
├── Next.js
├── Rails
└── Python
Changelog
```
Rules:
- Current page highlighted
- Sections collapsible (expanded by default for active section)
- Search at top of sidebar
- "Previous / Next" page navigation at bottom of content area
- Sticky on scroll (doesn't scroll away)
### Blog Category Sidebar
```
Categories
├── SEO (24)
├── CRO (18)
├── Content (15)
├── Paid Ads (12)
└── Analytics (9)
Popular Posts
├── How to Improve SEO
├── Landing Page Guide
└── Analytics Setup
Newsletter
└── [Email signup form]
```
---
## Breadcrumbs
### Standard Format
```
Home > Features > Analytics
Home > Blog > SEO Category > How to Do Keyword Research
Home > Docs > API Reference > Authentication
```
Rules:
- Separator: `>` or `/` (be consistent)
- Every segment is a link except the current page
- Current page is plain text (not linked)
- Don't include the current page if the title is already visible as an H1
### With Schema Markup
```html
<nav aria-label="Breadcrumb">
<ol itemscope itemtype="https://schema.org/BreadcrumbList">
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<a itemprop="item" href="/"><span itemprop="name">Home</span></a>
<meta itemprop="position" content="1" />
</li>
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<a itemprop="item" href="/features"><span itemprop="name">Features</span></a>
<meta itemprop="position" content="2" />
</li>
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<span itemprop="name">Analytics</span>
<meta itemprop="position" content="3" />
</li>
</ol>
</nav>
```
Or use JSON-LD (recommended):
```json
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://example.com/" },
{ "@type": "ListItem", "position": 2, "name": "Features", "item": "https://example.com/features" },
{ "@type": "ListItem", "position": 3, "name": "Analytics" }
]
}
```
---
## Mobile Navigation
### Hamburger Menu
Standard for mobile. All nav items collapse into a menu icon.
Rules:
- Hamburger icon (three lines) top-right or top-left
- Full-screen or slide-out panel
- CTA button visible without opening the menu (sticky header)
- Search accessible from mobile menu
- Accordion pattern for nested items
### Bottom Tab Bar
Best for: web apps, PWAs, mobile-first products.
```
┌──────────────────────────────────────┐
│ │
│ [Page Content] │
│ │
├──────────────────────────────────────┤
│ Home Search Create Profile │
│ 🏠 🔍 ➕ 👤 │
└──────────────────────────────────────┘
```
Rules:
- 3-5 items max
- Icons + labels (not just icons)
- Active state clearly indicated
- Most important action in the center
---
## Anti-Patterns
### Things to Avoid
- **Too many header items** (8+): causes decision paralysis, nav becomes unreadable on smaller screens
- **Dropdown inception**: dropdowns inside dropdowns inside dropdowns
- **Mystery icons**: icons without labels — users don't know what they mean
- **Hidden primary nav**: burying important pages in hamburger menus on desktop
- **Inconsistent nav between pages**: nav should be identical across the site (except app vs marketing)
- **No mobile consideration**: desktop nav that doesn't translate to mobile
- **Footer as sitemap dump**: 50+ links in the footer with no organization
- **Breadcrumbs that don't match URLs**: breadcrumb says "Products > Widget" but URL is `/shop/widget-pro`
### Common Fixes
| Problem | Fix |
|---------|-----|
| Too many nav items | Group into dropdowns or mega menus |
| Users can't find pages | Add search, improve labeling |
| High bounce from nav | Simplify choices, use clearer labels |
| SEO pages not linked | Add to footer or resource sections |
| Mobile nav is broken | Test on real devices, use hamburger pattern |
---
## Navigation for SEO
Internal links in navigation pass PageRank. Use this strategically:
- **Header nav links are strongest** — put your most important pages here
- **Footer links pass less value** but still matter — good for comparison pages, location pages
- **Sidebar links** help with section-level authority — good for blog categories, doc sections
- **Breadcrumbs** provide structural signals to search engines — implement with schema markup
- **Don't use JavaScript-only nav** — search engines need crawlable HTML links
- **Use descriptive anchor text** — "Analytics Features" not just "Features"
FILE:references/site-type-templates.md
# Site Type Templates
Full page hierarchy templates with ASCII trees, URL maps, and navigation recommendations for common site types.
---
## SaaS Marketing Site
### Page Hierarchy
```
Homepage (/)
├── Features (/features)
│ ├── Feature A (/features/feature-a)
│ ├── Feature B (/features/feature-b)
│ └── Feature C (/features/feature-c)
├── Pricing (/pricing)
├── Customers (/customers)
│ ├── Case Study 1 (/customers/company-name)
│ └── Case Study 2 (/customers/company-name-2)
├── Resources (/resources)
│ ├── Blog (/blog)
│ │ └── [Posts] (/blog/post-slug)
│ ├── Templates (/resources/templates)
│ │ └── [Template] (/resources/templates/template-slug)
│ └── Guides (/resources/guides)
│ └── [Guide] (/resources/guides/guide-slug)
├── Integrations (/integrations)
│ └── [Integration] (/integrations/integration-name)
├── Docs (/docs)
│ ├── Getting Started (/docs/getting-started)
│ ├── Guides (/docs/guides)
│ └── API Reference (/docs/api)
├── About (/about)
│ ├── Careers (/about/careers)
│ └── Contact (/contact)
├── Compare (/compare)
│ └── [Competitor] (/compare/competitor-name)
├── Privacy (/privacy)
└── Terms (/terms)
```
### URL Map
| Page | URL | Nav Location | Priority |
|------|-----|-------------|----------|
| Homepage | `/` | Header (logo) | Critical |
| Features | `/features` | Header | High |
| Feature pages | `/features/{slug}` | Header dropdown | Medium |
| Pricing | `/pricing` | Header | Critical |
| Customers | `/customers` | Header | Medium |
| Case studies | `/customers/{slug}` | Customers dropdown | Medium |
| Blog | `/blog` | Header (Resources) | High |
| Blog posts | `/blog/{slug}` | — | Medium |
| Integrations | `/integrations` | Header | Medium |
| Docs | `/docs` | Header | Medium |
| Compare | `/compare/{slug}` | Footer | High (SEO) |
| About | `/about` | Footer | Low |
| Pricing CTA | `/pricing` | Header (CTA button) | Critical |
### Navigation
**Header (6 items + CTA)**: Features | Pricing | Customers | Resources | Integrations | Docs | [Get Started]
**Footer columns**:
- Product: Features, Pricing, Integrations, Changelog, Security
- Resources: Blog, Templates, Guides, Case Studies
- Company: About, Careers, Contact, Press
- Legal: Privacy, Terms, Security
---
## Content / Blog Site
### Page Hierarchy
```
Homepage (/)
├── Blog (/blog)
│ ├── [Category: Topic A] (/blog/category/topic-a)
│ ├── [Category: Topic B] (/blog/category/topic-b)
│ ├── [Category: Topic C] (/blog/category/topic-c)
│ └── [Posts] (/blog/post-slug)
├── Newsletter (/newsletter)
├── Resources (/resources)
│ ├── Guides (/resources/guides)
│ │ └── [Guide] (/resources/guides/guide-slug)
│ └── Tools (/resources/tools)
│ └── [Tool] (/resources/tools/tool-slug)
├── About (/about)
├── Contact (/contact)
├── Privacy (/privacy)
└── Terms (/terms)
```
### URL Map
| Page | URL | Nav Location | Priority |
|------|-----|-------------|----------|
| Homepage | `/` | Header (logo) | Critical |
| Blog index | `/blog` | Header | High |
| Categories | `/blog/category/{slug}` | Header dropdown | Medium |
| Posts | `/blog/{slug}` | — | Medium |
| Newsletter | `/newsletter` | Header (CTA) | High |
| Guides | `/resources/guides` | Header | Medium |
| About | `/about` | Header | Low |
### Navigation
**Header (4 items + CTA)**: Blog | Resources | About | Contact | [Subscribe]
**Sidebar** (on blog): Categories, Popular Posts, Newsletter signup
---
## E-Commerce
### Page Hierarchy
```
Homepage (/)
├── Shop (/shop)
│ ├── Category A (/shop/category-a)
│ │ ├── Subcategory (/shop/category-a/subcategory)
│ │ │ └── [Product] (/shop/category-a/subcategory/product-slug)
│ │ └── [Product] (/shop/category-a/product-slug)
│ ├── Category B (/shop/category-b)
│ │ └── [Product] (/shop/category-b/product-slug)
│ └── Category C (/shop/category-c)
│ └── [Product] (/shop/category-c/product-slug)
├── Collections (/collections)
│ └── [Collection] (/collections/collection-slug)
├── Sale (/sale)
├── Blog (/blog)
│ └── [Posts] (/blog/post-slug)
├── About (/about)
│ └── Our Story (/about/our-story)
├── Help (/help)
│ ├── FAQ (/help/faq)
│ ├── Shipping (/help/shipping)
│ ├── Returns (/help/returns)
│ └── Contact (/contact)
├── Cart (/cart)
├── Account (/account)
├── Privacy (/privacy)
└── Terms (/terms)
```
### URL Map
| Page | URL | Nav Location | Priority |
|------|-----|-------------|----------|
| Homepage | `/` | Header (logo) | Critical |
| Shop | `/shop` | Header | Critical |
| Categories | `/shop/{category}` | Header mega menu | High |
| Products | `/shop/{category}/{product}` | — | High |
| Collections | `/collections/{slug}` | Header | Medium |
| Sale | `/sale` | Header (highlighted) | High |
| Cart | `/cart` | Header (icon) | Critical |
| Account | `/account` | Header (icon) | Medium |
### Navigation
**Header (5 items + cart/account)**: Shop (mega menu) | Collections | Sale | Blog | Help | [Cart icon] [Account icon]
**Mega menu under Shop**: Category columns with featured products/images
---
## Documentation Site
### Page Hierarchy
```
Docs Home (/docs)
├── Getting Started (/docs/getting-started)
│ ├── Installation (/docs/getting-started/installation)
│ ├── Quick Start (/docs/getting-started/quick-start)
│ └── Configuration (/docs/getting-started/configuration)
├── Guides (/docs/guides)
│ ├── Guide A (/docs/guides/guide-a)
│ ├── Guide B (/docs/guides/guide-b)
│ └── Guide C (/docs/guides/guide-c)
├── API Reference (/docs/api)
│ ├── Authentication (/docs/api/authentication)
│ ├── Endpoints (/docs/api/endpoints)
│ └── Webhooks (/docs/api/webhooks)
├── Examples (/docs/examples)
│ └── [Example] (/docs/examples/example-slug)
├── Changelog (/docs/changelog)
└── FAQ (/docs/faq)
```
### URL Map
| Page | URL | Nav Location | Priority |
|------|-----|-------------|----------|
| Docs home | `/docs` | Header | High |
| Getting Started | `/docs/getting-started` | Sidebar (top) | Critical |
| Guides | `/docs/guides` | Sidebar | High |
| API Reference | `/docs/api` | Sidebar | High |
| Changelog | `/docs/changelog` | Sidebar (bottom) | Low |
### Navigation
**Header**: Docs | API | Blog | Community | GitHub | [Dashboard]
**Sidebar** (persistent, left): Getting Started, Guides, API Reference, Examples, Changelog — with expandable subsections
**On-page**: Previous/Next navigation at bottom of each doc page
---
## Hybrid SaaS + Content
### Page Hierarchy
```
Homepage (/)
├── Product (/product)
│ ├── Feature A (/product/feature-a)
│ ├── Feature B (/product/feature-b)
│ └── Feature C (/product/feature-c)
├── Solutions (/solutions)
│ ├── By Use Case (/solutions/use-case-slug)
│ └── By Industry (/solutions/industry-slug)
├── Pricing (/pricing)
├── Blog (/blog)
│ ├── [Category] (/blog/category/slug)
│ └── [Posts] (/blog/post-slug)
├── Resources (/resources)
│ ├── Guides (/resources/guides)
│ ├── Templates (/resources/templates)
│ ├── Webinars (/resources/webinars)
│ └── Case Studies (/resources/case-studies)
├── Docs (/docs)
│ ├── Getting Started (/docs/getting-started)
│ └── API (/docs/api)
├── Integrations (/integrations)
│ └── [Integration] (/integrations/slug)
├── Compare (/compare)
│ └── [Competitor] (/compare/competitor-slug)
├── About (/about)
│ ├── Careers (/about/careers)
│ └── Contact (/contact)
├── Privacy (/privacy)
└── Terms (/terms)
```
### Navigation
**Header (7 items + CTA)**: Product | Solutions | Pricing | Resources | Blog | Docs | Integrations | [Start Free Trial]
Use mega menus for Product (features list), Solutions (use cases + industries), and Resources (blog, guides, templates, webinars, case studies).
---
## Small Business / Local
### Page Hierarchy
```
Homepage (/)
├── Services (/services)
│ ├── Service A (/services/service-a)
│ ├── Service B (/services/service-b)
│ └── Service C (/services/service-c)
├── About (/about)
├── Testimonials (/testimonials)
├── Blog (/blog)
│ └── [Posts] (/blog/post-slug)
├── Contact (/contact)
├── Privacy (/privacy)
└── Terms (/terms)
```
### URL Map
| Page | URL | Nav Location | Priority |
|------|-----|-------------|----------|
| Homepage | `/` | Header (logo) | Critical |
| Services | `/services` | Header | High |
| Service pages | `/services/{slug}` | Header dropdown | High |
| About | `/about` | Header | Medium |
| Testimonials | `/testimonials` | Header | Medium |
| Blog | `/blog` | Header | Medium |
| Contact | `/contact` | Header (CTA) | High |
### Navigation
**Header (5 items + CTA)**: Services | About | Testimonials | Blog | [Contact Us]
Keep it simple. Small business sites should be flat (1-2 levels max). Every page should be reachable from the header.
Bộ 42 skill marketing cho các coding agent, chia 7 nhóm: nội dung, SEO, CRO, kênh, tăng trưởng, thông tin thị trường, bán hàng, kèm 27 công cụ Python.
--- name: "marketing-skills" description: "42 marketing agent skills and plugins for Claude Code, Codex, Gemini CLI, Cursor, OpenClaw, and 6 more coding agents. 7 pods: content, SEO, CRO, channels, growth, intelligence, sales. Foundation context + orchestration router. 27 Python tools (stdlib-only)." version: 2.9.0 author: Alireza Rezvani license: MIT tags: - marketing - seo - content - copywriting - cro - analytics - ai-seo agents: - claude-code - codex-cli - openclaw --- # Marketing Skills Division 42 production-ready marketing skills organized into 7 specialist pods with a context foundation and orchestration layer. ## Quick Start ### Claude Code ``` /read marketing-skill/marketing-ops/SKILL.md ``` The router will direct you to the right specialist skill. ### Codex CLI ```bash codex --full-auto "Read marketing-skill/marketing-ops/SKILL.md, then help me write a blog post about [topic]" ``` ### OpenClaw Skills are auto-discovered from the repository. Ask your agent for marketing help — it routes via `marketing-ops`. ## Architecture ``` marketing-skill/ ├── marketing-context/ ← Foundation: brand voice, audience, goals ├── marketing-ops/ ← Router: dispatches to the right skill │ ├── Content Pod (8) ← Strategy → Production → Editing → Social ├── SEO Pod (5) ← Traditional + AI SEO + Schema + Architecture ├── CRO Pod (6) ← Pages, Forms, Signup, Onboarding, Popups, Paywall ├── Channels Pod (5) ← Email, Ads, Cold Email, Ad Creative, Social Mgmt ├── Growth Pod (4) ← A/B Testing, Referrals, Free Tools, Churn ├── Intelligence Pod (4) ← Competitors, Psychology, Analytics, Campaigns └── Sales & GTM Pod (2) ← Pricing, Launch Strategy ``` ## First-Time Setup Run `marketing-context` to create your `marketing-context.md` file. Every other skill reads this for brand voice, audience personas, and competitive landscape. Do this once — it makes everything better. ## Pod Overview | Pod | Skills | Python Tools | Key Capabilities | |-----|--------|-------------|-----------------| | **Foundation** | 2 | 2 | Brand context capture, skill routing | | **Content** | 8 | 5 | Strategy → production → editing → humanization | | **SEO** | 5 | 2 | Technical SEO, AI SEO (AEO/GEO), schema, architecture | | **CRO** | 6 | 0 | Page, form, signup, onboarding, popup, paywall optimization | | **Channels** | 5 | 2 | Email sequences, paid ads, cold email, ad creative | | **Growth** | 4 | 2 | A/B testing, referral programs, free tools, churn prevention | | **Intelligence** | 4 | 4 | Competitor analysis, marketing psychology, analytics, campaigns | | **Sales & GTM** | 2 | 1 | Pricing strategy, launch planning | | **Standalone** | 4 | 9 | ASO, brand guidelines, PMM strategy, prompt engineering | ## Python Tools (27 scripts) All scripts are stdlib-only (zero pip installs), CLI-first with JSON output, and include embedded sample data for demo mode. ```bash # Content scoring python3 marketing-skill/content-production/scripts/content_scorer.py article.md # AI writing detection python3 marketing-skill/content-humanizer/scripts/humanizer_scorer.py draft.md # Brand voice analysis python3 marketing-skill/content-production/scripts/brand_voice_analyzer.py copy.txt # Ad copy validation python3 marketing-skill/ad-creative/scripts/ad_copy_validator.py ads.json # Pricing scenario modeling python3 marketing-skill/pricing-strategy/scripts/pricing_modeler.py # Tracking plan generation python3 marketing-skill/analytics-tracking/scripts/tracking_plan_generator.py ``` ## Unique Features - **AI SEO (AEO/GEO/LLMO)** — Optimize for AI citation, not just ranking - **Content Humanizer** — Detect and fix AI writing patterns with scoring - **Context Foundation** — One brand context file feeds all 42 skills - **Orchestration Router** — Smart routing by keyword + complexity scoring - **Zero Dependencies** — All Python tools use stdlib only
Thêm, sửa và tối ưu schema markup, dữ liệu cấu trúc JSON-LD để xuất hiện rich results trên Google.
---
name: schema
description: When the user wants to add, fix, or optimize schema markup and structured data on their site. Also use when the user mentions "schema markup," "structured data," "JSON-LD," "rich snippets," "schema.org," "FAQ schema," "product schema," "review schema," "breadcrumb schema," "Google rich results," "knowledge panel," "star ratings in search," or "add structured data." Use this whenever someone wants their pages to show enhanced results in Google. For broader SEO issues, see seo-audit. For AI search optimization, see ai-seo.
metadata:
version: 2.0.0
---
# Schema Markup
You are an expert in structured data and schema markup. Your goal is to implement schema.org markup that helps search engines understand content and enables rich results in search.
## Initial Assessment
**Check for product marketing context first:**
If `.agents/product-marketing.md` exists (or `.claude/product-marketing.md`, or the legacy `product-marketing-context.md` filename, in older setups), read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Before implementing schema, understand:
1. **Page Type** - What kind of page? What's the primary content? What rich results are possible?
2. **Current State** - Any existing schema? Errors in implementation? Which rich results already appearing?
3. **Goals** - Which rich results are you targeting? What's the business value?
---
## Core Principles
### 1. Accuracy First
- Schema must accurately represent page content
- Don't markup content that doesn't exist
- Keep updated when content changes
### 2. Use JSON-LD
- Google recommends JSON-LD format
- Easier to implement and maintain
- Place in `<head>` or end of `<body>`
### 3. Follow Google's Guidelines
- Only use markup Google supports
- Avoid spam tactics
- Review eligibility requirements
### 4. Validate Everything
- Test before deploying
- Monitor Search Console
- Fix errors promptly
---
## Common Schema Types
| Type | Use For | Required Properties |
|------|---------|-------------------|
| Organization | Company homepage/about | name, url |
| WebSite | Homepage (search box) | name, url |
| Article | Blog posts, news | headline, image, datePublished, author |
| Product | Product pages | name, image, offers |
| SoftwareApplication | SaaS/app pages | name, offers |
| FAQPage | FAQ content | mainEntity (Q&A array) |
| HowTo | Tutorials | name, step |
| BreadcrumbList | Any page with breadcrumbs | itemListElement |
| LocalBusiness | Local business pages | name, address |
| Event | Events, webinars | name, startDate, location |
**For complete JSON-LD examples**: See [references/schema-examples.md](references/schema-examples.md)
---
## Quick Reference
### Organization (Company Page)
Required: name, url
Recommended: logo, sameAs (social profiles), contactPoint
### Article/BlogPosting
Required: headline, image, datePublished, author
Recommended: dateModified, publisher, description
### Product
Required: name, image, offers (price + availability)
Recommended: sku, brand, aggregateRating, review
### FAQPage
Required: mainEntity (array of Question/Answer pairs)
### BreadcrumbList
Required: itemListElement (array with position, name, item)
---
## Multiple Schema Types
You can combine multiple schema types on one page using `@graph`:
```json
{
"@context": "https://schema.org",
"@graph": [
{ "@type": "Organization", ... },
{ "@type": "WebSite", ... },
{ "@type": "BreadcrumbList", ... }
]
}
```
---
## Validation and Testing
### Tools
- **Google Rich Results Test**: https://search.google.com/test/rich-results
- **Schema.org Validator**: https://validator.schema.org/
- **Search Console**: Enhancements reports
### Common Errors
**Missing required properties** - Check Google's documentation for required fields
**Invalid values** - Dates must be ISO 8601, URLs fully qualified, enumerations exact
**Mismatch with page content** - Schema doesn't match visible content
---
## Implementation
### Static Sites
- Add JSON-LD directly in HTML template
- Use includes/partials for reusable schema
### Dynamic Sites (React, Next.js)
- Component that renders schema
- Server-side rendered for SEO
- Serialize data to JSON-LD
### CMS / WordPress
- Plugins (Yoast, Rank Math, Schema Pro)
- Theme modifications
- Custom fields to structured data
---
## Output Format
### Schema Implementation
```json
// Full JSON-LD code block
{
"@context": "https://schema.org",
"@type": "...",
// Complete markup
}
```
### Testing Checklist
- [ ] Validates in Rich Results Test
- [ ] No errors or warnings
- [ ] Matches page content
- [ ] All required properties included
---
## Task-Specific Questions
1. What type of page is this?
2. What rich results are you hoping to achieve?
3. What data is available to populate the schema?
4. Is there existing schema on the page?
5. What's your tech stack?
---
## Related Skills
- **seo-audit**: For overall SEO including schema review
- **ai-seo**: For AI search optimization (schema helps AI understand content)
- **programmatic-seo**: For templated schema at scale
- **site-architecture**: For breadcrumb structure and navigation schema planning
FILE:evals/evals.json
{
"skill_name": "schema",
"evals": [
{
"id": 1,
"prompt": "Add schema markup to our SaaS product's homepage. We're a project management tool called TaskFlow. We need Organization schema and any other relevant types.",
"expected_output": "Should check for product-marketing.md first. Should implement Organization schema in JSON-LD format with all required and recommended properties (name, url, logo, description, sameAs for social profiles). Should recommend additional schema types for a SaaS homepage: WebSite (with SearchAction if applicable), SoftwareApplication or Product. Should use @graph for multiple schema types on one page. Should provide the complete JSON-LD code ready to implement. Should recommend validation with Google's Rich Results Test and Schema.org validator.",
"assertions": [
"Checks for product-marketing.md",
"Implements Organization schema in JSON-LD",
"Includes required and recommended properties",
"Recommends additional relevant schema types",
"Uses @graph for multiple types",
"Provides complete JSON-LD code",
"Recommends validation tools"
],
"files": []
},
{
"id": 2,
"prompt": "We have a FAQ page with 20 questions about our product. How do I add FAQ schema to get the rich results in Google?",
"expected_output": "Should implement FAQPage schema in JSON-LD format. Should show the correct structure: FAQPage as mainEntity containing Question items, each with acceptedAnswer. Should provide a complete code example with 2-3 sample questions. Should explain that FAQ schema can enable rich results showing questions/answers directly in search. Should note Google's guidelines for FAQ schema (factual answers, not promotional). Should recommend validation approach.",
"assertions": [
"Implements FAQPage schema in JSON-LD",
"Shows correct nested structure (FAQPage > Question > Answer)",
"Provides complete code example",
"Explains rich result benefits",
"Notes Google's FAQ schema guidelines",
"Recommends validation"
],
"files": []
},
{
"id": 3,
"prompt": "add schema to our blog posts. we publish articles about marketing tips.",
"expected_output": "Should trigger on casual phrasing. Should implement Article (or BlogPosting) schema in JSON-LD. Should include required properties: headline, author (as Person with name and url), datePublished, dateModified, image, publisher (as Organization). Should recommend BreadcrumbList schema alongside the article schema. Should provide template code that can be reused across blog posts. Should address how to populate dynamic fields (date, author, headline) from the CMS.",
"assertions": [
"Triggers on casual phrasing",
"Implements Article or BlogPosting schema",
"Includes author, datePublished, image, publisher",
"Recommends BreadcrumbList alongside",
"Provides reusable template code",
"Addresses CMS integration for dynamic fields"
],
"files": []
},
{
"id": 4,
"prompt": "We're an e-commerce site selling physical products. What schema markup do we need for our product pages?",
"expected_output": "Should implement Product schema with full properties: name, description, image, brand, sku, offers (with price, priceCurrency, availability, url). Should recommend AggregateRating if they have reviews, and Review schema for individual reviews. Should include BreadcrumbList for navigation. Should address common e-commerce schema types: Product, Offer, AggregateRating, Review. Should provide complete JSON-LD code. Should note that Product schema can enable rich results (price, availability, ratings in search).",
"assertions": [
"Implements Product schema with full properties",
"Includes Offer with price, availability",
"Recommends AggregateRating and Review schema",
"Includes BreadcrumbList",
"Provides complete JSON-LD code",
"Notes rich result benefits for products"
],
"files": []
},
{
"id": 5,
"prompt": "We added schema markup to our site but it's not showing rich results in Google. Can you help debug?",
"expected_output": "Should provide a systematic debugging approach: first validate with Google Rich Results Test and Schema.org validator (syntax errors), then check for common issues (incorrect nesting, missing required properties, JSON-LD placement errors). Should explain that valid schema doesn't guarantee rich results — Google chooses when to show them. Should recommend checking Search Console for structured data reports and errors. Should address common debugging scenarios: schema not detected, warnings vs errors, eligible vs displayed.",
"assertions": [
"Recommends validation tools for debugging",
"Checks for common schema errors",
"Explains valid schema doesn't guarantee rich results",
"Recommends Search Console structured data reports",
"Addresses warnings vs errors distinction",
"Provides systematic debugging approach"
],
"files": []
},
{
"id": 6,
"prompt": "Our organic search traffic dropped after a site redesign. Can you do a technical SEO audit?",
"expected_output": "Should recognize this is a technical SEO audit request, not a schema markup task. Should defer to or cross-reference the seo-audit skill, which handles comprehensive technical SEO audits. Schema markup is one component of SEO but doesn't address the broader technical issues (redirects, crawlability, indexation) that likely caused the traffic drop.",
"assertions": [
"Recognizes this as a technical SEO audit request",
"References or defers to seo-audit skill",
"Does not attempt full SEO audit using schema markup patterns"
],
"files": []
}
]
}
FILE:references/schema-examples.md
# Schema Markup Examples
Complete JSON-LD examples for common schema types.
## Contents
- Organization
- WebSite (with SearchAction)
- Article / BlogPosting
- Product
- SoftwareApplication
- FAQPage
- HowTo
- BreadcrumbList
- LocalBusiness
- Event
- Multiple Schema Types
- Implementation Example (Next.js)
## Organization
For company/brand homepage or about page.
```json
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Example Company",
"url": "https://example.com",
"logo": "https://example.com/logo.png",
"sameAs": [
"https://twitter.com/example",
"https://linkedin.com/company/example",
"https://facebook.com/example"
],
"contactPoint": {
"@type": "ContactPoint",
"telephone": "+1-555-555-5555",
"contactType": "customer service"
}
}
```
---
## WebSite (with SearchAction)
For homepage, enables sitelinks search box.
```json
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Example",
"url": "https://example.com",
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://example.com/search?q={search_term_string}"
},
"query-input": "required name=search_term_string"
}
}
```
---
## Article / BlogPosting
For blog posts and news articles.
```json
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "How to Implement Schema Markup",
"image": "https://example.com/image.jpg",
"datePublished": "2024-01-15T08:00:00+00:00",
"dateModified": "2024-01-20T10:00:00+00:00",
"author": {
"@type": "Person",
"name": "Jane Doe",
"url": "https://example.com/authors/jane"
},
"publisher": {
"@type": "Organization",
"name": "Example Company",
"logo": {
"@type": "ImageObject",
"url": "https://example.com/logo.png"
}
},
"description": "A complete guide to implementing schema markup...",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://example.com/schema-guide"
}
}
```
---
## Product
For product pages (e-commerce or SaaS).
```json
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Premium Widget",
"image": "https://example.com/widget.jpg",
"description": "Our best-selling widget for professionals",
"sku": "WIDGET-001",
"brand": {
"@type": "Brand",
"name": "Example Co"
},
"offers": {
"@type": "Offer",
"url": "https://example.com/products/widget",
"priceCurrency": "USD",
"price": "99.99",
"availability": "https://schema.org/InStock",
"priceValidUntil": "2024-12-31"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.8",
"reviewCount": "127"
}
}
```
---
## SoftwareApplication
For SaaS product pages and app landing pages.
```json
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Example App",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Web, iOS, Android",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.6",
"ratingCount": "1250"
}
}
```
---
## FAQPage
For pages with frequently asked questions.
```json
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is schema markup?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Schema markup is a structured data vocabulary that helps search engines understand your content..."
}
},
{
"@type": "Question",
"name": "How do I implement schema?",
"acceptedAnswer": {
"@type": "Answer",
"text": "The recommended approach is to use JSON-LD format, placing the script in your page's head..."
}
}
]
}
```
---
## HowTo
For instructional content and tutorials.
```json
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "How to Add Schema Markup to Your Website",
"description": "A step-by-step guide to implementing JSON-LD schema",
"totalTime": "PT15M",
"step": [
{
"@type": "HowToStep",
"name": "Choose your schema type",
"text": "Identify the appropriate schema type for your page content...",
"url": "https://example.com/guide#step1"
},
{
"@type": "HowToStep",
"name": "Write the JSON-LD",
"text": "Create the JSON-LD markup following schema.org specifications...",
"url": "https://example.com/guide#step2"
},
{
"@type": "HowToStep",
"name": "Add to your page",
"text": "Insert the script tag in your page's head section...",
"url": "https://example.com/guide#step3"
}
]
}
```
---
## BreadcrumbList
For any page with breadcrumb navigation.
```json
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://example.com"
},
{
"@type": "ListItem",
"position": 2,
"name": "Blog",
"item": "https://example.com/blog"
},
{
"@type": "ListItem",
"position": 3,
"name": "SEO Guide",
"item": "https://example.com/blog/seo-guide"
}
]
}
```
---
## LocalBusiness
For local business location pages.
```json
{
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": "Example Coffee Shop",
"image": "https://example.com/shop.jpg",
"address": {
"@type": "PostalAddress",
"streetAddress": "123 Main Street",
"addressLocality": "San Francisco",
"addressRegion": "CA",
"postalCode": "94102",
"addressCountry": "US"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": "37.7749",
"longitude": "-122.4194"
},
"telephone": "+1-555-555-5555",
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "08:00",
"closes": "18:00"
}
],
"priceRange": "$$"
}
```
---
## Event
For event pages, webinars, conferences.
```json
{
"@context": "https://schema.org",
"@type": "Event",
"name": "Annual Marketing Conference",
"startDate": "2024-06-15T09:00:00-07:00",
"endDate": "2024-06-15T17:00:00-07:00",
"eventAttendanceMode": "https://schema.org/OnlineEventAttendanceMode",
"eventStatus": "https://schema.org/EventScheduled",
"location": {
"@type": "VirtualLocation",
"url": "https://example.com/conference"
},
"image": "https://example.com/conference.jpg",
"description": "Join us for our annual marketing conference...",
"offers": {
"@type": "Offer",
"url": "https://example.com/conference/tickets",
"price": "199",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock",
"validFrom": "2024-01-01"
},
"performer": {
"@type": "Organization",
"name": "Example Company"
},
"organizer": {
"@type": "Organization",
"name": "Example Company",
"url": "https://example.com"
}
}
```
---
## Multiple Schema Types
Combine multiple schema types using @graph.
```json
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://example.com/#organization",
"name": "Example Company",
"url": "https://example.com"
},
{
"@type": "WebSite",
"@id": "https://example.com/#website",
"url": "https://example.com",
"name": "Example",
"publisher": {
"@id": "https://example.com/#organization"
}
},
{
"@type": "BreadcrumbList",
"itemListElement": [...]
}
]
}
```
---
## Implementation Example (Next.js)
```jsx
export default function ProductPage({ product }) {
const schema = {
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
// ... other properties
};
return (
<>
<Head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
</Head>
{/* Page content */}
</>
);
}
```
Kiểm chứng cơ hội sản phẩm, lập bản đồ giả định, lên kế hoạch discovery sprint và thử độ khớp vấn đề-giải pháp trước khi đầu tư phát triển.
---
name: product-discovery
description: Use when validating product opportunities, mapping assumptions, planning discovery sprints, or testing problem-solution fit before committing delivery resources.
---
# Product Discovery
Run structured discovery to identify high-value opportunities and de-risk product bets.
## When To Use
Use this skill for:
- Opportunity Solution Tree facilitation
- Assumption mapping and test planning
- Problem validation interviews and evidence synthesis
- Solution validation with prototypes/experiments
- Discovery sprint planning and outputs
## Core Discovery Workflow
1. Define desired outcome
- Set one measurable outcome to improve.
- Establish baseline and target horizon.
2. Build Opportunity Solution Tree (OST)
- Outcome -> opportunities -> solution ideas -> experiments
- Keep opportunities grounded in user evidence, not internal opinions.
3. Map assumptions
- Identify desirability, viability, feasibility, and usability assumptions.
- Score assumptions by risk and certainty.
Use:
```bash
python3 scripts/assumption_mapper.py assumptions.csv
```
4. Validate the problem
- Conduct interviews and behavior analysis.
- Confirm frequency, severity, and willingness to solve.
- Reject weak opportunities early.
5. Validate the solution
- Prototype before building.
- Run concept, usability, and value tests.
- Measure behavior, not only stated preference.
6. Plan discovery sprint
- 1-2 week cycle with explicit hypotheses
- Daily evidence reviews
- End with decision: proceed, pivot, or stop
## Opportunity Solution Tree (Teresa Torres)
Structure:
- Outcome: metric you want to move
- Opportunities: unmet customer needs/pains
- Solutions: candidate interventions
- Experiments: fastest learning actions
Quality checks:
- At least 3 distinct opportunities before converging.
- At least 2 experiments per top opportunity.
- Tie every branch to evidence source.
## Assumption Mapping
Assumption categories:
- Desirability: users want this
- Viability: business value exists
- Feasibility: team can build/operate it
- Usability: users can successfully use it
Prioritization rule:
- High risk + low certainty assumptions are tested first.
## Problem Validation Techniques
- Problem interviews focused on current behavior
- Journey friction mapping
- Support ticket and sales-call synthesis
- Behavioral analytics triangulation
Evidence threshold examples:
- Same pain repeated across multiple target users
- Observable workaround behavior
- Measurable cost of current pain
## Solution Validation Techniques
- Concept tests (value proposition comprehension)
- Prototype usability tests (task success/time-to-complete)
- Fake door or concierge tests (demand signal)
- Limited beta cohorts (retention/activation signals)
## Discovery Sprint Planning
Suggested 10-day structure:
- Day 1-2: Outcome + opportunity framing
- Day 3-4: Assumption mapping + test design
- Day 5-7: Problem and solution tests
- Day 8-9: Evidence synthesis + decision options
- Day 10: Stakeholder decision review
## Tooling
### `scripts/assumption_mapper.py`
CLI utility that:
- reads assumptions from CSV or inline input
- scores risk/certainty priority
- emits prioritized test plan with suggested test types
See `references/discovery-frameworks.md` for framework details.
FILE:references/discovery-frameworks.md
# Discovery Frameworks
## Opportunity Solution Tree (OST)
Purpose: continuously connect product outcomes to validated opportunities and tested solutions.
Core structure:
- Outcome (metric)
- Opportunity nodes (needs/pains)
- Solution ideas
- Experiments
OST practice tips:
- Keep tree live; update after each interview or test.
- Separate opportunity evidence from solution proposals.
- Avoid single-branch trees that force one solution.
## Jobs-to-be-Done (JTBD)
Use JTBD to understand progress users seek.
JTBD template:
"When [situation], I want to [motivation], so I can [expected outcome]."
JTBD interview focus:
- Trigger moments
- Current alternatives and workarounds
- Purchase/adoption anxieties
- Desired progress and success criteria
## Kano Model
Classify features by impact on satisfaction:
- Must-be: expected baseline features
- Performance: more is better
- Delighters: unexpected value multipliers
- Indifferent: low impact
- Reverse: can reduce satisfaction for some users
Use Kano when prioritizing solution concepts after problem validation.
## Design Sprint Methodology
Typical phases:
1. Understand
2. Sketch
3. Decide
4. Prototype
5. Test
Discovery usage:
- Compress learning cycle into one week.
- Best for high-ambiguity opportunities requiring cross-functional alignment.
## Assumption Prioritization Matrix
Map assumptions on two axes:
- Risk if wrong (low -> high)
- Certainty (low -> high)
Priority order:
1. High risk, low certainty (test first)
2. High risk, high certainty (validate quickly)
3. Low risk, low certainty (defer)
4. Low risk, high certainty (document)
## Discovery Evidence Rules
- One source is not enough for major decisions.
- Triangulate qualitative and quantitative signals.
- Predefine decision criteria before test execution.
- Archive evidence with date, segment, and method.
FILE:scripts/assumption_mapper.py
#!/usr/bin/env python3
"""Prioritize product assumptions and suggest validation tests."""
import argparse
import csv
from dataclasses import dataclass
@dataclass
class Assumption:
statement: str
category: str
risk: float
certainty: float
@property
def priority_score(self) -> float:
# High-risk, low-certainty assumptions should be tested first.
return self.risk * (1.0 - self.certainty)
def parse_float(value: str, field: str) -> float:
number = float(value)
if number < 0 or number > 1:
raise ValueError(f"{field} must be in [0, 1]")
return number
def suggest_test(category: str) -> str:
category = category.lower().strip()
if category == "desirability":
return "problem interviews or fake-door test"
if category == "viability":
return "pricing/willingness-to-pay test"
if category == "feasibility":
return "technical spike or architecture prototype"
if category == "usability":
return "moderated usability test"
return "smallest possible experiment with clear success criteria"
def load_from_csv(path: str) -> list[Assumption]:
assumptions: list[Assumption] = []
with open(path, "r", encoding="utf-8", newline="") as handle:
reader = csv.DictReader(handle)
required = {"assumption", "category", "risk", "certainty"}
missing = required - set(reader.fieldnames or [])
if missing:
missing_str = ", ".join(sorted(missing))
raise ValueError(f"Missing required columns: {missing_str}")
for row in reader:
assumptions.append(
Assumption(
statement=(row.get("assumption") or "").strip(),
category=(row.get("category") or "").strip(),
risk=parse_float(row.get("risk") or "0", "risk"),
certainty=parse_float(row.get("certainty") or "0", "certainty"),
)
)
return assumptions
def parse_inline(items: list[str]) -> list[Assumption]:
assumptions: list[Assumption] = []
for item in items:
# format: statement|category|risk|certainty
parts = [part.strip() for part in item.split("|")]
if len(parts) != 4:
raise ValueError("Inline assumption must be: statement|category|risk|certainty")
assumptions.append(
Assumption(
statement=parts[0],
category=parts[1],
risk=parse_float(parts[2], "risk"),
certainty=parse_float(parts[3], "certainty"),
)
)
return assumptions
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Prioritize assumptions and generate test plan.")
parser.add_argument("input", nargs="?", help="CSV file path")
parser.add_argument(
"--assumption",
action="append",
default=[],
help="Inline assumption: statement|category|risk|certainty",
)
parser.add_argument("--top", type=int, default=10, help="Maximum assumptions to print")
return parser
def main() -> int:
parser = build_parser()
args = parser.parse_args()
assumptions: list[Assumption] = []
if args.input:
assumptions.extend(load_from_csv(args.input))
if args.assumption:
assumptions.extend(parse_inline(args.assumption))
if not assumptions:
parser.error("Provide a CSV input file or at least one --assumption value.")
assumptions.sort(key=lambda item: item.priority_score, reverse=True)
print("prioritized_assumption_test_plan")
print("rank,priority_score,category,risk,certainty,test,assumption")
for rank, item in enumerate(assumptions[: args.top], start=1):
test = suggest_test(item.category)
print(
f"{rank},{item.priority_score:.4f},{item.category},{item.risk:.2f},"
f"{item.certainty:.2f},{test},{item.statement}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
Audit mã nguồn trước khi lên production về bảo mật, CSDL, triển khai, chất lượng, AI/LLM, phụ thuộc và chặn deploy khi còn lỗi nghiêm trọng.
---
name: ship-gate
description: >
Pre-production audit that scans a codebase for security, database,
deployment, code quality, AI/LLM, dependency, frontend, and observability
issues. Intercepts deploy commands and blocks until critical items pass.
Stack-agnostic. Use for "run ship gate", "am I ready to ship",
"pre-launch audit", "can I deploy", "push to production", "go live
checklist", "preflight check". Not for CI/CD setup or infra provisioning.
license: MIT
metadata:
author: Rajaraman Arumugam
version: 1.0.0
---
# Ship Gate
Pre-production audit that scans a codebase and reports pass/fail/manual
across 8 categories before anything ships.
## Intercept Behavior
When the user says "push to production", "deploy", "ship it", "go live",
or similar deploy-intent phrases, do NOT proceed with deployment. Instead:
1. Ask: "Have you run the ship gate? Want me to scan now?"
2. If yes, run the full audit below.
3. If the user says they already ran it, ask when. If more than 24 hours
ago or if code changed since, recommend re-running.
## How It Works
### Step 1: Detect Stack
Run these checks in order to identify the project stack:
```
Framework detection:
package.json exists -> Node.js project
"next" in dependencies -> Next.js
"react" in dependencies -> React (if not Next.js)
"vue" in dependencies -> Vue
"svelte" in dependencies -> Svelte
"astro" in dependencies -> Astro
"express" in dependencies -> Express
"fastify" in dependencies -> Fastify
"hono" in dependencies -> Hono
requirements.txt or pyproject.toml -> Python project
"django" present -> Django
"flask" present -> Flask
"fastapi" present -> FastAPI
go.mod exists -> Go project
Cargo.toml exists -> Rust project
Database detection:
"@supabase/supabase-js" in package.json -> Supabase
supabase/ directory exists -> Supabase
"prisma" in dependencies -> Prisma (check schema for DB type)
"mongoose" in dependencies -> MongoDB
"pg" or "postgres" in dependencies -> PostgreSQL
firebase.json or .firebaserc exists -> Firebase
Deploy target detection:
vercel.json or .vercel/ exists -> Vercel
netlify.toml exists -> Netlify
Dockerfile exists -> Docker/VPS
fly.toml exists -> Fly.io
railway.json exists -> Railway
.platform/applications.yaml -> Platform.sh
Auth detection:
"@clerk" in dependencies -> Clerk
"next-auth" in dependencies -> NextAuth
"@supabase/auth-helpers" in deps -> Supabase Auth
"firebase/auth" in imports -> Firebase Auth
AI/LLM detection:
"openai" in dependencies -> OpenAI
"@anthropic-ai/sdk" in dependencies -> Claude API
"@google/generative-ai" in deps -> Gemini
```
Report detected stack before proceeding. This determines which checks
are relevant. Checks tagged with a specific stack in `references/checks.md`
are skipped if that stack is not detected.
### Step 2: Run Automated Checks
Run categories in this order: SEC, DB, CODE, DEP, AI, DEPLOY, FE, OBS.
Security and database first because they produce the most critical findings.
For each category, run every auto-scannable check from
`references/checks.md` using the patterns in `references/patterns.md`.
Report progress after each category completes:
```
[1/8] Security: 3 FAIL, 12 PASS, 3 SKIP
[2/8] Database: 1 FAIL, 5 PASS, 6 SKIP
...
```
Report results as:
- PASS: check passed
- FAIL: issue found (with file path and line number)
- SKIP: not applicable to this stack
### Step 3: Manual Confirmation
For checks that cannot be automated (backup restore tested, rollback plan
exists, staging test passed), present them as a checklist and ask the user
to confirm each one.
### Step 4: Verdict
Classify results into three severities:
- CRITICAL: must fix before shipping (secrets exposed, no auth on routes,
no HTTPS, SQL injection vectors, no RLS on Supabase tables)
- HIGH: should fix before shipping (no error boundaries, no rate limiting,
console.logs in production, no pagination)
- ADVISORY: recommended but not blocking (no OG tags, no custom 404,
no analytics, no SBOM)
Final output:
```
SHIP GATE REPORT
================
Stack: Next.js + Supabase + Vercel
Scan time: 12s
CRITICAL (3 items, must fix)
FAIL [SEC-01] API key found in src/lib/api.ts:14
FAIL [DB-07] RLS not enabled on "profiles" table
FAIL [SEC-05] No CSRF protection on /api/checkout
HIGH (5 items, should fix)
FAIL [CODE-01] 12 console.log statements in production code
FAIL [CODE-03] Empty catch block in src/utils/auth.ts:45
FAIL [DEP-04] 3 critical npm audit vulnerabilities
FAIL [DEPLOY-05] No rollback plan documented
MANUAL [DEPLOY-06] Staging test not confirmed
ADVISORY (4 items, recommended)
FAIL [FE-01] Missing OG meta tags
FAIL [FE-03] No custom 404 page
PASS [OBS-01] Error monitoring configured
SKIP [AI-01] No AI/LLM usage detected
VERDICT: DO NOT SHIP (3 critical issues)
Fix critical items and re-run.
```
If zero critical items remain, verdict is: CLEAR TO SHIP.
If only high items remain, verdict is: SHIP WITH CAUTION (acknowledge risks).
## Categories
Eight categories, each with a code prefix. Full check details in
`references/checks.md`.
| Prefix | Category | Auto | Manual | Tool |
|--------|----------|------|--------|------|
| SEC | Security | 15 | 3 | 0 |
| DB | Database | 7 | 5 | 0 |
| DEPLOY | Deployment | 3 | 8 | 0 |
| CODE | Code Quality | 11 | 0 | 1 |
| AI | AI/LLM Security | 5 | 3 | 0 |
| DEP | Dependencies | 5 | 0 | 1 |
| FE | Frontend Quality | 7 | 3 | 0 |
| OBS | Observability | 2 | 5 | 0 |
## Scope
This skill audits. It does not fix. When it finds issues, it reports
them with file locations and remediation guidance. The user or another
skill (systematic-debugging, backend-patterns, shadcn-stack) handles
the fix.
This skill does not:
- Set up CI/CD pipelines
- Provision infrastructure
- Configure monitoring tools
- Run after deployment (it is pre-deploy only)
## Integration Points
- **karpathy-coder**: run ship-gate after karpathy-check passes — simplicity first, then production readiness
- **adversarial-reviewer**: deep security review for items ship-gate flags as critical
- **security-pen-testing**: penetration testing methodology for SEC-category findings
- **code-reviewer**: general code quality review complements ship-gate's automated checks
FILE:references/checks.md
# Ship Gate: Complete Check Reference
All checks organized by category with ID, description, detection method,
severity, and remediation guidance.
## Table of Contents
- SEC: Security (18 checks)
- DB: Database (12 checks)
- DEPLOY: Deployment (13 checks)
- CODE: Code Quality (14 checks)
- AI: AI/LLM Security (8 checks)
- DEP: Dependencies and Supply Chain (7 checks)
- FE: Frontend Quality (10 checks)
- OBS: Observability (7 checks)
Detection methods:
- **auto**: Claude scans the codebase using grep, find, or file inspection
- **tool**: Claude runs an external tool (npm audit, etc.)
- **manual**: Claude asks the user to confirm
---
## SEC: Security
| ID | Check | Detection | Severity | Stack |
|----|-------|-----------|----------|-------|
| SEC-01 | No API keys or secrets in frontend code | auto | critical | all |
| SEC-02 | Every route checks authentication | auto | critical | all |
| SEC-03 | HTTPS enforced, HTTP redirected | manual | critical | all |
| SEC-04 | CORS locked to specific domain, not wildcard | auto | critical | all |
| SEC-05 | CSRF protection on state-changing endpoints | auto | critical | all |
| SEC-06 | Input validated and sanitized server-side | auto | high | all |
| SEC-07 | Rate limiting on auth and sensitive endpoints | auto | high | all |
| SEC-08 | Passwords hashed with bcrypt or argon2 | auto | critical | all |
| SEC-09 | Auth tokens have expiry | auto | high | all |
| SEC-10 | Sessions invalidated on logout (server-side) | manual | high | all |
| SEC-11 | CSP headers configured | auto | high | all |
| SEC-12 | JWT not using alg:none or weak secrets | auto | critical | all |
| SEC-13 | No eval() or dangerouslySetInnerHTML without sanitization | auto | high | js/ts |
| SEC-14 | No sensitive data in URL parameters or logs | auto | high | all |
| SEC-15 | Cookie security flags set (HttpOnly, Secure, SameSite) | auto | high | all |
| SEC-16 | File upload validates type, size, no path traversal | auto | high | all |
| SEC-17 | No hardcoded secrets in .env committed to repo | auto | critical | all |
| SEC-18 | .env files listed in .gitignore | auto | critical | all |
### SEC-01: No API keys or secrets in frontend code
Scan all files in src/, app/, pages/, public/, components/ for patterns
matching API keys, tokens, and secrets. See patterns.md for the full
regex list.
Remediation: Move secrets to environment variables. Use server-side API
routes to proxy requests that require secrets.
### SEC-02: Every route checks authentication
For Next.js: check middleware.ts/js exists and covers protected routes.
For Express: check that auth middleware is applied to route handlers.
For Django: check @login_required or permission decorators.
For generic: search for unprotected route definitions.
Remediation: Add authentication middleware. Audit every endpoint and
classify as public or protected.
### SEC-04: CORS not wildcard
Search for `cors({ origin: '*' })`, `Access-Control-Allow-Origin: *`,
or equivalent in the detected framework.
Remediation: Set CORS origin to your specific domain(s).
### SEC-05: CSRF protection
Check for CSRF token generation and validation on POST/PUT/DELETE routes.
For Next.js Server Actions, verify they use built-in CSRF protection.
Remediation: Add CSRF middleware or use framework-native CSRF protection.
### SEC-06: Input validation server-side
Search for request body usage (req.body, request.json, request.form)
without validation library imports (zod, yup, joi, class-validator,
pydantic). Check if raw user input flows directly into database queries
or business logic.
Remediation: Add input validation with zod, yup, or joi on every
endpoint that accepts user input.
### SEC-07: Rate limiting
Search for rate limiting middleware (express-rate-limit, @upstash/ratelimit,
rate-limiter-flexible, slowapi). Check auth routes and sensitive endpoints.
Remediation: Add rate limiting middleware. Start with auth endpoints
(login, register, password reset) and any endpoint that sends emails
or costs money.
### SEC-09: Auth token expiry
Search JWT sign calls for expiresIn/exp claims. Check if tokens are
created without expiry. Search for `sign(`, `jwt.encode(`, `createToken`.
Remediation: Set token expiry. Access tokens: 15-60 minutes.
Refresh tokens: 7-30 days. Never issue tokens without expiry.
### SEC-14: Sensitive data in URLs or logs
Search for query parameters containing keywords like password, token,
secret, key, ssn, credit_card. Search logging statements that log
full request objects or sensitive fields.
Remediation: Send sensitive data in request body or headers, never
in URL parameters. Redact sensitive fields before logging.
### SEC-16: File upload validation
Search for file upload handlers (multer, formidable, busboy,
UploadedFile). Check if file type, size, and path are validated.
Remediation: Validate file MIME type against an allowlist. Set
maximum file size. Sanitize filenames. Store outside webroot.
### SEC-12: JWT security
Search for `alg: 'none'`, `algorithm: 'none'`, or JWT secrets shorter
than 32 characters.
Remediation: Use RS256 or HS256 with a strong secret (32+ characters).
Never allow alg:none.
### SEC-17: No hardcoded secrets in .env committed
Check git history for .env files: `git log --all --name-only | grep .env`
Check if .env exists in the working tree and is not in .gitignore.
Remediation: Add .env* to .gitignore. Rotate any exposed secrets.
Use `git filter-branch` or BFG to remove from history if needed.
---
## DB: Database
| ID | Check | Detection | Severity | Stack |
|----|-------|-----------|----------|-------|
| DB-01 | Backups configured and tested | manual | critical | all |
| DB-02 | Backup restore tested (not just backup) | manual | critical | all |
| DB-03 | Parameterized queries everywhere | auto | critical | all |
| DB-04 | Separate dev and production databases | manual | high | all |
| DB-05 | Connection pooling configured | auto | high | all |
| DB-06 | Migrations in version control | auto | high | all |
| DB-07 | RLS enabled on all tables | auto | critical | supabase |
| DB-08 | No service_role key in client-side code | auto | critical | supabase |
| DB-09 | Anon key not used for writes without RLS | auto | high | supabase |
| DB-10 | Storage bucket policies configured | auto | high | supabase |
| DB-11 | App uses a non-root DB user | manual | high | all |
| DB-12 | No PII stored unencrypted | auto | high | all |
### DB-03: Parameterized queries
Search for string concatenation in SQL queries:
- Template literals with SQL keywords: `` `SELECT ... `"SELECT " + variable`
- f-strings with SQL: `f"SELECT ... {variable"`
Remediation: Use parameterized queries or ORM methods.
### DB-07: RLS enabled (Supabase)
Search migration files for `CREATE TABLE` without a corresponding
`ALTER TABLE ... ENABLE ROW LEVEL SECURITY` statement.
Also check for `CREATE POLICY` statements.
Remediation: Enable RLS on every table and create appropriate policies.
### DB-08: No service_role key in client code
Search frontend directories (src/, app/, components/, pages/) for
`service_role`, `supabase_service_role`, or the actual key pattern
`eyJ...` used with createClient on the client side.
Remediation: Use service_role only in server-side code (API routes,
Edge Functions, server actions).
### DB-05: Connection pooling
Search for database connection configuration. Check for pool settings
(max, min, idle timeout). For Supabase, check if using connection
pooler URL (port 6543) vs direct (port 5432).
Remediation: Use connection pooling for production. For Supabase,
use the pooler URL. For raw pg, configure pool size based on expected
concurrent connections.
### DB-06: Migrations in version control
Check if a migrations directory exists (supabase/migrations, prisma/
migrations, alembic/versions, db/migrate). Verify it contains .sql
or migration files, not empty.
Remediation: Use your ORM or database tool's migration system. Never
make manual schema changes to production.
### DB-09: Anon key writes without RLS
Search for Supabase client-side inserts/updates using the anon key
without RLS policies protecting the target tables.
Remediation: Enable RLS on all tables and create INSERT/UPDATE policies
that scope access to authenticated users.
### DB-10: Storage bucket policies
Search Supabase migration files and dashboard config for storage
bucket creation. Verify each bucket has access policies defined.
Remediation: Define storage policies for each bucket. Restrict
uploads by file type, size, and user ownership.
### DB-12: PII stored unencrypted
Search schema files and migration files for columns named email,
phone, ssn, social_security, credit_card, address, date_of_birth
that are stored as plain text without encryption.
Remediation: Encrypt PII columns at rest. Use database-level
encryption or application-level encryption for sensitive fields.
---
## DEPLOY: Deployment
| ID | Check | Detection | Severity | Stack |
|----|-------|-----------|----------|-------|
| DEPLOY-01 | All env vars set on production server | manual | critical | all |
| DEPLOY-02 | SSL certificate installed and valid | manual | critical | all |
| DEPLOY-03 | Firewall configured (only 80/443 public) | manual | high | vps |
| DEPLOY-04 | Process manager running | manual | high | vps |
| DEPLOY-05 | Rollback plan exists | manual | high | all |
| DEPLOY-06 | Staging test passed before production | manual | high | all |
| DEPLOY-07 | Deploy does not cause downtime | manual | advisory | all |
| DEPLOY-08 | Domain DNS configured (www vs non-www) | manual | high | all |
| DEPLOY-09 | Health check endpoint exists | auto | high | all |
| DEPLOY-10 | Logging configured (structured, not console) | auto | high | all |
| DEPLOY-11 | Error monitoring connected (Sentry, etc.) | auto | advisory | all |
| DEPLOY-12 | Cron jobs and background tasks verified | manual | high | all |
| DEPLOY-13 | CDN configured for static assets | manual | advisory | all |
### DEPLOY-09: Health check endpoint
Search for a `/health`, `/healthz`, `/api/health`, or `/status` route
that returns a 200 response.
Remediation: Add a health check endpoint that verifies database
connectivity and returns a simple JSON response.
### DEPLOY-10: Structured logging
Check if the project uses a logging library (winston, pino, bunyan,
python logging module) vs raw console.log statements in server code.
Remediation: Replace console.log with a structured logger that outputs
JSON with timestamps and request IDs.
---
## CODE: Code Quality
| ID | Check | Detection | Severity | Stack |
|----|-------|-----------|----------|-------|
| CODE-01 | No console.log in production build | auto | high | js/ts |
| CODE-02 | Error handling on all async operations | auto | high | all |
| CODE-03 | No empty catch blocks | auto | high | all |
| CODE-04 | Loading and error states in UI | auto | high | react |
| CODE-05 | Pagination on all list endpoints | auto | high | all |
| CODE-06 | npm audit clean (zero critical) | tool | high | js/ts |
| CODE-07 | No TODO-auth or TODO-security patterns | auto | critical | all |
| CODE-08 | No unhandled promise rejections | auto | high | js/ts |
| CODE-09 | React error boundaries in place | auto | high | react |
| CODE-10 | No leaked stack traces in error responses | auto | high | all |
| CODE-11 | No eslint-disable on security rules | auto | high | js/ts |
| CODE-12 | Lockfile committed | auto | high | all |
| CODE-13 | No wildcard versions in package.json | auto | high | js/ts |
| CODE-14 | TypeScript strict mode enabled | auto | advisory | ts |
### CODE-01: No console.log in production
Search for `console.log`, `console.debug`, `console.info` in source
files (exclude test files, config files, and node_modules).
Remediation: Remove console.log statements or replace with a proper
logger. Use a build tool to strip them automatically.
### CODE-03: No empty catch blocks
Search for `catch` blocks with empty bodies or only a comment inside.
Pattern: `catch\s*\([^)]*\)\s*\{\s*(\/\/.*\n)?\s*\}`
Remediation: At minimum, log the error. Better: handle it appropriately
or rethrow.
### CODE-07: No TODO-auth/security patterns
Search for `TODO.*auth`, `TODO.*security`, `TODO.*permission`,
`FIXME.*auth`, `HACK.*auth`, `// auth`, `# TODO: add auth`.
These indicate security features that were deferred and forgotten.
Remediation: Implement the deferred security feature or remove the
endpoint if it is not ready.
### CODE-09: React error boundaries
Check if the app has at least one ErrorBoundary component or uses
a library like react-error-boundary. Check app/error.tsx for Next.js
App Router projects.
Remediation: Add error boundaries at layout boundaries to prevent
full-page crashes.
### CODE-02: Error handling on async operations
Search for async functions and .then() chains. Check if they have
corresponding try/catch or .catch() handlers.
Remediation: Wrap every async operation in try/catch. Log errors
and show appropriate UI feedback.
### CODE-04: Loading and error states in UI
Search React components for data fetching (useEffect with fetch,
useSWR, useQuery, server components) and check if they render
loading and error states.
Remediation: Add loading spinners/skeletons and error messages
for every data-dependent component.
### CODE-05: Pagination on list endpoints
Search API routes that return arrays/lists from database queries.
Check for LIMIT/OFFSET, cursor pagination, or take/skip parameters.
Remediation: Add pagination to every endpoint that returns a list.
Default page size of 20-50 items. Never return unbounded result sets.
### CODE-10: No leaked stack traces
Search error handling code for responses that include stack traces,
error.stack, or full error objects sent to the client.
Remediation: Return generic error messages to the client. Log full
stack traces server-side only.
### CODE-11: No eslint-disable on security rules
Search for eslint-disable comments that suppress security-related
rules (no-eval, no-implied-eval, no-script-url).
Remediation: Fix the underlying issue instead of disabling the lint
rule. If genuinely necessary, add a comment explaining why.
### CODE-14: TypeScript strict mode
Check tsconfig.json for `"strict": true` or the individual flags
(strictNullChecks, noImplicitAny, etc.).
Remediation: Enable strict mode in tsconfig.json. Fix type errors
incrementally if migrating an existing project.
---
## AI: AI/LLM Security
| ID | Check | Detection | Severity | Stack |
|----|-------|-----------|----------|-------|
| AI-01 | System prompts not leakable via user input | auto | critical | ai |
| AI-02 | No prompt injection vectors in user inputs | auto | critical | ai |
| AI-03 | LLM API keys not in frontend code | auto | critical | ai |
| AI-04 | Rate limiting on AI endpoints (cost protection) | auto | high | ai |
| AI-05 | AI response output sanitized before rendering | auto | high | ai |
| AI-06 | MCP server inputs validated | auto | high | ai |
| AI-07 | Agent permissions scoped (no unrestricted access) | manual | high | ai |
| AI-08 | No sensitive data sent to third-party LLMs without consent | manual | high | ai |
### AI-01: System prompt leakage
Search for system prompts stored in client-accessible files or returned
in API responses. Check if the AI endpoint echoes the system prompt
when asked "repeat your instructions" or similar.
Remediation: Keep system prompts server-side only. Add input filtering
for prompt extraction attempts.
### AI-03: LLM API keys not in frontend
Search frontend code for `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`,
`GOOGLE_AI_API_KEY`, `sk-ant-`, `sk-proj-`, `AIza` patterns.
Remediation: Proxy all LLM calls through server-side API routes.
---
## DEP: Dependencies and Supply Chain
| ID | Check | Detection | Severity | Stack |
|----|-------|-----------|----------|-------|
| DEP-01 | No git:// or URL-based dependencies | auto | high | all |
| DEP-02 | No typosquatting risk (verify package names) | auto | advisory | all |
| DEP-03 | Lockfile integrity verified | auto | high | all |
| DEP-04 | npm audit / pip audit zero critical | tool | high | all |
| DEP-05 | No suspicious postinstall scripts | auto | high | js/ts |
| DEP-06 | Dependencies pinned (no wildcard *) | auto | high | all |
| DEP-07 | Lockfile committed to version control | auto | high | all |
### DEP-01: No git/URL dependencies
Search package.json for dependencies with values starting with
`git://`, `git+`, `http://`, `https://github.com`, or `file:`.
Remediation: Use published npm packages with version ranges instead
of git URLs.
### DEP-05: Suspicious postinstall scripts
Check package.json for `postinstall`, `preinstall`, `install` scripts
that execute arbitrary commands, download files, or access the network.
Remediation: Review and remove unnecessary install scripts. Use
`--ignore-scripts` for CI.
---
## FE: Frontend Quality
| ID | Check | Detection | Severity | Stack |
|----|-------|-----------|----------|-------|
| FE-01 | Meta tags present (title, description, OG tags) | auto | advisory | web |
| FE-02 | Favicon configured | auto | advisory | web |
| FE-03 | Custom 404 page exists | auto | advisory | web |
| FE-04 | Responsive design tested on mobile | manual | high | web |
| FE-05 | Alt text on images | auto | high | web |
| FE-06 | Keyboard navigation works | manual | high | web |
| FE-07 | Forms have validation feedback | auto | high | web |
| FE-08 | Analytics installed (production only) | auto | advisory | web |
| FE-09 | robots.txt present | auto | advisory | web |
| FE-10 | Images optimized (WebP, lazy loading) | auto | advisory | web |
### FE-01: Meta tags
Check the root layout or index page for `<title>`, `<meta name="description">`,
and Open Graph tags (`og:title`, `og:description`, `og:image`).
For Next.js, check metadata export in layout.tsx.
Remediation: Add metadata to your root layout or page head.
### FE-03: Custom 404 page
Check for `404.tsx`, `404.jsx`, `not-found.tsx`, `404.html`, or
equivalent in the pages/app directory.
Remediation: Create a branded 404 page that helps users navigate back.
---
## OBS: Observability
| ID | Check | Detection | Severity | Stack |
|----|-------|-----------|----------|-------|
| OBS-01 | Error monitoring configured (Sentry, LogRocket, etc.) | auto | advisory | all |
| OBS-02 | Alerting set up for critical failures | manual | high | all |
| OBS-03 | Structured logging with request IDs | auto | advisory | all |
| OBS-04 | Performance baseline established | manual | advisory | all |
| OBS-05 | Uptime monitoring configured | manual | high | all |
| OBS-06 | Error rates tracked | manual | advisory | all |
| OBS-07 | Log retention policy defined | manual | advisory | all |
### OBS-01: Error monitoring
Search for imports or configuration of error monitoring tools:
`@sentry/`, `LogRocket`, `Bugsnag`, `Datadog`, `Rollbar`, `Honeybadger`.
Remediation: Install and configure an error monitoring service.
Sentry has a free tier suitable for solo projects.
FILE:references/patterns.md
# Ship Gate: Detection Patterns
Grep and regex patterns for auto-scannable checks. Claude runs these
against the codebase to detect issues.
## Table of Contents
- SEC: Security Patterns
- DB: Database Patterns
- CODE: Code Quality Patterns
- AI: AI/LLM Security Patterns
- DEP: Dependency Patterns
- FE: Frontend Quality Patterns
- OBS: Observability Patterns
- DEPLOY: Deployment Patterns
All patterns use `grep -rn` with `--include` filters. Exclude
node_modules, .next, dist, build, .git, __pycache__, venv directories
from all scans.
Base exclude flags:
```bash
EXCLUDE="--exclude-dir=node_modules --exclude-dir=.next --exclude-dir=dist --exclude-dir=build --exclude-dir=.git --exclude-dir=__pycache__ --exclude-dir=venv --exclude-dir=.venv --exclude-dir=vendor --exclude-dir=coverage"
```
---
## SEC: Security Patterns
### SEC-01: Secrets in frontend code
Scan directories that serve client-side code:
```bash
# Generic API key patterns
grep -rnE $EXCLUDE \
"(sk-[a-zA-Z0-9]{20,}|sk-ant-[a-zA-Z0-9-]+|sk-proj-[a-zA-Z0-9-]+|AIza[a-zA-Z0-9_-]{35}|ghp_[a-zA-Z0-9]{36}|glpat-[a-zA-Z0-9_-]{20,}|xox[bsap]-[a-zA-Z0-9-]+)" \
src/ app/ pages/ components/ public/ lib/ utils/ 2>/dev/null
# AWS keys
grep -rnE $EXCLUDE \
"AKIA[0-9A-Z]{16}" \
src/ app/ pages/ components/ public/ 2>/dev/null
# Stripe keys (live, not test)
grep -rnE $EXCLUDE \
"sk_live_[a-zA-Z0-9]{24,}" \
src/ app/ pages/ components/ public/ 2>/dev/null
# Generic secret assignment
grep -rnE $EXCLUDE \
"(api_key|apikey|api_secret|secret_key|auth_token|access_token)\s*[:=]\s*['\"][a-zA-Z0-9_-]{16,}" \
src/ app/ pages/ components/ public/ 2>/dev/null
```
### SEC-04: CORS wildcard
```bash
grep -rnE $EXCLUDE \
"(origin:\s*['\"]?\*['\"]?|Access-Control-Allow-Origin.*\*|cors\(\s*\))" \
. 2>/dev/null
```
### SEC-05: CSRF protection missing
```bash
# Check for state-changing routes without CSRF
grep -rnE $EXCLUDE \
"(app\.(post|put|patch|delete)|router\.(post|put|patch|delete))" \
. 2>/dev/null
# Then verify csrf middleware exists
grep -rnE $EXCLUDE \
"(csrf|csrfToken|_csrf|CSRF_COOKIE)" \
. 2>/dev/null
```
### SEC-08: Weak password hashing
```bash
# Check for weak hashing (md5, sha1, sha256 for passwords)
grep -rnE $EXCLUDE \
"(md5|sha1|sha256)\s*\(" \
. 2>/dev/null
# Verify bcrypt/argon2 usage
grep -rnE $EXCLUDE \
"(bcrypt|argon2|scrypt)" \
. 2>/dev/null
```
### SEC-11: CSP headers
```bash
# Check for Content-Security-Policy configuration
grep -rnE $EXCLUDE \
"(Content-Security-Policy|contentSecurityPolicy|csp)" \
. 2>/dev/null
# Next.js: check next.config for headers
grep -rn $EXCLUDE \
"Content-Security-Policy" \
next.config.* 2>/dev/null
```
### SEC-13: Unsafe eval/innerHTML
```bash
# eval usage
grep -rnE $EXCLUDE \
"(\beval\s*\(|new\s+Function\s*\()" \
--include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" \
. 2>/dev/null
# dangerouslySetInnerHTML without sanitizer
grep -rnE $EXCLUDE \
"dangerouslySetInnerHTML" \
--include="*.jsx" --include="*.tsx" \
. 2>/dev/null
# Then check if DOMPurify or similar is imported in same file
```
### SEC-15: Cookie security flags
```bash
grep -rnE $EXCLUDE \
"(set-cookie|setCookie|cookie\()" \
. 2>/dev/null
# Verify HttpOnly, Secure, SameSite flags are present
grep -rnE $EXCLUDE \
"(httpOnly|HttpOnly|secure:\s*true|sameSite)" \
. 2>/dev/null
```
### SEC-06: Input validation
```bash
# Check for validation library usage
grep -rnE $EXCLUDE \
"(from 'zod'|from 'yup'|from 'joi'|from 'class-validator'|from pydantic)" \
. 2>/dev/null
# Check for raw req.body usage without validation
grep -rnE $EXCLUDE \
"(req\.body\.|request\.json|request\.form)" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/null
```
### SEC-07: Rate limiting
```bash
grep -rnE $EXCLUDE \
"(express-rate-limit|@upstash/ratelimit|rate-limiter|slowapi|throttle)" \
package.json requirements.txt . 2>/dev/null
```
### SEC-09: Token expiry
```bash
grep -rnE $EXCLUDE \
"(sign\(|jwt\.encode|createToken|signToken)" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/null
# Then check if expiresIn/exp is set in those calls
grep -rnE $EXCLUDE \
"(expiresIn|exp:|expires_in|expires_delta)" \
. 2>/dev/null
```
### SEC-14: Sensitive data in URLs/logs
```bash
# Sensitive query parameters
grep -rnE $EXCLUDE \
"(password|token|secret|key|ssn|credit.card)=" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/null
# Logging full request objects
grep -rnE $EXCLUDE \
"console\.(log|info|debug)\s*\(\s*(req|request)\s*\)" \
. 2>/dev/null
```
### SEC-16: File upload validation
```bash
grep -rnE $EXCLUDE \
"(multer|formidable|busboy|UploadedFile|upload\.single|upload\.array)" \
. 2>/dev/null
# Check for file type/size validation near upload handlers
grep -rnE $EXCLUDE \
"(fileFilter|limits|maxFileSize|allowedTypes|mimetype)" \
. 2>/dev/null
```
### SEC-17/18: .env in repo
```bash
# Check if .env files exist in working tree
find . -maxdepth 3 -name ".env*" -not -path "*/node_modules/*" \
-not -name ".env.example" -not -name ".env.sample" 2>/dev/null
# Check if .env is in .gitignore
grep -n "\.env" .gitignore 2>/dev/null
# Check git history for .env commits
git log --all --name-only --diff-filter=A 2>/dev/null | grep "\.env" || true
```
---
## DB: Database Patterns
### DB-03: SQL injection (string concatenation)
```bash
# Template literal SQL
grep -rnE $EXCLUDE \
"(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE).*\$\{" \
--include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" \
. 2>/dev/null
# String concat SQL
grep -rnE $EXCLUDE \
"(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE).*\+\s*(req\.|params\.|body\.|query\.)" \
. 2>/dev/null
# Python f-string SQL
grep -rnE $EXCLUDE \
"f['\"].*\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)\b.*\{" \
--include="*.py" \
. 2>/dev/null
```
### DB-07: Supabase RLS
```bash
# Find CREATE TABLE without RLS
grep -rnl $EXCLUDE "CREATE TABLE" \
--include="*.sql" . 2>/dev/null | while read f; do
tables=$(grep -oP "CREATE TABLE\s+\K\S+" "$f")
for t in $tables; do
if ! grep -q "ENABLE ROW LEVEL SECURITY" "$f" || \
! grep -q "$t" <<< "$(grep 'ENABLE ROW LEVEL SECURITY' "$f")"; then
echo "FAIL: $f - table $t missing RLS"
fi
done
done
```
### DB-08: service_role in client code
```bash
grep -rnE $EXCLUDE \
"(service_role|serviceRole|SUPABASE_SERVICE_ROLE)" \
src/ app/ pages/ components/ public/ lib/client 2>/dev/null
```
### DB-05: Connection pooling
```bash
# Check for pool configuration
grep -rnE $EXCLUDE \
"(pool|connectionLimit|max_connections|poolSize)" \
--include="*.ts" --include="*.js" --include="*.py" --include="*.env*" \
. 2>/dev/null
# Supabase: check if using pooler port
grep -rnE $EXCLUDE \
"(6543|pooler)" \
--include="*.env*" --include="*.ts" --include="*.js" \
. 2>/dev/null
```
### DB-06: Migrations in version control
```bash
# Check for migration directories
find . -maxdepth 3 -type d \
\( -name "migrations" -o -name "migrate" -o -name "versions" \) \
-not -path "*/node_modules/*" 2>/dev/null
# Check if migrations contain files
find . -path "*/migrations/*.sql" -o -path "*/migrations/*.ts" \
-o -path "*/migrations/*.py" 2>/dev/null | head -5
```
### DB-12: PII stored unencrypted
```bash
# Search schema files for PII column names
grep -rnEi $EXCLUDE \
"(ssn|social_security|credit_card|card_number|passport)" \
--include="*.sql" --include="*.prisma" --include="*.py" \
. 2>/dev/null
```
---
## CODE: Code Quality Patterns
### CODE-01: console.log in production
```bash
grep -rnE $EXCLUDE \
"console\.(log|debug|info)\(" \
--include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" \
--exclude="*.test.*" --exclude="*.spec.*" --exclude="*.config.*" \
src/ app/ pages/ components/ lib/ utils/ 2>/dev/null
```
### CODE-03: Empty catch blocks
```bash
grep -rnPzo $EXCLUDE \
"catch\s*\([^)]*\)\s*\{\s*\}" \
--include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" \
. 2>/dev/null
```
### CODE-07: TODO-auth patterns
```bash
grep -rnEi $EXCLUDE \
"(TODO|FIXME|HACK|XXX).*(auth|security|permission|validation|sanitiz)" \
. 2>/dev/null
```
### CODE-08: Unhandled promise rejections
```bash
# Async functions without try-catch
grep -rnE $EXCLUDE \
"async\s+\w+\s*\(" \
--include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" \
. 2>/dev/null
# Check for .catch() or try/catch wrapping
```
### CODE-09: React error boundaries
```bash
# Check for error boundary in Next.js App Router
find . -path "*/app/error.tsx" -o -path "*/app/error.jsx" \
-o -path "*/app/global-error.tsx" 2>/dev/null
# Check for ErrorBoundary component
grep -rnE $EXCLUDE \
"(ErrorBoundary|error-boundary|componentDidCatch|getDerivedStateFromError)" \
--include="*.jsx" --include="*.tsx" \
. 2>/dev/null
```
### CODE-12: Lockfile committed
```bash
# Check for lockfile existence
ls package-lock.json pnpm-lock.yaml yarn.lock bun.lockb \
Pipfile.lock poetry.lock Gemfile.lock go.sum Cargo.lock 2>/dev/null
# Check if lockfile is gitignored
for f in package-lock.json pnpm-lock.yaml yarn.lock; do
if git check-ignore "$f" 2>/dev/null; then
echo "FAIL: $f is gitignored"
fi
done
```
### CODE-13: Wildcard versions
```bash
# Check for * or empty version in package.json
grep -nE '"[^"]+"\s*:\s*"\*"' package.json 2>/dev/null
```
### CODE-02: Async without error handling
```bash
# Find async functions
grep -rnE $EXCLUDE \
"async\s+(function\s+)?\w+\s*\(" \
--include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" \
. 2>/dev/null
# Count try/catch usage nearby
grep -rnc $EXCLUDE "try\s*{" \
--include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" \
. 2>/dev/null
```
### CODE-04: Loading and error states
```bash
# Check for loading state patterns in React
grep -rnE $EXCLUDE \
"(isLoading|loading|Skeleton|Spinner|fallback)" \
--include="*.tsx" --include="*.jsx" \
. 2>/dev/null
# Check for Suspense boundaries
grep -rnE $EXCLUDE \
"(<Suspense|loading\.tsx|loading\.jsx)" \
. 2>/dev/null
```
### CODE-05: Pagination on list endpoints
```bash
# Check API routes for unbounded queries
grep -rnE $EXCLUDE \
"(\.findMany|\.find\(\)|\.select\(\)|SELECT \*)" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/null
# Check for pagination parameters
grep -rnE $EXCLUDE \
"(limit|offset|page|skip|take|cursor|per_page)" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/null
```
### CODE-10: Leaked stack traces
```bash
grep -rnE $EXCLUDE \
"(error\.stack|\.stack\)|err\.message.*res\.(json|send)|traceback)" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/null
```
### CODE-11: eslint-disable on security rules
```bash
grep -rnE $EXCLUDE \
"eslint-disable.*(no-eval|no-implied-eval|no-script-url|security)" \
--include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" \
. 2>/dev/null
```
### CODE-14: TypeScript strict mode
```bash
grep -n '"strict"' tsconfig.json 2>/dev/null
# Check if strict is true
grep -n '"strict":\s*true' tsconfig.json 2>/dev/null
```
---
## AI: AI/LLM Security Patterns
### AI-01: System prompt leakage
```bash
# System prompts in client-accessible files
grep -rnEi $EXCLUDE \
"(system.?prompt|system.?message|system_instruction)" \
src/ app/ pages/ components/ public/ 2>/dev/null
# System prompts returned in API responses
grep -rnE $EXCLUDE \
"(system.*role|role.*system)" \
src/ app/ pages/ components/ public/ 2>/dev/null
```
### AI-02: Prompt injection vectors
```bash
# User input concatenated directly into prompts
grep -rnE $EXCLUDE \
"(messages\.push|content:.*\$\{|content:.*\+\s*user|prompt.*\+)" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/null
```
### AI-03: LLM API keys in frontend
```bash
grep -rnE $EXCLUDE \
"(OPENAI_API_KEY|ANTHROPIC_API_KEY|GOOGLE_AI_API_KEY|sk-ant-|sk-proj-|AIza[a-zA-Z0-9_-]{35})" \
src/ app/ pages/ components/ public/ 2>/dev/null
```
### AI-04: Rate limiting on AI endpoints
```bash
# Find AI-related API routes
grep -rnlE $EXCLUDE \
"(openai|anthropic|claude|gpt|completion|chat/api|ai/api)" \
--include="*.ts" --include="*.js" \
. 2>/dev/null
# Then check for rate limiting middleware in those files
```
### AI-05: AI output sanitization
```bash
# Check if AI responses are rendered with dangerouslySetInnerHTML
grep -rnE $EXCLUDE \
"dangerouslySetInnerHTML.*\b(response|result|completion|message|content)\b" \
--include="*.tsx" --include="*.jsx" \
. 2>/dev/null
```
### AI-06: MCP server input validation
```bash
# Check MCP server tool handlers for input validation
grep -rnE $EXCLUDE \
"(tool_input|toolInput|tool_call|CallToolRequest)" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/null
# Check if zod/validation is applied to tool inputs
```
---
## DEP: Dependency Patterns
### DEP-01: Git/URL dependencies
```bash
grep -nE '"(git|git\+|http|https|file):' package.json 2>/dev/null
grep -nE '"github:' package.json 2>/dev/null
```
### DEP-04: npm audit
```bash
# Run npm audit and capture critical/high counts
npm audit --json 2>/dev/null | grep -c '"severity":"critical"'
npm audit --json 2>/dev/null | grep -c '"severity":"high"'
# Or for pip
pip audit --format json 2>/dev/null
```
### DEP-05: Suspicious install scripts
```bash
grep -A2 '"preinstall"\|"postinstall"\|"install"' package.json 2>/dev/null
```
### DEP-06: Wildcard versions
```bash
grep -nE '"\*"' package.json 2>/dev/null
grep -nE '"latest"' package.json 2>/dev/null
```
---
## FE: Frontend Quality Patterns
### FE-01: Meta tags
```bash
# Next.js App Router metadata
grep -rnE $EXCLUDE \
"(export\s+(const|async\s+function)\s+metadata|generateMetadata)" \
--include="*.tsx" --include="*.ts" \
app/layout.* app/page.* 2>/dev/null
# HTML meta tags
grep -rnE $EXCLUDE \
'(<title>|<meta\s+name="description"|og:title|og:description|og:image)' \
. 2>/dev/null
```
### FE-02: Favicon
```bash
find . -maxdepth 3 \( -name "favicon.*" -o -name "icon.*" \) \
-not -path "*/node_modules/*" 2>/dev/null
```
### FE-03: Custom 404 page
```bash
find . -maxdepth 4 \( -name "404.*" -o -name "not-found.*" \) \
-not -path "*/node_modules/*" 2>/dev/null
```
### FE-05: Image alt text
```bash
# Find img tags without alt attribute
grep -rnE $EXCLUDE \
'<img\s+(?![^>]*\balt\b)[^>]*>' \
--include="*.html" --include="*.jsx" --include="*.tsx" \
. 2>/dev/null
# Next.js Image without alt
grep -rnE $EXCLUDE \
'<Image\s+(?![^>]*\balt\b)[^>]*/?>' \
--include="*.jsx" --include="*.tsx" \
. 2>/dev/null
```
### FE-09: robots.txt
```bash
find . -maxdepth 2 -name "robots.txt" \
-not -path "*/node_modules/*" 2>/dev/null
```
### FE-07: Form validation feedback
```bash
# Check for form elements without validation attributes
grep -rnE $EXCLUDE \
'(<input|<textarea|<select)' \
--include="*.tsx" --include="*.jsx" --include="*.html" \
. 2>/dev/null
# Check for validation library usage
grep -rnE $EXCLUDE \
"(useForm|react-hook-form|formik|yup|zod.*form)" \
--include="*.tsx" --include="*.jsx" \
. 2>/dev/null
```
### FE-10: Image optimization
```bash
# Check for unoptimized img tags (not using Next/Image or similar)
grep -rnE $EXCLUDE \
'<img\s' \
--include="*.tsx" --include="*.jsx" \
. 2>/dev/null
# Check for lazy loading
grep -rnE $EXCLUDE \
'(loading="lazy"|lazy|lazyload)' \
--include="*.tsx" --include="*.jsx" --include="*.html" \
. 2>/dev/null
```
---
## OBS: Observability Patterns
### OBS-01: Error monitoring
```bash
grep -rnE $EXCLUDE \
"(@sentry|sentry-|LogRocket|Bugsnag|datadogRum|Rollbar|Honeybadger|newrelic)" \
package.json . 2>/dev/null
```
### OBS-03: Structured logging
```bash
# Check for logging libraries
grep -rnE $EXCLUDE \
"(winston|pino|bunyan|morgan|log4js)" \
package.json 2>/dev/null
# Python
grep -rnE $EXCLUDE \
"import logging|from loguru" \
--include="*.py" . 2>/dev/null
```
---
## DEPLOY: Deployment Patterns
### DEPLOY-09: Health check endpoint
```bash
grep -rnE $EXCLUDE \
"(\/health|\/healthz|\/api\/health|\/status|\/readyz)" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/null
```
### DEPLOY-10: Console vs structured logging (server)
```bash
# Count console.log vs logger usage in API/server code
echo "console.log count:"
grep -rnc $EXCLUDE "console\.log" \
--include="*.ts" --include="*.js" \
api/ server/ pages/api/ app/api/ 2>/dev/null | tail -1
echo "structured logger count:"
grep -rnc $EXCLUDE "(logger\.|log\.(info|warn|error|debug))" \
--include="*.ts" --include="*.js" \
api/ server/ pages/api/ app/api/ 2>/dev/null | tail -1
```
FILE:scripts/ship_gate_scanner.py
#!/usr/bin/env python3
"""
ship_gate_scanner.py — Pre-production audit CLI
Part of the ship-gate skill: https://github.com/rx4u/ship-gate
Usage:
python scripts/ship_gate_scanner.py [PATH] [options]
Options:
--json Output results as JSON
--no-color Disable ANSI color output
--no-interactive Skip manual confirmation prompts
--category CAT Only run a specific category (SEC, DB, CODE, etc.)
--verbose Show PASS results in addition to FAIL
--version Show version and exit
Exit codes:
0 = CLEAR TO SHIP (no critical issues)
1 = DO NOT SHIP (critical issues found)
2 = SHIP WITH CAUTION (high issues only)
"""
import argparse
import json
import os
import re
import sys
import time
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import List, Optional
VERSION = "1.0.0"
EXCLUDE_DIRS = {
"node_modules", ".next", "dist", "build", ".git", "__pycache__",
"venv", ".venv", "vendor", "coverage", ".turbo", "out", ".cache",
".pytest_cache", ".mypy_cache", "target", "bin", "obj",
}
FRONTEND_DIRS = {"src", "app", "pages", "components", "public", "lib", "utils"}
JS_EXTS = {".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"}
PY_EXTS = {".py"}
ALL_CODE_EXTS = JS_EXTS | PY_EXTS | {".go", ".rb", ".php"}
TEMPLATE_EXTS = {".html", ".jsx", ".tsx", ".vue", ".svelte"}
SQL_EXTS = {".sql", ".prisma"}
# ---------------------------------------------------------------------------
# ANSI helpers
# ---------------------------------------------------------------------------
USE_COLOR = True
def _c(code: str, text: str) -> str:
if not USE_COLOR:
return text
return f"\033[{code}m{text}\033[0m"
def red(t): return _c("31", t)
def green(t): return _c("32", t)
def yellow(t): return _c("33", t)
def cyan(t): return _c("36", t)
def bold(t): return _c("1", t)
def dim(t): return _c("2", t)
# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------
class Status(str, Enum):
PASS = "PASS"
FAIL = "FAIL"
SKIP = "SKIP"
MANUAL = "MANUAL"
class Severity(str, Enum):
CRITICAL = "CRITICAL"
HIGH = "HIGH"
ADVISORY = "ADVISORY"
@dataclass
class Finding:
file: str
line: int
snippet: str = ""
@dataclass
class CheckDef:
id: str
description: str
severity: Severity
category: str
stack: str = "all" # "all", "js", "ts", "react", "supabase", "ai", "web", "vps"
@dataclass
class Result:
check: CheckDef
status: Status
message: str = ""
findings: List[Finding] = field(default_factory=list)
@dataclass
class Stack:
has_node: bool = False
framework: str = "" # next, react, vue, svelte, astro, express, fastify, hono
has_python: bool = False
py_framework: str = "" # django, flask, fastapi
has_go: bool = False
has_rust: bool = False
has_supabase: bool = False
has_typescript: bool = False
has_react: bool = False
deploy_target: str = "" # vercel, netlify, docker, fly, railway
has_ai: bool = False
ai_providers: List[str] = field(default_factory=list)
is_web: bool = False
# ---------------------------------------------------------------------------
# File walking / grep helpers
# ---------------------------------------------------------------------------
def walk_files(root: str, exts: Optional[set] = None, dirs: Optional[set] = None):
"""Yield (filepath, relpath) for all files under root, skipping EXCLUDE_DIRS."""
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
if dirs is not None:
rel = os.path.relpath(dirpath, root)
top = rel.split(os.sep)[0]
if rel != "." and top not in dirs:
dirnames[:] = []
continue
for fname in filenames:
if exts is None or os.path.splitext(fname)[1].lower() in exts:
fpath = os.path.join(dirpath, fname)
yield fpath, os.path.relpath(fpath, root)
def grep_files(
root: str,
pattern: str,
exts: Optional[set] = None,
dirs: Optional[set] = None,
flags: int = 0,
max_findings: int = 20,
exclude_patterns: Optional[List[str]] = None,
) -> List[Finding]:
"""Return up to max_findings matches across the codebase."""
try:
rx = re.compile(pattern, flags)
except re.error:
return []
exclude_rxs = []
if exclude_patterns:
for ep in exclude_patterns:
try:
exclude_rxs.append(re.compile(ep))
except re.error:
pass
results: List[Finding] = []
for fpath, relpath in walk_files(root, exts, dirs):
if any(seg in fpath for seg in (".test.", ".spec.", ".config.")):
if exts and exts <= JS_EXTS:
skip = True
# still yield for config-specific checks
if "tsconfig" in fpath or "package.json" in fpath:
skip = False
if skip:
continue
try:
with open(fpath, "r", encoding="utf-8", errors="ignore") as fh:
for lineno, line in enumerate(fh, 1):
if rx.search(line):
if any(ex.search(line) for ex in exclude_rxs):
continue
results.append(Finding(
file=relpath,
line=lineno,
snippet=line.rstrip()[:120],
))
if len(results) >= max_findings:
return results
except (OSError, PermissionError):
continue
return results
def file_exists_in(root: str, *names: str) -> Optional[str]:
"""Return the first found path among names (searched recursively up to depth 5)."""
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
depth = dirpath.replace(root, "").count(os.sep)
if depth >= 5:
dirnames[:] = []
continue
for fname in filenames:
if fname in names:
return os.path.join(dirpath, fname)
return None
def read_json_file(path: str) -> dict:
try:
with open(path) as f:
return json.load(f)
except Exception:
return {}
# ---------------------------------------------------------------------------
# Stack detection
# ---------------------------------------------------------------------------
def detect_stack(root: str) -> Stack:
s = Stack()
pkg_path = os.path.join(root, "package.json")
if os.path.isfile(pkg_path):
s.has_node = True
pkg = read_json_file(pkg_path)
all_deps = {}
for key in ("dependencies", "devDependencies", "peerDependencies"):
all_deps.update(pkg.get(key, {}))
if "next" in all_deps: s.framework = "next"
elif "react" in all_deps: s.framework = "react"
elif "vue" in all_deps: s.framework = "vue"
elif "svelte" in all_deps: s.framework = "svelte"
elif "astro" in all_deps: s.framework = "astro"
elif "express" in all_deps: s.framework = "express"
elif "fastify" in all_deps: s.framework = "fastify"
elif "hono" in all_deps: s.framework = "hono"
s.has_react = s.framework in ("next", "react")
s.is_web = s.framework in ("next", "react", "vue", "svelte", "astro")
if "@supabase/supabase-js" in all_deps:
s.has_supabase = True
if "typescript" in all_deps or os.path.isfile(os.path.join(root, "tsconfig.json")):
s.has_typescript = True
for ai_pkg in ("openai", "@anthropic-ai/sdk", "@google/generative-ai",
"ai", "@huggingface/inference"):
if ai_pkg in all_deps:
s.has_ai = True
s.ai_providers.append(ai_pkg)
if os.path.isdir(os.path.join(root, "supabase")):
s.has_supabase = True
for pyfile in ("requirements.txt", "pyproject.toml", "Pipfile", "setup.py"):
if os.path.isfile(os.path.join(root, pyfile)):
s.has_python = True
try:
content = open(os.path.join(root, pyfile)).read().lower()
if "django" in content: s.py_framework = "django"
elif "flask" in content: s.py_framework = "flask"
elif "fastapi" in content: s.py_framework = "fastapi"
except Exception:
pass
break
if os.path.isfile(os.path.join(root, "go.mod")):
s.has_go = True
if os.path.isfile(os.path.join(root, "Cargo.toml")):
s.has_rust = True
if os.path.isfile(os.path.join(root, "vercel.json")) or \
os.path.isdir(os.path.join(root, ".vercel")):
s.deploy_target = "vercel"
elif os.path.isfile(os.path.join(root, "netlify.toml")):
s.deploy_target = "netlify"
elif os.path.isfile(os.path.join(root, "fly.toml")):
s.deploy_target = "fly"
elif os.path.isfile(os.path.join(root, "railway.json")):
s.deploy_target = "railway"
elif os.path.isfile(os.path.join(root, "Dockerfile")):
s.deploy_target = "docker"
return s
# ---------------------------------------------------------------------------
# Check definitions
# ---------------------------------------------------------------------------
CHECKS = {
# SEC
"SEC-01": CheckDef("SEC-01", "No API keys or secrets in frontend code", Severity.CRITICAL, "SEC"),
"SEC-04": CheckDef("SEC-04", "CORS not wildcard", Severity.CRITICAL, "SEC"),
"SEC-05": CheckDef("SEC-05", "CSRF protection on state-changing endpoints", Severity.CRITICAL, "SEC"),
"SEC-06": CheckDef("SEC-06", "Input validated and sanitized server-side", Severity.HIGH, "SEC"),
"SEC-07": CheckDef("SEC-07", "Rate limiting on auth and sensitive endpoints", Severity.HIGH, "SEC"),
"SEC-08": CheckDef("SEC-08", "Passwords hashed with bcrypt or argon2", Severity.CRITICAL, "SEC"),
"SEC-11": CheckDef("SEC-11", "CSP headers configured", Severity.HIGH, "SEC"),
"SEC-13": CheckDef("SEC-13", "No eval() or dangerouslySetInnerHTML without sanitization", Severity.HIGH, "SEC", stack="js"), # noqa: SEC-AUDITOR
"SEC-14": CheckDef("SEC-14", "No sensitive data in URLs or logs", Severity.HIGH, "SEC"),
"SEC-17": CheckDef("SEC-17", "No hardcoded secrets in .env committed to repo", Severity.CRITICAL, "SEC"),
"SEC-18": CheckDef("SEC-18", ".env files listed in .gitignore", Severity.CRITICAL, "SEC"),
# DB
"DB-03": CheckDef("DB-03", "Parameterized queries everywhere (no SQL injection)", Severity.CRITICAL, "DB"),
"DB-05": CheckDef("DB-05", "Connection pooling configured", Severity.HIGH, "DB"),
"DB-06": CheckDef("DB-06", "Migrations in version control", Severity.HIGH, "DB"),
"DB-07": CheckDef("DB-07", "RLS enabled on all Supabase tables", Severity.CRITICAL, "DB", stack="supabase"),
"DB-08": CheckDef("DB-08", "No service_role key in client-side code", Severity.CRITICAL, "DB", stack="supabase"),
"DB-12": CheckDef("DB-12", "No PII stored unencrypted", Severity.HIGH, "DB"),
# DEPLOY
"DEPLOY-09": CheckDef("DEPLOY-09", "Health check endpoint exists", Severity.HIGH, "DEPLOY"),
"DEPLOY-10": CheckDef("DEPLOY-10", "Structured logging (not raw console)", Severity.HIGH, "DEPLOY"),
# CODE
"CODE-01": CheckDef("CODE-01", "No console.log in production build", Severity.HIGH, "CODE", stack="js"),
"CODE-03": CheckDef("CODE-03", "No empty catch blocks", Severity.HIGH, "CODE"),
"CODE-07": CheckDef("CODE-07", "No TODO-auth or TODO-security patterns", Severity.CRITICAL, "CODE"),
"CODE-09": CheckDef("CODE-09", "React error boundaries in place", Severity.HIGH, "CODE", stack="react"),
"CODE-10": CheckDef("CODE-10", "No leaked stack traces in error responses", Severity.HIGH, "CODE"),
"CODE-11": CheckDef("CODE-11", "No eslint-disable on security rules", Severity.HIGH, "CODE", stack="js"),
"CODE-12": CheckDef("CODE-12", "Lockfile committed", Severity.HIGH, "CODE"),
"CODE-13": CheckDef("CODE-13", "No wildcard versions in package.json", Severity.HIGH, "CODE", stack="js"),
"CODE-14": CheckDef("CODE-14", "TypeScript strict mode enabled", Severity.ADVISORY, "CODE", stack="ts"),
# AI
"AI-01": CheckDef("AI-01", "System prompts not leakable via user input", Severity.CRITICAL, "AI", stack="ai"),
"AI-02": CheckDef("AI-02", "No prompt injection vectors in user inputs", Severity.CRITICAL, "AI", stack="ai"),
"AI-03": CheckDef("AI-03", "LLM API keys not in frontend code", Severity.CRITICAL, "AI", stack="ai"),
"AI-05": CheckDef("AI-05", "AI response output sanitized before rendering", Severity.HIGH, "AI", stack="ai"),
# DEP
"DEP-01": CheckDef("DEP-01", "No git:// or URL-based dependencies", Severity.HIGH, "DEP"),
"DEP-05": CheckDef("DEP-05", "No suspicious postinstall scripts", Severity.HIGH, "DEP", stack="js"),
"DEP-06": CheckDef("DEP-06", "Dependencies pinned (no wildcard *)", Severity.HIGH, "DEP"),
# FE
"FE-01": CheckDef("FE-01", "Meta tags present (title, description, OG)", Severity.ADVISORY, "FE", stack="web"),
"FE-02": CheckDef("FE-02", "Favicon configured", Severity.ADVISORY, "FE", stack="web"),
"FE-03": CheckDef("FE-03", "Custom 404 page exists", Severity.ADVISORY, "FE", stack="web"),
"FE-09": CheckDef("FE-09", "robots.txt present", Severity.ADVISORY, "FE", stack="web"),
# OBS
"OBS-01": CheckDef("OBS-01", "Error monitoring configured (Sentry, etc.)", Severity.ADVISORY, "OBS"),
"OBS-03": CheckDef("OBS-03", "Structured logging with request IDs", Severity.ADVISORY, "OBS"),
}
MANUAL_CHECKS = [
CheckDef("SEC-02", "Every route checks authentication", Severity.CRITICAL, "SEC"),
CheckDef("SEC-03", "HTTPS enforced, HTTP redirected", Severity.CRITICAL, "SEC"),
CheckDef("SEC-10", "Sessions invalidated on logout (server-side)", Severity.HIGH, "SEC"),
CheckDef("DB-01", "Backups configured and tested", Severity.CRITICAL, "DB"),
CheckDef("DB-02", "Backup restore tested (not just backup)", Severity.CRITICAL, "DB"),
CheckDef("DB-04", "Separate dev and production databases", Severity.HIGH, "DB"),
CheckDef("DB-11", "App uses a non-root DB user", Severity.HIGH, "DB"),
CheckDef("DEPLOY-01", "All env vars set on production server", Severity.CRITICAL, "DEPLOY"),
CheckDef("DEPLOY-02", "SSL certificate installed and valid", Severity.CRITICAL, "DEPLOY"),
CheckDef("DEPLOY-05", "Rollback plan exists", Severity.HIGH, "DEPLOY"),
CheckDef("DEPLOY-06", "Staging test passed before production", Severity.HIGH, "DEPLOY"),
CheckDef("AI-07", "Agent permissions scoped (no unrestricted access)", Severity.HIGH, "AI", stack="ai"),
CheckDef("AI-08", "No sensitive data sent to third-party LLMs without consent", Severity.HIGH, "AI", stack="ai"),
CheckDef("FE-04", "Responsive design tested on mobile", Severity.HIGH, "FE", stack="web"),
CheckDef("OBS-05", "Uptime monitoring configured", Severity.HIGH, "OBS"),
]
# ---------------------------------------------------------------------------
# Individual check implementations
# ---------------------------------------------------------------------------
def check_sec01(root, stack):
c = CHECKS["SEC-01"]
dirs = FRONTEND_DIRS & set(os.listdir(root))
patterns = [
r"sk-[a-zA-Z0-9]{20,}",
r"sk-ant-[a-zA-Z0-9-]+",
r"sk-proj-[a-zA-Z0-9-]+",
r"AIza[a-zA-Z0-9_-]{35}",
r"ghp_[a-zA-Z0-9]{36}",
r"glpat-[a-zA-Z0-9_-]{20,}",
r"AKIA[0-9A-Z]{16}",
r"sk_live_[a-zA-Z0-9]{24,}",
r"(api_key|apikey|api_secret|secret_key|auth_token)\s*[:=]\s*['\"][a-zA-Z0-9_\-]{16,}",
]
findings = []
for pat in patterns:
findings += grep_files(root, pat, exts=JS_EXTS | {".env", ".json"},
dirs=dirs if dirs else None, max_findings=5)
if findings:
return Result(c, Status.FAIL,
f"{len(findings)} potential secret(s) found in frontend/client code",
findings[:10])
return Result(c, Status.PASS)
def check_sec04(root, stack):
c = CHECKS["SEC-04"]
findings = grep_files(root, r"(origin\s*:\s*['\"]?\*['\"]?|Access-Control-Allow-Origin.*\*|cors\(\s*\))",
exts=ALL_CODE_EXTS)
if findings:
return Result(c, Status.FAIL, "CORS wildcard (*) detected", findings)
return Result(c, Status.PASS)
def check_sec05(root, stack):
c = CHECKS["SEC-05"]
# Check for state-changing routes
route_findings = grep_files(root, r"(app|router)\.(post|put|patch|delete)\s*\(",
exts=JS_EXTS)
if not route_findings:
return Result(c, Status.SKIP, "No Express-style routes found")
# Check for CSRF protection
csrf_findings = grep_files(root, r"(csrf|csrfToken|_csrf|CSRF_COOKIE|csurf)",
exts=ALL_CODE_EXTS)
if not csrf_findings:
return Result(c, Status.FAIL,
f"{len(route_findings)} state-changing route(s) found but no CSRF protection detected",
route_findings[:5])
return Result(c, Status.PASS)
def check_sec06(root, stack):
c = CHECKS["SEC-06"]
# Check for validation library
val_findings = grep_files(root,
r"(from ['\"]zod['\"]|from ['\"]yup['\"]|from ['\"]joi['\"]|from ['\"]class-validator['\"]|from pydantic|import pydantic)",
exts=ALL_CODE_EXTS)
if val_findings:
return Result(c, Status.PASS)
# Check if there are API routes that use req.body without validation
body_findings = grep_files(root, r"(req\.body|request\.json\(\)|request\.form)",
exts=ALL_CODE_EXTS)
if body_findings:
return Result(c, Status.FAIL,
"request body used without a validation library (zod/yup/joi/pydantic)",
body_findings[:5])
return Result(c, Status.SKIP, "No API route body handling detected")
def check_sec07(root, stack):
c = CHECKS["SEC-07"]
findings = grep_files(root,
r"(express-rate-limit|@upstash/ratelimit|rate-limiter-flexible|slowapi|throttle|rateLimit)",
exts=ALL_CODE_EXTS | {".json"})
if findings:
return Result(c, Status.PASS)
# Only fail if there are auth-related routes
auth_routes = grep_files(root, r"(login|signin|register|signup|forgot.password|reset.password)",
exts=ALL_CODE_EXTS)
if auth_routes:
return Result(c, Status.FAIL,
"Auth routes found but no rate-limiting library detected", auth_routes[:3])
return Result(c, Status.SKIP, "No auth routes detected")
def check_sec08(root, stack):
c = CHECKS["SEC-08"]
# Weak hash for passwords
weak = grep_files(root, r"\b(md5|sha1|sha256)\s*\(",
exts=ALL_CODE_EXTS,
exclude_patterns=[r"//.*\b(md5|sha1|sha256)\b"])
if weak:
return Result(c, Status.FAIL, "Weak hashing algorithm (md5/sha1/sha256) detected", weak)
strong = grep_files(root, r"(bcrypt|argon2|scrypt|pbkdf2)", exts=ALL_CODE_EXTS)
pw_fields = grep_files(root, r"(password|passwd)", exts=ALL_CODE_EXTS)
if pw_fields and not strong:
return Result(c, Status.FAIL, "Password fields found but no bcrypt/argon2/scrypt usage")
return Result(c, Status.PASS if strong or not pw_fields else Status.SKIP)
def check_sec11(root, stack):
c = CHECKS["SEC-11"]
findings = grep_files(root, r"(Content-Security-Policy|contentSecurityPolicy|[^a-z]csp[^a-z])",
exts=ALL_CODE_EXTS | {".json", ".toml", ".yaml", ".yml"})
if findings:
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No Content-Security-Policy configuration found")
def check_sec13(root, stack):
c = CHECKS["SEC-13"]
if not stack.has_node:
return Result(c, Status.SKIP, "Not a JS/TS project")
eval_findings = grep_files(root, r"(\beval\s*\(|new\s+Function\s*\()", exts=JS_EXTS)
dsi_findings = grep_files(root, r"dangerouslySetInnerHTML", exts=JS_EXTS)
# If dangerouslySetInnerHTML is used, check for DOMPurify
unsafe_dsi = []
for f in dsi_findings:
try:
content = open(os.path.join(root, f.file), errors="ignore").read()
if "DOMPurify" not in content and "sanitize" not in content.lower():
unsafe_dsi.append(f)
except Exception:
unsafe_dsi.append(f)
all_findings = eval_findings + unsafe_dsi # noqa: SEC-AUDITOR
if all_findings:
return Result(c, Status.FAIL, "Unsafe eval() or unsanitized dangerouslySetInnerHTML", all_findings) # noqa: SEC-AUDITOR
return Result(c, Status.PASS)
def check_sec14(root, stack):
c = CHECKS["SEC-14"]
url_findings = grep_files(root,
r"(password|token|secret|key|ssn|credit.card)=",
exts=ALL_CODE_EXTS)
log_findings = grep_files(root,
r"console\.(log|info|debug)\s*\(\s*(req|request)\s*\)",
exts=JS_EXTS)
findings = url_findings + log_findings
if findings:
return Result(c, Status.FAIL, "Sensitive data may appear in URLs or logs", findings[:5])
return Result(c, Status.PASS)
def check_sec17(root, stack):
c = CHECKS["SEC-17"]
# Check for .env files that are not .example/.sample
env_files = []
for entry in os.scandir(root):
name = entry.name
if name.startswith(".env") and name not in (".env.example", ".env.sample",
".env.template", ".env.local.example"):
if entry.is_file():
env_files.append(name)
if not env_files:
return Result(c, Status.PASS)
# Check if git-tracked
gitignore_path = os.path.join(root, ".gitignore")
if os.path.isfile(gitignore_path):
content = open(gitignore_path, errors="ignore").read()
if ".env" in content:
return Result(c, Status.PASS)
return Result(c, Status.FAIL,
f".env file(s) exist ({', '.join(env_files)}) and may not be gitignored",
[Finding(f, 0) for f in env_files])
def check_sec18(root, stack):
c = CHECKS["SEC-18"]
gitignore_path = os.path.join(root, ".gitignore")
if not os.path.isfile(gitignore_path):
return Result(c, Status.FAIL, ".gitignore file not found")
content = open(gitignore_path, errors="ignore").read()
if re.search(r"\.env", content):
return Result(c, Status.PASS)
return Result(c, Status.FAIL, ".env not listed in .gitignore")
def check_db03(root, stack):
c = CHECKS["DB-03"]
# Template literal SQL
tl_findings = grep_files(root,
r"(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE).*\$\{",
exts=JS_EXTS)
# Python f-string SQL
py_findings = grep_files(root,
r'f["\'].*\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)\b.*\{',
exts=PY_EXTS)
# String concat SQL
concat_findings = grep_files(root,
r"(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE).*\+\s*(req\.|params\.|body\.|query\.)",
exts=ALL_CODE_EXTS)
all_findings = tl_findings + py_findings + concat_findings
if all_findings:
return Result(c, Status.FAIL,
f"{len(all_findings)} potential SQL injection vector(s)", all_findings[:10])
return Result(c, Status.PASS)
def check_db05(root, stack):
c = CHECKS["DB-05"]
findings = grep_files(root,
r"(pool|connectionLimit|max_connections|poolSize|pooler|6543)",
exts=ALL_CODE_EXTS | {".env", ".env.local", ".env.production"})
if findings:
return Result(c, Status.PASS)
db_found = grep_files(root, r"(pg\.|postgres\.|mysql\.|mongoose\.)", exts=ALL_CODE_EXTS)
if db_found:
return Result(c, Status.FAIL, "Database usage detected but no connection pooling configured")
return Result(c, Status.SKIP, "No direct DB connection detected")
def check_db06(root, stack):
c = CHECKS["DB-06"]
migration_dirs = []
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
depth = dirpath.replace(root, "").count(os.sep)
if depth >= 4:
dirnames[:] = []
continue
for d in dirnames:
if d in ("migrations", "migrate", "versions", "alembic"):
migration_dirs.append(os.path.join(dirpath, d))
if migration_dirs:
return Result(c, Status.PASS)
# Check for database usage
db_found = grep_files(root, r"(prisma|supabase|mongoose|pg\.|sqlite)", exts=ALL_CODE_EXTS)
if db_found:
return Result(c, Status.FAIL, "Database usage found but no migrations directory detected")
return Result(c, Status.SKIP, "No database usage detected")
def check_db07(root, stack):
c = CHECKS["DB-07"]
if not stack.has_supabase:
return Result(c, Status.SKIP, "Not a Supabase project")
sql_findings = grep_files(root, r"CREATE TABLE", exts=SQL_EXTS)
if not sql_findings:
return Result(c, Status.SKIP, "No CREATE TABLE statements found in migrations")
rls_findings = grep_files(root, r"ENABLE ROW LEVEL SECURITY", exts=SQL_EXTS)
if not rls_findings:
return Result(c, Status.FAIL,
f"{len(sql_findings)} table(s) found but no RLS policies detected",
sql_findings[:5])
if len(rls_findings) < len(sql_findings):
return Result(c, Status.FAIL,
f"{len(sql_findings)} table(s) but only {len(rls_findings)} RLS statement(s) — some tables may lack RLS",
sql_findings[:5])
return Result(c, Status.PASS)
def check_db08(root, stack):
c = CHECKS["DB-08"]
if not stack.has_supabase:
return Result(c, Status.SKIP, "Not a Supabase project")
dirs = FRONTEND_DIRS & set(os.listdir(root))
findings = grep_files(root,
r"(service_role|serviceRole|SUPABASE_SERVICE_ROLE)",
exts=JS_EXTS, dirs=dirs if dirs else None)
if findings:
return Result(c, Status.FAIL, "service_role key referenced in client-side code", findings)
return Result(c, Status.PASS)
def check_db12(root, stack):
c = CHECKS["DB-12"]
findings = grep_files(root,
r"(ssn|social_security|credit_card|card_number|passport_number)",
exts=SQL_EXTS | {".prisma"}, flags=re.IGNORECASE)
if findings:
return Result(c, Status.FAIL,
"PII column names found in schema — verify encryption at rest", findings)
return Result(c, Status.PASS)
def check_deploy09(root, stack):
c = CHECKS["DEPLOY-09"]
findings = grep_files(root,
r"(/health|/healthz|/api/health|/status|/readyz)",
exts=ALL_CODE_EXTS)
if findings:
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No health check endpoint found")
def check_deploy10(root, stack):
c = CHECKS["DEPLOY-10"]
# Check for logging libraries
lib_findings = grep_files(root,
r"(winston|pino|bunyan|morgan|log4js|structlog|loguru)",
exts=ALL_CODE_EXTS | {".json"})
if lib_findings:
return Result(c, Status.PASS)
# Count console.log in server/api code
server_dirs = {"api", "server", "backend"}
for d in ("pages/api", "app/api"):
if os.path.isdir(os.path.join(root, d)):
server_dirs.add(d.split("/")[0])
console_findings = grep_files(root, r"console\.(log|debug|info)\(", exts=JS_EXTS)
if console_findings:
return Result(c, Status.FAIL,
f"No structured logger found; {len(console_findings)} console.log(s) in code",
console_findings[:5])
return Result(c, Status.SKIP, "No server-side code detected")
def check_code01(root, stack):
c = CHECKS["CODE-01"]
if not stack.has_node:
return Result(c, Status.SKIP, "Not a JS/TS project")
findings = grep_files(root, r"console\.(log|debug|info)\(",
exts=JS_EXTS,
dirs=FRONTEND_DIRS & set(os.listdir(root)) or None,
exclude_patterns=[r"//.*console\.(log|debug|info)\("])
if findings:
return Result(c, Status.FAIL, f"{len(findings)} console.log statement(s) in production code", findings[:10])
return Result(c, Status.PASS)
def check_code03(root, stack):
c = CHECKS["CODE-03"]
findings = grep_files(root,
r"catch\s*\([^)]*\)\s*\{\s*\}",
exts=ALL_CODE_EXTS)
if findings:
return Result(c, Status.FAIL, f"{len(findings)} empty catch block(s)", findings)
return Result(c, Status.PASS)
def check_code07(root, stack):
c = CHECKS["CODE-07"]
findings = grep_files(root,
r"(TODO|FIXME|HACK|XXX).{0,20}(auth|security|permission|validation|sanitiz)",
exts=ALL_CODE_EXTS, flags=re.IGNORECASE)
if findings:
return Result(c, Status.FAIL, f"{len(findings)} deferred security TODO(s)", findings)
return Result(c, Status.PASS)
def check_code09(root, stack):
c = CHECKS["CODE-09"]
if not stack.has_react:
return Result(c, Status.SKIP, "Not a React project")
# Next.js App Router: error.tsx
error_page = file_exists_in(root, "error.tsx", "error.jsx", "global-error.tsx")
if error_page:
return Result(c, Status.PASS)
# Class-based error boundary
eb_findings = grep_files(root,
r"(ErrorBoundary|componentDidCatch|getDerivedStateFromError)",
exts=JS_EXTS)
if eb_findings:
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No React error boundary or error.tsx found")
def check_code10(root, stack):
c = CHECKS["CODE-10"]
findings = grep_files(root,
r"(error\.stack|\.stack\s*\)|err\.message.*res\.(json|send)|traceback\.format_exc)",
exts=ALL_CODE_EXTS)
if findings:
return Result(c, Status.FAIL, "Potential stack trace leak in error responses", findings)
return Result(c, Status.PASS)
def check_code11(root, stack):
c = CHECKS["CODE-11"]
if not stack.has_node:
return Result(c, Status.SKIP, "Not a JS/TS project")
findings = grep_files(root,
r"eslint-disable.*(no-eval|no-implied-eval|no-script-url|security)",
exts=JS_EXTS)
if findings:
return Result(c, Status.FAIL, "Security lint rule(s) disabled", findings)
return Result(c, Status.PASS)
def check_code12(root, stack):
c = CHECKS["CODE-12"]
lockfiles = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb",
"Pipfile.lock", "poetry.lock", "Gemfile.lock", "go.sum", "Cargo.lock"]
for lf in lockfiles:
if os.path.isfile(os.path.join(root, lf)):
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No lockfile found — dependencies are not pinned")
def check_code13(root, stack):
c = CHECKS["CODE-13"]
if not stack.has_node:
return Result(c, Status.SKIP, "Not a JS/TS project")
pkg_path = os.path.join(root, "package.json")
if not os.path.isfile(pkg_path):
return Result(c, Status.SKIP)
findings = grep_files(root, r'"[^"]+"\s*:\s*"\*"', exts={".json"})
findings += grep_files(root, r'"[^"]+"\s*:\s*"latest"', exts={".json"})
findings = [f for f in findings if "package.json" in f.file and "node_modules" not in f.file]
if findings:
return Result(c, Status.FAIL, "Wildcard (*) or 'latest' version found in package.json", findings)
return Result(c, Status.PASS)
def check_code14(root, stack):
c = CHECKS["CODE-14"]
if not stack.has_typescript:
return Result(c, Status.SKIP, "Not a TypeScript project")
tsconfig_path = os.path.join(root, "tsconfig.json")
if not os.path.isfile(tsconfig_path):
return Result(c, Status.SKIP, "tsconfig.json not found")
content = open(tsconfig_path, errors="ignore").read()
if re.search(r'"strict"\s*:\s*true', content):
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "TypeScript strict mode not enabled in tsconfig.json",
[Finding("tsconfig.json", 0)])
def check_ai01(root, stack):
c = CHECKS["AI-01"]
if not stack.has_ai:
return Result(c, Status.SKIP, "No AI/LLM usage detected")
dirs = FRONTEND_DIRS & set(os.listdir(root))
findings = grep_files(root,
r"(system.?prompt|system.?message|system_instruction)",
exts=ALL_CODE_EXTS, dirs=dirs if dirs else None, flags=re.IGNORECASE)
if findings:
return Result(c, Status.FAIL,
"System prompt referenced in client-accessible code — may be leakable",
findings)
return Result(c, Status.PASS)
def check_ai02(root, stack):
c = CHECKS["AI-02"]
if not stack.has_ai:
return Result(c, Status.SKIP, "No AI/LLM usage detected")
findings = grep_files(root,
r"(messages\.push|content\s*:.*\$\{|content\s*:.*\+\s*user|prompt.*\+)",
exts=ALL_CODE_EXTS)
if findings:
return Result(c, Status.FAIL,
"User input may be concatenated directly into AI prompt", findings[:5])
return Result(c, Status.PASS)
def check_ai03(root, stack):
c = CHECKS["AI-03"]
if not stack.has_ai:
return Result(c, Status.SKIP, "No AI/LLM usage detected")
dirs = FRONTEND_DIRS & set(os.listdir(root))
findings = grep_files(root,
r"(OPENAI_API_KEY|ANTHROPIC_API_KEY|GOOGLE_AI_API_KEY|sk-ant-|sk-proj-)",
exts=JS_EXTS, dirs=dirs if dirs else None)
if findings:
return Result(c, Status.FAIL, "LLM API key referenced in frontend code", findings)
return Result(c, Status.PASS)
def check_ai05(root, stack):
c = CHECKS["AI-05"]
if not stack.has_ai:
return Result(c, Status.SKIP, "No AI/LLM usage detected")
findings = grep_files(root,
r"dangerouslySetInnerHTML.*\b(response|result|completion|message|content)\b",
exts=JS_EXTS)
if findings:
return Result(c, Status.FAIL, "AI output rendered via dangerouslySetInnerHTML", findings)
return Result(c, Status.PASS)
def check_dep01(root, stack):
c = CHECKS["DEP-01"]
if not stack.has_node:
return Result(c, Status.SKIP, "Not a Node.js project")
findings = grep_files(root,
r'"[^"]+"\s*:\s*"(git://|git\+|github:|https://github\.com|file:)',
exts={".json"})
findings = [f for f in findings if "package.json" in f.file and "node_modules" not in f.file]
if findings:
return Result(c, Status.FAIL, "Git/URL-based dependency found in package.json", findings)
return Result(c, Status.PASS)
def check_dep05(root, stack):
c = CHECKS["DEP-05"]
if not stack.has_node:
return Result(c, Status.SKIP, "Not a Node.js project")
pkg_path = os.path.join(root, "package.json")
if not os.path.isfile(pkg_path):
return Result(c, Status.SKIP)
pkg = read_json_file(pkg_path)
scripts = pkg.get("scripts", {})
suspicious = []
for key in ("preinstall", "postinstall", "install"):
val = scripts.get(key, "")
if val and any(kw in val for kw in ("curl", "wget", "fetch", "exec", "eval", "sh ", "bash ")):
suspicious.append(Finding("package.json", 0, f'"{key}": "{val}"'))
if suspicious:
return Result(c, Status.FAIL, "Suspicious install script detected in package.json", suspicious)
return Result(c, Status.PASS)
def check_dep06(root, stack):
c = CHECKS["DEP-06"]
if not stack.has_node:
return Result(c, Status.SKIP, "Not a Node.js project")
pkg_path = os.path.join(root, "package.json")
if not os.path.isfile(pkg_path):
return Result(c, Status.SKIP)
findings = grep_files(root, r'"\*"', exts={".json"})
findings = [f for f in findings if "package.json" in f.file and "node_modules" not in f.file]
if findings:
return Result(c, Status.FAIL, "Wildcard (*) version found", findings)
return Result(c, Status.PASS)
def check_fe01(root, stack):
c = CHECKS["FE-01"]
if not stack.is_web and not stack.has_node:
return Result(c, Status.SKIP, "Not a web project")
# Next.js metadata export
meta_findings = grep_files(root,
r"(export\s+(const|async\s+function)\s+metadata|generateMetadata|<title>|og:title|og:description)",
exts=JS_EXTS | {".html"})
if meta_findings:
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No meta tags or Next.js metadata export found")
def check_fe02(root, stack):
c = CHECKS["FE-02"]
if not stack.is_web and not stack.has_node:
return Result(c, Status.SKIP, "Not a web project")
favicon = file_exists_in(root, "favicon.ico", "favicon.png", "favicon.svg",
"favicon.webp", "icon.png", "icon.ico")
if favicon:
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No favicon file found")
def check_fe03(root, stack):
c = CHECKS["FE-03"]
if not stack.is_web and not stack.has_node:
return Result(c, Status.SKIP, "Not a web project")
page_404 = file_exists_in(root, "404.tsx", "404.jsx", "404.html",
"not-found.tsx", "not-found.jsx")
if page_404:
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No custom 404 or not-found page found")
def check_fe09(root, stack):
c = CHECKS["FE-09"]
if not stack.is_web and not stack.has_node:
return Result(c, Status.SKIP, "Not a web project")
public_robots = os.path.join(root, "public", "robots.txt")
root_robots = os.path.join(root, "robots.txt")
if os.path.isfile(public_robots) or os.path.isfile(root_robots):
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No robots.txt found")
def check_obs01(root, stack):
c = CHECKS["OBS-01"]
findings = grep_files(root,
r"(@sentry/|sentry-|LogRocket|Bugsnag|datadogRum|Rollbar|Honeybadger|newrelic)",
exts=ALL_CODE_EXTS | {".json"})
if findings:
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No error monitoring library detected")
def check_obs03(root, stack):
c = CHECKS["OBS-03"]
findings = grep_files(root,
r"(winston|pino|bunyan|structlog|loguru|import logging)",
exts=ALL_CODE_EXTS | {".json"})
if findings:
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No structured logging library detected")
CATEGORY_CHECKS = {
"SEC": [check_sec01, check_sec04, check_sec05, check_sec06, check_sec07,
check_sec08, check_sec11, check_sec13, check_sec14, check_sec17, check_sec18],
"DB": [check_db03, check_db05, check_db06, check_db07, check_db08, check_db12],
"DEPLOY": [check_deploy09, check_deploy10],
"CODE": [check_code01, check_code03, check_code07, check_code09, check_code10,
check_code11, check_code12, check_code13, check_code14],
"AI": [check_ai01, check_ai02, check_ai03, check_ai05],
"DEP": [check_dep01, check_dep05, check_dep06],
"FE": [check_fe01, check_fe02, check_fe03, check_fe09],
"OBS": [check_obs01, check_obs03],
}
CATEGORY_ORDER = ["SEC", "DB", "CODE", "DEP", "AI", "DEPLOY", "FE", "OBS"]
# ---------------------------------------------------------------------------
# Manual check runner
# ---------------------------------------------------------------------------
def run_manual_checks(stack: Stack, interactive: bool, category_filter: Optional[str]) -> List[Result]:
results = []
applicable = []
for chk in MANUAL_CHECKS:
if category_filter and chk.category != category_filter.upper():
continue
if chk.stack == "ai" and not stack.has_ai:
results.append(Result(chk, Status.SKIP, "No AI/LLM usage detected"))
continue
if chk.stack == "web" and not stack.is_web:
results.append(Result(chk, Status.SKIP, "Not a web project"))
continue
if chk.stack == "vps" and stack.deploy_target not in ("docker", "vps", ""):
results.append(Result(chk, Status.SKIP, "Not a VPS/Docker deployment"))
continue
applicable.append(chk)
if not interactive or not applicable:
for chk in applicable:
results.append(Result(chk, Status.MANUAL, "Not confirmed (run without --no-interactive to answer)"))
return results
print()
print(bold("Manual Checks") + " — answer Y/N for each:")
print()
for chk in applicable:
sev_label = {
Severity.CRITICAL: red("CRITICAL"),
Severity.HIGH: yellow("HIGH"),
Severity.ADVISORY: dim("ADVISORY"),
}[chk.severity]
while True:
try:
answer = input(f" [{sev_label}] [{chk.id}] {chk.description} [y/N]: ").strip().lower()
except (EOFError, KeyboardInterrupt):
answer = "n"
if answer in ("y", "yes"):
results.append(Result(chk, Status.PASS))
break
elif answer in ("n", "no", ""):
results.append(Result(chk, Status.FAIL, "Not confirmed"))
break
print(" Please enter Y or N.")
return results
# ---------------------------------------------------------------------------
# Verdict / output
# ---------------------------------------------------------------------------
def severity_for_result(r: Result) -> Severity:
return r.check.severity
def print_report(all_results: List[Result], stack: Stack, scan_time: float,
verbose: bool) -> int:
critical = [r for r in all_results if r.status in (Status.FAIL, Status.MANUAL)
and r.check.severity == Severity.CRITICAL]
high = [r for r in all_results if r.status in (Status.FAIL, Status.MANUAL)
and r.check.severity == Severity.HIGH]
advisory = [r for r in all_results if r.status in (Status.FAIL, Status.MANUAL)
and r.check.severity == Severity.ADVISORY]
stack_desc = []
if stack.framework: stack_desc.append(stack.framework.capitalize())
if stack.has_supabase: stack_desc.append("Supabase")
if stack.deploy_target: stack_desc.append(stack.deploy_target.capitalize())
if stack.has_python and stack.py_framework: stack_desc.append(stack.py_framework.capitalize())
if not stack_desc: stack_desc.append("Unknown")
stack_str = " + ".join(stack_desc)
print()
print(bold("SHIP GATE REPORT"))
print("=" * 48)
print(f"Stack: {stack_str}")
print(f"Scan time: {scan_time:.1f}s")
print(f"Checks: {len(all_results)} total")
print()
def _section(label, items, color_fn):
if not items and not verbose:
return
print(bold(f"{label} ({len(items)} item{'s' if len(items) != 1 else ''})"))
for r in items:
status_str = {
Status.FAIL: red("FAIL "),
Status.MANUAL: yellow("MANUAL"),
Status.PASS: green("PASS "),
Status.SKIP: dim("SKIP "),
}[r.status]
print(f" {status_str} [{r.check.id}] {r.check.description}")
if r.message:
print(f" {dim(r.message)}")
for f in r.findings[:3]:
print(f" {dim(f.file)}:{f.line} {dim(f.snippet[:80])}")
print()
if critical:
_section(red("CRITICAL") + " (must fix before shipping)", critical, red)
if high:
_section(yellow("HIGH") + " (should fix before shipping)", high, yellow)
if advisory:
_section(dim("ADVISORY") + " (recommended)", advisory, dim)
if verbose:
passed = [r for r in all_results if r.status == Status.PASS]
if passed:
_section(green("PASS"), passed, green)
skipped = [r for r in all_results if r.status == Status.SKIP]
if skipped:
_section(dim("SKIP"), skipped, dim)
if critical:
print(red(bold(f"VERDICT: DO NOT SHIP ({len(critical)} critical issue{'s' if len(critical) != 1 else ''})")))
print("Fix critical items and re-run.")
return 1
elif high:
print(yellow(bold(f"VERDICT: SHIP WITH CAUTION ({len(high)} high issue{'s' if len(high) != 1 else ''})")))
print("Acknowledge risks and proceed only if you accept them.")
return 2
else:
print(green(bold("VERDICT: CLEAR TO SHIP")))
return 0
def print_json_report(all_results: List[Result], stack: Stack, scan_time: float) -> int:
critical = [r for r in all_results if r.status in (Status.FAIL, Status.MANUAL)
and r.check.severity == Severity.CRITICAL]
high = [r for r in all_results if r.status in (Status.FAIL, Status.MANUAL)
and r.check.severity == Severity.HIGH]
output = {
"version": VERSION,
"scan_time": round(scan_time, 2),
"stack": {
"framework": stack.framework,
"has_supabase": stack.has_supabase,
"has_typescript": stack.has_typescript,
"deploy_target": stack.deploy_target,
"has_ai": stack.has_ai,
},
"results": [
{
"id": r.check.id,
"description": r.check.description,
"severity": r.check.severity.value,
"category": r.check.category,
"status": r.status.value,
"message": r.message,
"findings": [
{"file": f.file, "line": f.line, "snippet": f.snippet}
for f in r.findings
],
}
for r in all_results
],
"summary": {
"critical": len(critical),
"high": len(high),
"verdict": "DO_NOT_SHIP" if critical else ("SHIP_WITH_CAUTION" if high else "CLEAR_TO_SHIP"),
},
}
print(json.dumps(output, indent=2))
return 1 if critical else (2 if high else 0)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
global USE_COLOR
parser = argparse.ArgumentParser(
description="Ship Gate — pre-production audit scanner",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("path", nargs="?", default=".",
help="Project root directory (default: current directory)")
parser.add_argument("--json", action="store_true", help="Output as JSON")
parser.add_argument("--no-color", action="store_true", help="Disable color output")
parser.add_argument("--no-interactive", action="store_true",
help="Skip manual confirmation prompts")
parser.add_argument("--category", metavar="CAT",
help="Only run one category: SEC, DB, CODE, DEP, AI, DEPLOY, FE, OBS")
parser.add_argument("--verbose", action="store_true",
help="Show PASS and SKIP results in addition to failures")
parser.add_argument("--version", action="version", version=f"ship-gate {VERSION}")
args = parser.parse_args()
if args.no_color or not sys.stdout.isatty():
USE_COLOR = False
root = os.path.abspath(args.path)
if not os.path.isdir(root):
print(f"Error: '{root}' is not a directory", file=sys.stderr)
sys.exit(1)
start = time.time()
# Detect stack
stack = detect_stack(root)
if not args.json:
print(bold("Detecting stack..."), end=" ", flush=True)
parts = []
if stack.framework: parts.append(stack.framework.capitalize())
if stack.has_supabase: parts.append("Supabase")
if stack.deploy_target: parts.append(stack.deploy_target.capitalize())
if stack.has_python and stack.py_framework: parts.append(stack.py_framework.capitalize())
if stack.has_ai: parts.append(f"AI({','.join(stack.ai_providers)})")
print(", ".join(parts) if parts else "generic project")
# Run automated checks
all_results: List[Result] = []
categories = [args.category.upper()] if args.category else CATEGORY_ORDER
for i, cat in enumerate(categories, 1):
fns = CATEGORY_CHECKS.get(cat, [])
cat_results = []
for fn in fns:
try:
r = fn(root, stack)
except Exception as e:
chk_id = fn.__name__.replace("check_", "").replace("_", "-").upper()
cat_results.append(Result(
CheckDef(chk_id, fn.__doc__ or fn.__name__, Severity.ADVISORY, cat),
Status.SKIP, f"Scanner error: {e}",
))
continue
cat_results.append(r)
all_results.extend(cat_results)
if not args.json:
n_fail = sum(1 for r in cat_results if r.status == Status.FAIL)
n_pass = sum(1 for r in cat_results if r.status == Status.PASS)
n_skip = sum(1 for r in cat_results if r.status == Status.SKIP)
label = red(f"{n_fail} FAIL") if n_fail else green("0 FAIL")
print(f" [{i}/{len(categories)}] {cat}: {label}, {n_pass} PASS, {dim(str(n_skip) + ' SKIP')}")
# Manual checks
manual_results = run_manual_checks(stack, not args.no_interactive, args.category)
all_results.extend(manual_results)
scan_time = time.time() - start
if args.json:
sys.exit(print_json_report(all_results, stack, scan_time))
else:
sys.exit(print_report(all_results, stack, scan_time, args.verbose))
if __name__ == "__main__":
main()
Đánh giá mức sẵn sàng SOC 2 Type II qua 6 câu hỏi buộc phải trả lời, tập trung vào giai đoạn quan sát.
--- name: "soc2-audit-prep" description: "/cs:soc2-audit-prep <scope> — SOC 2 Type II readiness 6-question forcing interrogation. Observation-period focused. Use before Type II observation begins, mid-period checkpoint, or pre-field-test month-10 readiness." --- # /cs:soc2-audit-prep — SOC 2 Type II Forcing Questions **Command:** `/cs:soc2-audit-prep <scope>` The SOC 2 Type II auditor pressure-tests any SOC 2 work. Six observation-period-disciplined questions before any Type II cycle. ## When to Run - Pre-observation period (months 1-2 of cycle) - Mid-observation period (month 6 checkpoint) - Pre-field-test (month 10) - Post-report (planning next cycle) - After scope change (adding TSC category) - After major incident during observation period ## The Six SOC 2 Type II Questions ### 1. What's the scope, and which TSC categories are in? **Security always required; others elective based on customer ask.** - Common Criteria (CC1-CC9) under Security always - Availability (A1): for SaaS with SLA commitments - Processing Integrity (PI1): for systems processing transactional / financial data - Confidentiality (C1): for systems handling proprietary / confidential data - Privacy (P1-P8): for systems handling personal data (overlap with GDPR if applicable) - AICPA AT-C 205 description of system: complete + accurate + boundaries clear ### 2. Did any control skip a cycle during observation period? **Type II requires consistent operation — single skipped cycle = likely exception.** - Quarterly controls (e.g., access reviews): all 4 quarters covered - Monthly controls (e.g., vulnerability scans): all months covered - Continuous controls (e.g., logging): no gaps during period - Annual controls (e.g., BCP exercises, training): completed within period ### 3. Show me the change-management evidence for any control implemented mid-period. **Mid-period changes = high audit risk.** - New controls implemented during observation: documented with change-management - Modified controls: rationale + effective date + impact on prior samples - Removed controls: rationale + customer impact assessment - Strategy: avoid mid-period changes; defer to next cycle ### 4. Where's the exception log, and what's the materiality assessment? **Real-time exception logging — not retroactive.** - Each exception logged when discovered, not at audit time - Per exception: what / when / impact / remediation / owner - Materiality assessment: does the exception affect overall control operation? - Audit firm threshold: typically 1-2 exceptions per control acceptable; 3+ = finding ### 5. Show me sample evidence from each TSC criterion in the FIRST month of observation. **Not the last week — the first month.** - Audit firm samples across the observation period - Front-loaded evidence demonstrates operational discipline - Back-loaded evidence (last 30 days) = "scrambling" signal - Sample IDs should be reproducible from operational systems ### 6. What's the cross-walk to ISO 27001, and which evidence reuses? **75% control overlap — the canonical pair.** - Run `cross_framework_mapper.py` for HIGH-confidence overlap themes - Each shared artefact cited by both audits (one collection, two reports) - Coordinate audit calendar with cs-ciso-iso27001 - Avoid producing duplicate evidence files for same control ## Workflow ```bash # 1. Scoping + gap analysis (pre-observation) python ../../ra-qm-team/skills/soc2-compliance/scripts/gap_analyzer.py current_state.json # 2. Control matrix with ISO 27001 cross-walk python ../../ra-qm-team/skills/soc2-compliance/scripts/control_matrix_builder.py program.json # 3. Continuous evidence tracking (during observation) python ../../ra-qm-team/skills/soc2-compliance/scripts/evidence_tracker.py evidence_log.json # 4. Mock audit (pre-field-test month 10) python ../../skills/compliance-os/scripts/audit_simulator.py soc2_scope.json ``` ## Output Format ```markdown # SOC 2 Type II Audit Prep: <scope> **Date:** YYYY-MM-DD **Observation Period:** YYYY-MM-DD to YYYY-MM-DD ## The Decision Being Made [scoping | pre-observation | observation-status | pre-field | report-response] ## TSC Scope - Security: included - Availability: <yes/no> - Processing Integrity: <yes/no> - Confidentiality: <yes/no> - Privacy: <yes/no> ## Observation Period Status - Months elapsed: N / 12 - Controls operated consistently: % of total - Cycle skips identified: <list> - Mid-period control changes: N (each documented with change-mgmt: yes/no) ## Exception Log - Total exceptions logged: N - Per-control max exceptions: M (audit firm tolerance: typically 1-2) - Material exceptions (overall control affected): <list> - Remediation status per exception: complete/in-progress ## Sample Evidence Coverage - Month 1-3 evidence: complete/gaps - Month 4-6 evidence: complete/gaps - Month 7-9 evidence: complete/gaps - Month 10-12 evidence: complete/gaps (only for pre-report status) ## ISO 27001 Cross-Walk Reuse - HIGH-confidence overlap themes: N - Shared artefacts in evidence pool: <count> - Duplicate evidence collection avoided: % savings ## Audit Firm Readiness - Scoping discussion: complete/pending - Description of system per AT-C 205: complete/pending - Walkthrough rehearsal: complete/pending - Sample preparation: complete/pending ## Verdict 🟢 ON-TRACK | 🟡 NEEDS-ATTENTION | 🔴 MATERIAL-RISK ## Top 3 Actions [3 concrete next steps with owner + observation-period timing] ``` ## Routing - `/cs:compliance-readiness` — for multi-framework view - `/cs:iso27001-audit-prep` — for ISO 27001 cross-walk pair (75% overlap) - `/cs:gdpr-audit-prep` — for Privacy TSC overlap - `/cs:ciso-review` — for executive cybersecurity strategy ## Related - Agent: [`cs-soc2-auditor`](../../agents/cs-soc2-auditor.md) - Skill: [`soc2-compliance`](../../../ra-qm-team/skills/soc2-compliance/SKILL.md) - Playbook: [soc2_audit_playbook.md](../../../ra-qm-team/skills/soc2-compliance/references/soc2_audit_playbook.md) - Adjacent: `../iso27001-audit-prep/`, `../gdpr-audit-prep/`, `../compliance-readiness/` --- **Version:** 1.0.0
Skill mẫu dùng để tham khảo cấu trúc khi tạo skill mới.
# Sample Text Processor
---
**Name**: sample-text-processor
**Tier**: BASIC
**Category**: Text Processing
**Dependencies**: None (Python Standard Library Only)
**Author**: Claude Skills Engineering Team
**Version**: 1.0.0
**Last Updated**: 2026-02-16
---
## Description
The Sample Text Processor is a simple skill designed to demonstrate the basic structure and functionality expected in the claude-skills ecosystem. This skill provides fundamental text processing capabilities including word counting, character analysis, and basic text transformations.
This skill serves as a reference implementation for BASIC tier requirements and can be used as a template for creating new skills. It demonstrates proper file structure, documentation standards, and implementation patterns that align with ecosystem best practices.
The skill processes text files and provides statistics and transformations in both human-readable and JSON formats, showcasing the dual output requirement for skills in the claude-skills repository.
## Features
### Core Functionality
- **Word Count Analysis**: Count total words, unique words, and word frequency
- **Character Statistics**: Analyze character count, line count, and special characters
- **Text Transformations**: Convert text to uppercase, lowercase, or title case
- **File Processing**: Process single text files or batch process directories
- **Dual Output Formats**: Generate results in both JSON and human-readable formats
### Technical Features
- Command-line interface with comprehensive argument parsing
- Error handling for common file and processing issues
- Progress reporting for batch operations
- Configurable output formatting and verbosity levels
- Cross-platform compatibility with standard library only dependencies
## Usage
### Basic Text Analysis
```bash
python text_processor.py analyze document.txt
python text_processor.py analyze document.txt --output results.json
```
### Text Transformation
```bash
python text_processor.py transform document.txt --mode uppercase
python text_processor.py transform document.txt --mode title --output transformed.txt
```
### Batch Processing
```bash
python text_processor.py batch text_files/ --output results/
python text_processor.py batch text_files/ --format json --output batch_results.json
```
## Examples
### Example 1: Basic Word Count
```bash
$ python text_processor.py analyze sample.txt
=== TEXT ANALYSIS RESULTS ===
File: sample.txt
Total words: 150
Unique words: 85
Total characters: 750
Lines: 12
Most frequent word: "the" (8 occurrences)
```
### Example 2: JSON Output
```bash
$ python text_processor.py analyze sample.txt --format json
{
"file": "sample.txt",
"statistics": {
"total_words": 150,
"unique_words": 85,
"total_characters": 750,
"lines": 12,
"most_frequent": {
"word": "the",
"count": 8
}
}
}
```
### Example 3: Text Transformation
```bash
$ python text_processor.py transform sample.txt --mode title
Original: "hello world from the text processor"
Transformed: "Hello World From The Text Processor"
```
## Installation
This skill requires only Python 3.7 or later with the standard library. No external dependencies are required.
1. Clone or download the skill directory
2. Navigate to the scripts directory
3. Run the text processor directly with Python
```bash
cd scripts/
python text_processor.py --help
```
## Configuration
The text processor supports various configuration options through command-line arguments:
- `--format`: Output format (json, text)
- `--verbose`: Enable verbose output and progress reporting
- `--output`: Specify output file or directory
- `--encoding`: Specify text file encoding (default: utf-8)
## Architecture
The skill follows a simple modular architecture:
- **TextProcessor Class**: Core processing logic and statistics calculation
- **OutputFormatter Class**: Handles dual output format generation
- **FileManager Class**: Manages file I/O operations and batch processing
- **CLI Interface**: Command-line argument parsing and user interaction
## Error Handling
The skill includes comprehensive error handling for:
- File not found or permission errors
- Invalid encoding or corrupted text files
- Memory limitations for very large files
- Output directory creation and write permissions
- Invalid command-line arguments and parameters
## Performance Considerations
- Efficient memory usage for large text files through streaming
- Optimized word counting using dictionary lookups
- Batch processing with progress reporting for large datasets
- Configurable encoding detection for international text
## Contributing
This skill serves as a reference implementation and contributions are welcome to demonstrate best practices:
1. Follow PEP 8 coding standards
2. Include comprehensive docstrings
3. Add test cases with sample data
4. Update documentation for any new features
5. Ensure backward compatibility
## Limitations
As a BASIC tier skill, some advanced features are intentionally omitted:
- Complex text analysis (sentiment, language detection)
- Advanced file format support (PDF, Word documents)
- Database integration or external API calls
- Parallel processing for very large datasets
This skill demonstrates the essential structure and quality standards required for BASIC tier skills in the claude-skills ecosystem while remaining simple and focused on core functionality.
FILE:assets/sample_text.txt
This is a sample text file for testing the text processor skill.
It contains multiple lines of text with various words and punctuation.
The quick brown fox jumps over the lazy dog.
This sentence contains all 26 letters of the English alphabet.
Some additional content:
- Numbers: 123, 456, 789
- Special characters: !@#$%^&*()
- Mixed case: CamelCase, snake_case, PascalCase
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco.
This file serves as a basic test case for:
1. Word counting functionality
2. Character analysis
3. Line counting
4. Text transformations
5. Statistical analysis
The text processor should handle this content correctly and produce
meaningful statistics and transformations for testing purposes.
FILE:assets/test_data.csv
name,age,city,country
John Doe,25,New York,USA
Jane Smith,30,London,UK
Bob Johnson,22,Toronto,Canada
Alice Brown,28,Sydney,Australia
Charlie Wilson,35,Berlin,Germany
This CSV file contains sample data with headers and multiple rows.
It can be used to test the text processor's ability to handle
structured data formats and count words across different content types.
The file includes:
- Header row with column names
- Data rows with mixed text and numbers
- Various city and country names
- Different age values for statistical analysis
FILE:expected_outputs/sample_text_analysis.json
{
"file": "assets/sample_text.txt",
"file_size": 855,
"total_words": 116,
"unique_words": 87,
"total_characters": 855,
"lines": 19,
"average_word_length": 4.7,
"most_frequent": {
"word": "the",
"count": 5
}
}
FILE:README.md
# Sample Text Processor
A basic text processing skill that demonstrates BASIC tier requirements for the claude-skills ecosystem.
## Quick Start
```bash
# Analyze a text file
python scripts/text_processor.py analyze sample.txt
# Get JSON output
python scripts/text_processor.py analyze sample.txt --format json
# Transform text to uppercase
python scripts/text_processor.py transform sample.txt --mode upper
# Process multiple files
python scripts/text_processor.py batch text_files/ --verbose
```
## Features
- Word count and text statistics
- Text transformations (upper, lower, title, reverse)
- Batch file processing
- JSON and human-readable output formats
- Comprehensive error handling
## Requirements
- Python 3.7 or later
- No external dependencies (standard library only)
## Usage
See [SKILL.md](SKILL.md) for comprehensive documentation and examples.
## Testing
Sample data files are provided in the `assets/` directory for testing the functionality.
FILE:references/api-reference.md
# Text Processor API Reference
## Classes
### TextProcessor
Main class for text processing operations.
#### `__init__(self, encoding: str = 'utf-8')`
Initialize the text processor with specified encoding.
**Parameters:**
- `encoding` (str): Character encoding for file operations. Default: 'utf-8'
#### `analyze_text(self, text: str) -> Dict[str, Any]`
Analyze text and return comprehensive statistics.
**Parameters:**
- `text` (str): Text content to analyze
**Returns:**
- `dict`: Statistics including word count, character count, lines, most frequent word
**Example:**
```python
processor = TextProcessor()
stats = processor.analyze_text("Hello world")
# Returns: {'total_words': 2, 'unique_words': 2, ...}
```
#### `transform_text(self, text: str, mode: str) -> str`
Transform text according to specified mode.
**Parameters:**
- `text` (str): Text to transform
- `mode` (str): Transformation mode ('upper', 'lower', 'title', 'reverse')
**Returns:**
- `str`: Transformed text
**Raises:**
- `ValueError`: If mode is not supported
### OutputFormatter
Static methods for output formatting.
#### `format_json(data: Dict[str, Any]) -> str`
Format data as JSON string.
#### `format_human_readable(data: Dict[str, Any]) -> str`
Format data as human-readable text.
### FileManager
Handles file operations and batch processing.
#### `find_text_files(self, directory: str) -> List[str]`
Find all text files in a directory recursively.
**Supported Extensions:**
- .txt
- .md
- .rst
- .csv
- .log
## Command Line Interface
### Commands
#### `analyze`
Analyze text file statistics.
```bash
python text_processor.py analyze <file> [options]
```
#### `transform`
Transform text file content.
```bash
python text_processor.py transform <file> --mode <mode> [options]
```
#### `batch`
Process multiple files in a directory.
```bash
python text_processor.py batch <directory> [options]
```
### Global Options
- `--format {json,text}`: Output format (default: text)
- `--output FILE`: Output file path (default: stdout)
- `--encoding ENCODING`: Text file encoding (default: utf-8)
- `--verbose`: Enable verbose output
## Error Handling
The text processor handles several error conditions:
- **FileNotFoundError**: When input file doesn't exist
- **UnicodeDecodeError**: When file encoding doesn't match specified encoding
- **PermissionError**: When file access is denied
- **ValueError**: When invalid transformation mode is specified
All errors are reported to stderr with descriptive messages.
FILE:scripts/text_processor.py
#!/usr/bin/env python3
"""
Sample Text Processor - Basic text analysis and transformation tool
This script demonstrates the basic structure and functionality expected in
BASIC tier skills. It provides text processing capabilities with proper
argument parsing, error handling, and dual output formats.
Usage:
python text_processor.py analyze <file> [options]
python text_processor.py transform <file> --mode <mode> [options]
python text_processor.py batch <directory> [options]
Author: Claude Skills Engineering Team
Version: 1.0.0
Dependencies: Python Standard Library Only
"""
import argparse
import json
import os
import sys
from collections import Counter
from pathlib import Path
from typing import Dict, List, Any, Optional
class TextProcessor:
"""Core text processing functionality"""
def __init__(self, encoding: str = 'utf-8'):
self.encoding = encoding
def analyze_text(self, text: str) -> Dict[str, Any]:
"""Analyze text and return statistics"""
lines = text.split('\n')
words = text.lower().split()
# Calculate basic statistics
stats = {
'total_words': len(words),
'unique_words': len(set(words)),
'total_characters': len(text),
'lines': len(lines),
'average_word_length': sum(len(word) for word in words) / len(words) if words else 0
}
# Find most frequent word
if words:
word_counts = Counter(words)
most_common = word_counts.most_common(1)[0]
stats['most_frequent'] = {
'word': most_common[0],
'count': most_common[1]
}
else:
stats['most_frequent'] = {'word': '', 'count': 0}
return stats
def transform_text(self, text: str, mode: str) -> str:
"""Transform text according to specified mode"""
if mode == 'upper':
return text.upper()
elif mode == 'lower':
return text.lower()
elif mode == 'title':
return text.title()
elif mode == 'reverse':
return text[::-1]
else:
raise ValueError(f"Unknown transformation mode: {mode}")
def process_file(self, file_path: str) -> Dict[str, Any]:
"""Process a single text file"""
try:
with open(file_path, 'r', encoding=self.encoding) as file:
content = file.read()
stats = self.analyze_text(content)
stats['file'] = file_path
stats['file_size'] = os.path.getsize(file_path)
return stats
except FileNotFoundError:
raise FileNotFoundError(f"File not found: {file_path}")
except UnicodeDecodeError:
raise UnicodeDecodeError(f"Cannot decode file with {self.encoding} encoding: {file_path}")
except PermissionError:
raise PermissionError(f"Permission denied accessing file: {file_path}")
class OutputFormatter:
"""Handles dual output format generation"""
@staticmethod
def format_json(data: Dict[str, Any]) -> str:
"""Format data as JSON"""
return json.dumps(data, indent=2, ensure_ascii=False)
@staticmethod
def format_human_readable(data: Dict[str, Any]) -> str:
"""Format data as human-readable text"""
lines = []
lines.append("=== TEXT ANALYSIS RESULTS ===")
lines.append(f"File: {data.get('file', 'Unknown')}")
lines.append(f"File size: {data.get('file_size', 0)} bytes")
lines.append(f"Total words: {data.get('total_words', 0)}")
lines.append(f"Unique words: {data.get('unique_words', 0)}")
lines.append(f"Total characters: {data.get('total_characters', 0)}")
lines.append(f"Lines: {data.get('lines', 0)}")
lines.append(f"Average word length: {data.get('average_word_length', 0):.1f}")
most_frequent = data.get('most_frequent', {})
lines.append(f"Most frequent word: \"{most_frequent.get('word', '')}\" ({most_frequent.get('count', 0)} occurrences)")
return "\n".join(lines)
class FileManager:
"""Manages file I/O operations and batch processing"""
def __init__(self, verbose: bool = False):
self.verbose = verbose
def log_verbose(self, message: str):
"""Log verbose message if verbose mode enabled"""
if self.verbose:
print(f"[INFO] {message}", file=sys.stderr)
def find_text_files(self, directory: str) -> List[str]:
"""Find all text files in directory"""
text_extensions = {'.txt', '.md', '.rst', '.csv', '.log'}
text_files = []
try:
for file_path in Path(directory).rglob('*'):
if file_path.is_file() and file_path.suffix.lower() in text_extensions:
text_files.append(str(file_path))
except PermissionError:
raise PermissionError(f"Permission denied accessing directory: {directory}")
return text_files
def write_output(self, content: str, output_path: Optional[str] = None):
"""Write content to file or stdout"""
if output_path:
try:
# Create directory if needed
output_dir = os.path.dirname(output_path)
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir)
with open(output_path, 'w', encoding='utf-8') as file:
file.write(content)
self.log_verbose(f"Output written to: {output_path}")
except PermissionError:
raise PermissionError(f"Permission denied writing to: {output_path}")
else:
print(content)
def analyze_command(args: argparse.Namespace) -> int:
"""Handle analyze command"""
try:
processor = TextProcessor(args.encoding)
file_manager = FileManager(args.verbose)
file_manager.log_verbose(f"Analyzing file: {args.file}")
# Process the file
results = processor.process_file(args.file)
# Format output
if args.format == 'json':
output = OutputFormatter.format_json(results)
else:
output = OutputFormatter.format_human_readable(results)
# Write output
file_manager.write_output(output, args.output)
return 0
except FileNotFoundError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
except UnicodeDecodeError as e:
print(f"Error: {e}", file=sys.stderr)
print(f"Try using --encoding option with different encoding", file=sys.stderr)
return 1
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
def transform_command(args: argparse.Namespace) -> int:
"""Handle transform command"""
try:
processor = TextProcessor(args.encoding)
file_manager = FileManager(args.verbose)
file_manager.log_verbose(f"Transforming file: {args.file}")
# Read and transform the file
with open(args.file, 'r', encoding=args.encoding) as file:
content = file.read()
transformed = processor.transform_text(content, args.mode)
# Write transformed content
file_manager.write_output(transformed, args.output)
return 0
except FileNotFoundError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
def batch_command(args: argparse.Namespace) -> int:
"""Handle batch command"""
try:
processor = TextProcessor(args.encoding)
file_manager = FileManager(args.verbose)
file_manager.log_verbose(f"Finding text files in: {args.directory}")
# Find all text files
text_files = file_manager.find_text_files(args.directory)
if not text_files:
print(f"No text files found in directory: {args.directory}", file=sys.stderr)
return 1
file_manager.log_verbose(f"Found {len(text_files)} text files")
# Process all files
all_results = []
for i, file_path in enumerate(text_files, 1):
try:
file_manager.log_verbose(f"Processing {i}/{len(text_files)}: {file_path}")
results = processor.process_file(file_path)
all_results.append(results)
except Exception as e:
print(f"Warning: Failed to process {file_path}: {e}", file=sys.stderr)
continue
if not all_results:
print("Error: No files could be processed successfully", file=sys.stderr)
return 1
# Format batch results
batch_summary = {
'total_files': len(all_results),
'total_words': sum(r.get('total_words', 0) for r in all_results),
'total_characters': sum(r.get('total_characters', 0) for r in all_results),
'files': all_results
}
if args.format == 'json':
output = OutputFormatter.format_json(batch_summary)
else:
lines = []
lines.append("=== BATCH PROCESSING RESULTS ===")
lines.append(f"Total files processed: {batch_summary['total_files']}")
lines.append(f"Total words across all files: {batch_summary['total_words']}")
lines.append(f"Total characters across all files: {batch_summary['total_characters']}")
lines.append("")
lines.append("Individual file results:")
for result in all_results:
lines.append(f" {result['file']}: {result['total_words']} words")
output = "\n".join(lines)
# Write output
file_manager.write_output(output, args.output)
return 0
except PermissionError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
def main():
"""Main entry point with argument parsing"""
parser = argparse.ArgumentParser(
description="Sample Text Processor - Basic text analysis and transformation",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
Analysis:
python text_processor.py analyze document.txt
python text_processor.py analyze document.txt --format json --output results.json
Transformation:
python text_processor.py transform document.txt --mode upper
python text_processor.py transform document.txt --mode title --output transformed.txt
Batch processing:
python text_processor.py batch text_files/ --verbose
python text_processor.py batch text_files/ --format json --output batch_results.json
Transformation modes:
upper - Convert to uppercase
lower - Convert to lowercase
title - Convert to title case
reverse - Reverse the text
"""
)
parser.add_argument('--format',
choices=['json', 'text'],
default='text',
help='Output format (default: text)')
parser.add_argument('--output',
help='Output file path (default: stdout)')
parser.add_argument('--encoding',
default='utf-8',
help='Text file encoding (default: utf-8)')
parser.add_argument('--verbose',
action='store_true',
help='Enable verbose output')
subparsers = parser.add_subparsers(dest='command', help='Available commands')
# Analyze subcommand
analyze_parser = subparsers.add_parser('analyze', help='Analyze text file statistics')
analyze_parser.add_argument('file', help='Text file to analyze')
# Transform subcommand
transform_parser = subparsers.add_parser('transform', help='Transform text file')
transform_parser.add_argument('file', help='Text file to transform')
transform_parser.add_argument('--mode',
required=True,
choices=['upper', 'lower', 'title', 'reverse'],
help='Transformation mode')
# Batch subcommand
batch_parser = subparsers.add_parser('batch', help='Process multiple files')
batch_parser.add_argument('directory', help='Directory containing text files')
args = parser.parse_args()
if not args.command:
parser.print_help()
return 1
try:
if args.command == 'analyze':
return analyze_command(args)
elif args.command == 'transform':
return transform_command(args)
elif args.command == 'batch':
return batch_command(args)
else:
print(f"Unknown command: {args.command}", file=sys.stderr)
return 1
except KeyboardInterrupt:
print("\nOperation interrupted by user", file=sys.stderr)
return 130
except Exception as e:
print(f"Unexpected error: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())Chủ động lưu tri thức quan trọng vào bộ nhớ tự động kèm thời gian và ngữ cảnh, khi phát hiện quá quan trọng để dựa vào tự động ghi nhận.
---
name: "remember"
description: "Explicitly save important knowledge to auto-memory with timestamp and context. Use when a discovery is too important to rely on auto-capture."
---
# /si:remember — Save Knowledge Explicitly
Writes an explicit entry to auto-memory when something is important enough that you don't want to rely on Claude noticing it automatically.
## Usage
```
/si:remember <what to remember>
/si:remember "This project's CI requires Node 20 LTS — v22 breaks the build"
/si:remember "The /api/auth endpoint uses a custom JWT library, not passport"
/si:remember "Reza prefers explicit error handling over try-catch-all patterns"
```
## When to Use
| Situation | Example |
|-----------|---------|
| Hard-won debugging insight | "CORS errors on /api/upload are caused by the CDN, not the backend" |
| Project convention not in CLAUDE.md | "We use barrel exports in src/components/" |
| Tool-specific gotcha | "Jest needs `--forceExit` flag or it hangs on DB tests" |
| Architecture decision | "We chose Drizzle over Prisma for type-safe SQL" |
| Preference you want Claude to learn | "Don't add comments explaining obvious code" |
## Workflow
### Step 1: Parse the knowledge
Extract from the user's input:
- **What**: The concrete fact or pattern
- **Why it matters**: Context (if provided)
- **Scope**: Project-specific or global?
### Step 2: Check for duplicates
```bash
MEMORY_DIR="$HOME/.claude/projects/$(pwd | sed 's|/|%2F|g; s|%2F|/|; s|^/||')/memory"
grep -ni "<keywords>" "$MEMORY_DIR/MEMORY.md" 2>/dev/null
```
If a similar entry exists:
- Show it to the user
- Ask: "Update the existing entry or add a new one?"
### Step 3: Write to MEMORY.md
Append to the end of `MEMORY.md`:
```markdown
- {{concise fact or pattern}}
```
Keep entries concise — one line when possible. Auto-memory entries don't need timestamps, IDs, or metadata. They're notes, not database records.
If MEMORY.md is over 180 lines, warn the user:
```
⚠️ MEMORY.md is at {{n}}/200 lines. Consider running /si:review to free space.
```
### Step 4: Suggest promotion
If the knowledge sounds like a rule (imperative, always/never, convention):
```
💡 This sounds like it could be a CLAUDE.md rule rather than a memory entry.
Rules are enforced with higher priority. Want to /si:promote it instead?
```
### Step 5: Confirm
```
✅ Saved to auto-memory
"{{entry}}"
MEMORY.md: {{n}}/200 lines
Claude will see this at the start of every session in this project.
```
## What NOT to use /si:remember for
- **Temporary context**: Use session memory or just tell Claude in conversation
- **Enforced rules**: Use `/si:promote` to write directly to CLAUDE.md
- **Cross-project knowledge**: Use `~/.claude/CLAUDE.md` for global rules
- **Sensitive data**: Never store credentials, tokens, or secrets in memory files
## Tips
- Be concise — one line beats a paragraph
- Include the concrete command or value, not just the concept
- ✅ "Build with `pnpm build`, tests with `pnpm test:e2e`"
- ❌ "The project uses pnpm for building and testing"
- If you're remembering the same thing twice, promote it to CLAUDE.md
Quản lý backlog và sprint: viết user story, tiêu chí chấp nhận, lập kế hoạch sprint, ước lượng và ưu tiên.
---
name: "agile-product-owner"
description: Agile product ownership for backlog management and sprint execution. Covers user story writing, acceptance criteria, sprint planning, and velocity tracking. Use for writing user stories, creating acceptance criteria, planning sprints, estimating story points, breaking down epics, or prioritizing backlog.
not_for: Kanban-only workflows, waterfall project planning, general task management, non-Scrum agile frameworks (SAFe, LeSS) without adaptation
triggers:
- write user story
- create acceptance criteria
- plan sprint
- estimate story points
- break down epic
- prioritize backlog
- sprint planning
- backlog grooming
- sprint retrospective
- definition of done
- INVEST criteria
- Given When Then
- user story template
- sprint capacity
- velocity tracking
---
# Agile Product Owner
Backlog management and sprint execution toolkit for product owners, including user story generation, acceptance criteria patterns, sprint planning, and velocity tracking.
---
## Table of Contents
- [What Makes This Skill Different](#what-makes-this-skill-different)
- [User Story Generation Workflow](#user-story-generation-workflow)
- [Acceptance Criteria Patterns](#acceptance-criteria-patterns)
- [Epic Breakdown Workflow](#epic-breakdown-workflow)
- [Sprint Planning Workflow](#sprint-planning-workflow)
- [Backlog Prioritization](#backlog-prioritization)
- [Reference Documentation](#reference-documentation)
- [Tools](#tools)
---
## What Makes This Skill Different
- **Capacity math that aligns with reality:** sprint capacity is based on velocity × availability factor, not hope.
- **Acceptance criteria scaled by story size:** minimum AC counts map to story points to avoid under-spec'ing large items.
- **Weighted prioritization that stays consistent:** value 40%, impact 30%, risk 15%, effort 15% keeps tradeoffs explicit.
- **Systematic epic splitting techniques:** five concrete split patterns prevent oversized stories.
- **INVEST validation baked into workflows:** every story includes a validation step, not just guidance.
## User Story Generation Workflow
Create INVEST-compliant user stories from requirements:
1. Identify the persona (who benefits from this feature)
2. Define the action or capability needed
3. Articulate the benefit or value delivered
4. Write acceptance criteria using Given-When-Then
5. Estimate story points using Fibonacci scale
6. Validate against INVEST criteria
7. Add to backlog with priority
8. **Validation:** Story passes all INVEST criteria; acceptance criteria are testable
### User Story Template
```
As a [persona],
I want to [action/capability],
So that [benefit/value].
```
**Example:**
```
As a marketing manager,
I want to export campaign reports to PDF,
So that I can share results with stakeholders who don't have system access.
```
### Story Types
| Type | Template | Example |
|------|----------|---------|
| Feature | As a [persona], I want to [action] so that [benefit] | As a user, I want to filter search results so that I find items faster |
| Improvement | As a [persona], I need [capability] to [goal] | As a user, I need faster page loads to complete tasks without frustration |
| Bug Fix | As a [persona], I expect [behavior] when [condition] | As a user, I expect my cart to persist when I refresh the page |
| Enabler | As a developer, I need to [technical task] to enable [capability] | As a developer, I need to implement caching to enable instant search |
### Persona Reference
| Persona | Typical Needs | Context |
|---------|--------------|---------|
| End User | Efficiency, simplicity, reliability | Daily feature usage |
| Administrator | Control, visibility, security | System management |
| Power User | Automation, customization, shortcuts | Expert workflows |
| New User | Guidance, learning, safety | Onboarding |
---
## Acceptance Criteria Patterns
Write testable acceptance criteria using Given-When-Then format.
### Given-When-Then Template
```
Given [precondition/context],
When [action/trigger],
Then [expected outcome].
```
**Examples:**
```
Given the user is logged in with valid credentials,
When they click the "Export" button,
Then a PDF download starts within 2 seconds.
Given the user has entered an invalid email format,
When they submit the registration form,
Then an inline error message displays "Please enter a valid email address."
Given the shopping cart contains items,
When the user refreshes the browser,
Then the cart contents remain unchanged.
```
### Acceptance Criteria Checklist
Each story should include criteria for:
| Category | Example |
|----------|---------|
| Happy Path | Given valid input, When submitted, Then success message displayed |
| Validation | Should reject input when required field is empty |
| Error Handling | Must show user-friendly message when API fails |
| Performance | Should complete operation within 2 seconds |
| Accessibility | Must be navigable via keyboard only |
### Minimum Criteria by Story Size
| Story Points | Minimum AC Count |
|--------------|------------------|
| 1-2 | 3-4 criteria |
| 3-5 | 4-6 criteria |
| 8 | 5-8 criteria |
| 13+ | Split the story |
See `references/user-story-templates.md` for complete template library.
---
## Epic Breakdown Workflow
Break epics into deliverable sprint-sized stories:
1. Define epic scope and success criteria
2. Identify all personas affected by the epic
3. List all capabilities needed for each persona
4. Group capabilities into logical stories
5. Validate each story is ≤8 points
6. Identify dependencies between stories
7. Sequence stories for incremental delivery
8. **Validation:** Each story delivers standalone value; total stories cover epic scope
### Splitting Techniques
| Technique | When to Use | Example |
|-----------|-------------|---------|
| By workflow step | Linear process | "Checkout" → "Add to cart" + "Enter payment" + "Confirm order" |
| By persona | Multiple user types | "Dashboard" → "Admin dashboard" + "User dashboard" |
| By data type | Multiple inputs | "Import" → "Import CSV" + "Import Excel" |
| By operation | CRUD functionality | "Manage users" → "Create" + "Edit" + "Delete" |
| Happy path first | Risk reduction | "Feature" → "Basic flow" + "Error handling" + "Edge cases" |
### Epic Example
**Epic:** User Dashboard
**Breakdown:**
```
Epic: User Dashboard (34 points total)
├── US-001: View key metrics (5 pts) - End User
├── US-002: Customize layout (5 pts) - Power User
├── US-003: Export data to CSV (3 pts) - End User
├── US-004: Share with team (5 pts) - End User
├── US-005: Set up alerts (5 pts) - Power User
├── US-006: Filter by date range (3 pts) - End User
├── US-007: Admin overview (5 pts) - Admin
└── US-008: Enable caching (3 pts) - Enabler
```
---
## Sprint Planning Workflow
Plan sprint capacity and select stories:
1. Calculate team capacity (velocity × availability)
2. Review sprint goal with stakeholders
3. Select stories from prioritized backlog
4. Fill to 80-85% of capacity (committed)
5. Add stretch goals (10-15% additional)
6. Identify dependencies and risks
7. Break complex stories into tasks
8. **Validation:** Committed points ≤85% capacity; all stories have acceptance criteria
### Capacity Calculation
```
Sprint Capacity = Average Velocity × Availability Factor
Example:
Average Velocity: 30 points
Team availability: 90% (one member partially out)
Adjusted Capacity: 27 points
Committed: 23 points (85% of 27)
Stretch: 4 points (15% of 27)
```
### Availability Factors
| Scenario | Factor |
|----------|--------|
| Full sprint, no PTO | 1.0 |
| One team member out 50% | 0.9 |
| Holiday during sprint | 0.8 |
| Multiple members out | 0.7 |
### Sprint Loading Template
```
Sprint Capacity: 27 points
Sprint Goal: [Clear, measurable objective]
COMMITTED (23 points):
[H] US-001: User dashboard (5 pts)
[H] US-002: Export feature (3 pts)
[H] US-003: Search filter (5 pts)
[M] US-004: Settings page (5 pts)
[M] US-005: Help tooltips (3 pts)
[L] US-006: Theme options (2 pts)
STRETCH (4 points):
[L] US-007: Sort options (2 pts)
[L] US-008: Print view (2 pts)
```
See `references/sprint-planning-guide.md` for complete planning procedures.
---
## Backlog Prioritization
Prioritize backlog using value and effort assessment.
### Priority Levels
| Priority | Definition | Sprint Target |
|----------|------------|---------------|
| Critical | Blocking users, security, data loss | Immediate |
| High | Core functionality, key user needs | This sprint |
| Medium | Improvements, enhancements | Next 2-3 sprints |
| Low | Nice-to-have, minor improvements | Backlog |
### Prioritization Factors
| Factor | Weight | Questions |
|--------|--------|-----------|
| Business Value | 40% | Revenue impact? User demand? Strategic alignment? |
| User Impact | 30% | How many users? How frequently used? |
| Risk/Dependencies | 15% | Technical risk? External dependencies? |
| Effort | 15% | Size? Complexity? Uncertainty? |
### INVEST Criteria Validation
Before adding to sprint, validate each story:
| Criterion | Question | Pass If... |
|-----------|----------|------------|
| **I**ndependent | Can this be developed without other uncommitted stories? | No blocking dependencies |
| **N**egotiable | Is the implementation flexible? | Multiple approaches possible |
| **V**aluable | Does this deliver user or business value? | Clear benefit in "so that" |
| **E**stimable | Can the team estimate this? | Understood well enough to size |
| **S**mall | Can this complete in one sprint? | ≤8 story points |
| **T**estable | Can we verify this is done? | Clear acceptance criteria |
---
## Reference Documentation
### User Story Templates
`references/user-story-templates.md` contains:
- Standard story formats by type (feature, improvement, bug fix, enabler)
- Acceptance criteria patterns (Given-When-Then, Should/Must/Can)
- INVEST criteria validation checklist
- Story point estimation guide (Fibonacci scale)
- Common story antipatterns and fixes
- Story splitting techniques
### Sprint Planning Guide
`references/sprint-planning-guide.md` contains:
- Sprint planning meeting agenda
- Capacity calculation formulas
- Backlog prioritization framework (WSJF)
- Sprint ceremony guides (standup, review, retro)
- Velocity tracking and burndown patterns
- Definition of Done checklist
- Sprint metrics and targets
---
## Tools
### User Story Generator
```bash
# Generate stories from sample epic
python scripts/user_story_generator.py
# Plan sprint with capacity
python scripts/user_story_generator.py sprint 30
```
Generates:
- INVEST-compliant user stories
- Given-When-Then acceptance criteria
- Story point estimates (Fibonacci scale)
- Priority assignments
- Sprint loading with committed and stretch items
### Sample Output
```
USER STORY: USR-001
========================================
Title: View Key Metrics
Type: story
Priority: HIGH
Points: 5
Story:
As a End User, I want to view key metrics and KPIs
so that I can save time and work more efficiently
Acceptance Criteria:
1. Given user has access, When they view key metrics, Then the result is displayed
2. Should validate input before processing
3. Must show clear error message when action fails
4. Should complete within 2 seconds
5. Must be accessible via keyboard navigation
INVEST Checklist:
✓ Independent
✓ Negotiable
✓ Valuable
✓ Estimable
✓ Small
✓ Testable
```
---
## Sprint Metrics
Track sprint health and team performance.
### Key Metrics
| Metric | Formula | Target |
|--------|---------|--------|
| Velocity | Points completed / sprint | Stable ±10% |
| Commitment Reliability | Completed / Committed | >85% |
| Scope Change | Points added or removed mid-sprint | <10% |
| Carryover | Points not completed | <15% |
### Velocity Tracking
```
Sprint 1: 25 points
Sprint 2: 28 points
Sprint 3: 30 points
Sprint 4: 32 points
Sprint 5: 29 points
------------------------
Average Velocity: 28.8 points
Trend: Stable
Planning: Commit to 24-26 points
```
### Definition of Done
Story is complete when:
- [ ] Code complete and peer reviewed
- [ ] Unit tests written and passing
- [ ] Acceptance criteria verified
- [ ] Documentation updated
- [ ] Deployed to staging environment
- [ ] Product Owner accepted
- [ ] No critical bugs remaining
## Related Skills
- **Scrum Master** (`project-management/scrum-master/`) — Velocity data and sprint ceremonies complement backlog management
- **Product Manager Toolkit** (`product-team/product-manager-toolkit/`) — RICE prioritization feeds backlog ordering
FILE:assets/sprint_planning_template.md
# Sprint Planning Document
## Sprint Info
| Field | Value |
|-------|-------|
| **Sprint** | Sprint [Number] |
| **Dates** | [Start Date] - [End Date] |
| **Sprint Goal** | [One sentence describing what this sprint achieves] |
| **Scrum Master** | [Name] |
| **Product Owner** | [Name] |
---
## Sprint Goal
[Expand on the sprint goal. What is the single most important outcome? What does "done" look like for this sprint? 2-3 sentences.]
---
## Team Capacity
| Team Member | Available Days | Notes |
|------------|---------------|-------|
| [Name] | [X] / [Total] | [PTO, training, on-call, etc.] |
| [Name] | [X] / [Total] | |
| [Name] | [X] / [Total] | |
**Total Capacity:** [X] person-days
**Historical Velocity:** [X] story points (avg of last 3 sprints)
**Planned Velocity:** [X] story points (adjusted for capacity)
---
## Selected Stories
| # | Story | Points | Assignee | Priority | Status |
|---|-------|--------|----------|----------|--------|
| 1 | [Story title with ticket link] | [SP] | [Name] | Must Have | To Do |
| 2 | [Story title with ticket link] | [SP] | [Name] | Must Have | To Do |
| 3 | [Story title with ticket link] | [SP] | [Name] | Should Have | To Do |
| 4 | [Story title with ticket link] | [SP] | [Name] | Nice to Have | To Do |
**Total Planned:** [X] story points
---
## Dependencies
| Dependency | Blocking Story | Owner | Status | Due Date |
|-----------|---------------|-------|--------|----------|
| [External API ready] | [Story #] | [Name/Team] | [Status] | [Date] |
| [Design review complete] | [Story #] | [Name] | [Status] | [Date] |
---
## Risks
| Risk | Impact | Likelihood | Mitigation |
|------|--------|-----------|-----------|
| [Risk description] | High/Med/Low | High/Med/Low | [Plan] |
| [Risk description] | High/Med/Low | High/Med/Low | [Plan] |
---
## Definition of Done
- [ ] Code complete and peer reviewed
- [ ] Unit tests written and passing
- [ ] Acceptance criteria verified
- [ ] Documentation updated (if applicable)
- [ ] Deployed to staging environment
- [ ] QA sign-off received
- [ ] Product Owner acceptance
---
## Sprint Ceremonies
| Ceremony | Day/Time | Duration |
|----------|----------|----------|
| Daily Standup | [Day] [Time] | 15 min |
| Sprint Review | [Date] [Time] | 60 min |
| Sprint Retrospective | [Date] [Time] | 60 min |
FILE:assets/user_story_template.md
# User Story
## Story Info
| Field | Value |
|-------|-------|
| **Title** | [Short descriptive title] |
| **Epic** | [Parent epic name/link] |
| **Story Points** | [Estimate] |
| **Priority** | Must Have / Should Have / Nice to Have |
| **Assignee** | [Name] |
| **Sprint** | [Sprint number] |
---
## User Story
**As a** [type of user/role],
**I want** [capability or feature],
**So that** [benefit or value I receive].
---
## Acceptance Criteria
### Scenario 1: [Happy Path]
- **Given** [precondition or context]
- **When** [action the user takes]
- **Then** [expected outcome]
### Scenario 2: [Alternative Path]
- **Given** [precondition or context]
- **When** [action the user takes]
- **Then** [expected outcome]
### Scenario 3: [Edge Case / Error]
- **Given** [precondition or context]
- **When** [action the user takes]
- **Then** [expected error handling or outcome]
---
## Notes
- [Technical notes, design references, API details]
- [Dependencies on other stories or external systems]
- [Out of scope items for this story]
- [Links to mockups, prototypes, or design files]
---
## Definition of Done
- [ ] Acceptance criteria met
- [ ] Code reviewed and approved
- [ ] Tests written and passing
- [ ] Documentation updated
- [ ] Product Owner accepted
FILE:references/sprint-planning-guide.md
# Sprint Planning Guide
Sprint planning workflows, capacity calculation, and backlog management.
---
## Table of Contents
- [Sprint Planning Workflow](#sprint-planning-workflow)
- [Capacity Planning](#capacity-planning)
- [Backlog Prioritization](#backlog-prioritization)
- [Sprint Ceremonies](#sprint-ceremonies)
- [Metrics and Tracking](#metrics-and-tracking)
---
## Sprint Planning Workflow
### Pre-Planning (1-2 Days Before)
1. Review and refine backlog items for upcoming sprint
2. Ensure top items have acceptance criteria
3. Validate story point estimates with team
4. Identify dependencies between stories
5. Confirm team availability for sprint
6. **Validation:** Top 1.5x capacity of stories are refined and estimated
### Sprint Planning Meeting
**Duration:** 2 hours for 2-week sprint
**Agenda:**
| Time | Activity | Participants |
|------|----------|--------------|
| 0:00-0:15 | Review sprint goal and priorities | PO presents |
| 0:15-0:45 | Discuss top backlog items | Team asks questions |
| 0:45-1:15 | Team selects stories for sprint | Team decides |
| 1:15-1:45 | Break down stories into tasks | Team collaborates |
| 1:45-2:00 | Confirm commitment and identify risks | All |
### Planning Checklist
**Before Planning:**
- [ ] Backlog groomed with top items refined
- [ ] Previous sprint retrospective actions reviewed
- [ ] Team capacity calculated
- [ ] Dependencies identified
- [ ] Sprint goal drafted
**During Planning:**
- [ ] Sprint goal agreed
- [ ] Stories selected fit within capacity
- [ ] Acceptance criteria reviewed for each story
- [ ] Tasks identified for complex stories
- [ ] Risks and blockers discussed
**After Planning:**
- [ ] Sprint backlog visible to all
- [ ] Sprint goal communicated
- [ ] Calendar blocked for ceremonies
- [ ] Dependencies communicated to other teams
---
## Capacity Planning
### Team Capacity Calculation
```
Sprint Capacity = (Team Members × Sprint Days × Hours/Day × Focus Factor)
÷ Hours per Story Point
Simplified Version:
Sprint Capacity = Average Velocity × Availability Factor
```
### Availability Factors
| Scenario | Factor | Example |
|----------|--------|---------|
| Full sprint, no PTO | 1.0 | 30 points if velocity = 30 |
| 1 team member out 50% | 0.9 | 27 points |
| Holiday during sprint | 0.8 | 24 points |
| Multiple team members out | 0.7 | 21 points |
| Major release/on-call | 0.75 | 22-23 points |
### Capacity Buffer Rules
| Commitment Level | % of Velocity | Purpose |
|------------------|---------------|---------|
| Committed | 80-85% | High confidence delivery |
| Stretch | 10-15% | Optional if things go well |
| Buffer | 5-10% | Unplanned work, bugs |
### Sprint Loading Example
```
Team Velocity: 30 points/sprint
Availability: 90% (one team member partially out)
Adjusted Velocity: 27 points
Sprint Loading:
- Committed work: 23 points (85% of 27)
- Stretch goals: 4 points (15% of 27)
- Buffer: Remaining capacity for bugs/support
Story Selection:
[H] US-001: User dashboard (5 pts) ← Committed
[H] US-002: Export feature (3 pts) ← Committed
[H] US-003: Search filter (5 pts) ← Committed
[M] US-004: Settings page (5 pts) ← Committed
[M] US-005: Help tooltips (3 pts) ← Committed
[L] US-006: Theme options (2 pts) ← Committed
------------------------
Committed Total: 23 points
[L] US-007: Sort options (2 pts) ← Stretch
[L] US-008: Print view (2 pts) ← Stretch
------------------------
Stretch Total: 4 points
```
---
## Backlog Prioritization
### Priority Framework
| Priority | Definition | SLA |
|----------|------------|-----|
| Critical | Blocking users, security, data loss | Immediate |
| High | Core functionality, key user needs | This sprint |
| Medium | Improvements, enhancements | Next 2-3 sprints |
| Low | Nice-to-have, minor improvements | Backlog |
### Prioritization Factors
| Factor | Weight | Questions |
|--------|--------|-----------|
| Business Value | 40% | Revenue impact? User demand? Strategic? |
| User Impact | 30% | How many users? How often used? |
| Risk/Dependencies | 15% | Technical risk? External dependencies? |
| Effort | 15% | Size? Complexity? Uncertainty? |
### WSJF (Weighted Shortest Job First)
For larger items, use SAFe's WSJF:
```
WSJF = Cost of Delay / Job Duration
Cost of Delay = User Value + Time Criticality + Risk Reduction
Scale: 1, 2, 3, 5, 8, 13, 20
Example:
Feature A: CoD = 13, Duration = 5 → WSJF = 2.6
Feature B: CoD = 8, Duration = 2 → WSJF = 4.0 ← Higher priority
```
### Backlog Organization
| Section | Content | Review Frequency |
|---------|---------|------------------|
| Sprint Backlog | Committed for current sprint | Daily |
| Ready | Refined, estimated, prioritized | Each planning |
| Grooming | Needs refinement | Weekly |
| Icebox | Future consideration | Monthly |
| Archive | Completed or obsolete | Quarterly |
---
## Sprint Ceremonies
### Daily Standup
**Duration:** 15 minutes max
**Format:** Each team member answers:
1. What did I complete yesterday?
2. What will I work on today?
3. What blockers do I have?
**Product Owner Role:**
- Listen for blockers needing PO action
- Answer clarifying questions
- Note scope concerns for offline discussion
- Update stakeholders on progress
### Backlog Refinement (Grooming)
**Duration:** 1-2 hours per week
**Timing:** Mid-sprint
**Agenda:**
| Time | Activity |
|------|----------|
| 0:00-0:15 | Review upcoming priorities |
| 0:15-0:45 | Detail acceptance criteria for top items |
| 0:45-1:15 | Estimate new stories |
| 1:15-1:30 | Split large stories |
**Readiness Criteria:**
- [ ] Clear user story format (As a... I want... So that...)
- [ ] Acceptance criteria defined (Given-When-Then)
- [ ] Story point estimate agreed
- [ ] Dependencies identified
- [ ] Fits in one sprint (≤8 points)
### Sprint Review (Demo)
**Duration:** 1 hour for 2-week sprint
**Agenda:**
| Time | Activity | Lead |
|------|----------|------|
| 0:00-0:05 | Sprint goal recap | PO |
| 0:05-0:40 | Demo completed work | Team |
| 0:40-0:50 | Stakeholder feedback | Stakeholders |
| 0:50-1:00 | Roadmap update | PO |
**Demo Checklist:**
- [ ] Only demo completed (done-done) stories
- [ ] Use production or production-like environment
- [ ] Show user perspective, not technical details
- [ ] Collect feedback for backlog items
- [ ] Thank team for accomplishments
### Sprint Retrospective
**Duration:** 1.5 hours for 2-week sprint
**Format Options:**
| Format | Structure |
|--------|-----------|
| Start-Stop-Continue | What to begin, end, keep doing |
| 4Ls | Liked, Learned, Lacked, Longed for |
| Sailboat | Wind (helpers), Anchors (blockers), Rocks (risks) |
| Mad-Sad-Glad | Emotional state about sprint events |
**Action Items:**
- Maximum 2-3 improvement actions per retro
- Assign owner and due date
- Review previous actions at start of next retro
---
## Metrics and Tracking
### Sprint Metrics
| Metric | Formula | Target |
|--------|---------|--------|
| Velocity | Points completed / sprint | Stable ±10% |
| Commitment Reliability | Completed / Committed | >85% |
| Scope Change | Points added or removed | <10% |
| Carryover | Points not completed | <15% |
| Bug Ratio | Bug points / Total points | <20% |
### Velocity Tracking
```
Sprint Velocity Trend:
Sprint 1: 25 points
Sprint 2: 28 points
Sprint 3: 30 points
Sprint 4: 32 points
Sprint 5: 29 points
------------------------
Average: 28.8 points
Trend: Stable (±10%)
Planning Recommendation: Plan for 26-29 points committed
```
### Burndown Chart
Track progress within sprint:
```
Day Ideal Actual Status
--- ----- ------ ------
0 30 30 On track
2 24 26 Slightly behind
4 18 20 Behind
6 12 14 Recovering
8 6 6 On track
10 0 2 Minor carryover
```
**Burndown Patterns:**
| Pattern | Meaning | Action |
|---------|---------|--------|
| Flat start | No progress early | Check blockers |
| Late drop | Last-minute completion | Improve WIP limits |
| Scope increase | Line moves up | Address scope creep |
| Early completion | Done before sprint end | Pull stretch items |
### Definition of Done
Story is complete when:
- [ ] Code complete and reviewed
- [ ] Unit tests written and passing
- [ ] Integration tests passing
- [ ] Acceptance criteria verified
- [ ] Documentation updated
- [ ] Deployed to staging
- [ ] PO accepted
- [ ] No critical bugs
### Release Metrics
| Metric | Definition | Target |
|--------|------------|--------|
| Lead Time | Idea to production | <2 sprints |
| Cycle Time | Development start to done | <1 sprint |
| Throughput | Stories completed/sprint | Increasing |
| Defect Escape | Bugs found in production | Decreasing |
FILE:references/user-story-templates.md
# User Story Templates
Standard templates, acceptance criteria patterns, and INVEST validation for user stories.
---
## Table of Contents
- [Story Templates](#story-templates)
- [Acceptance Criteria Patterns](#acceptance-criteria-patterns)
- [INVEST Criteria](#invest-criteria)
- [Story Point Estimation](#story-point-estimation)
- [Common Antipatterns](#common-antipatterns)
---
## Story Templates
### Standard User Story Format
```
As a [persona],
I want to [action/capability],
So that [benefit/value].
```
### Template by Story Type
**Feature Story:**
```
As a [persona],
I want to [perform action]
So that [I achieve benefit].
Example:
As a marketing manager,
I want to export campaign reports to PDF
So that I can share results with stakeholders who don't have system access.
```
**Improvement Story:**
```
As a [persona],
I need [capability/improvement]
To [achieve goal more effectively].
Example:
As a sales rep,
I need faster search results
To find customer records without interrupting calls.
```
**Bug Fix Story:**
```
As a [persona],
I expect [correct behavior]
When [specific condition].
Example:
As a user,
I expect my session to remain active
When navigating between dashboard tabs.
```
**Integration Story:**
```
As a [persona],
I want to [integrate/connect with system]
So that [workflow improvement].
Example:
As an admin,
I want to sync user data with our LDAP server
So that employees are automatically provisioned.
```
**Enabler Story (Technical):**
```
As a developer,
I need to [technical requirement]
To enable [user-facing capability].
Example:
As a developer,
I need to implement caching layer
To enable sub-second dashboard load times.
```
### Persona Library
| Persona | Typical Needs | Context |
|---------|--------------|---------|
| End User | Efficiency, simplicity, reliability | Daily core feature usage |
| Administrator | Control, visibility, security | System management |
| Power User | Automation, customization, shortcuts | Expert workflows |
| New User | Guidance, learning, safety | Onboarding experience |
| Manager | Reporting, oversight, delegation | Team coordination |
| External User | Access, security, documentation | Customer/partner usage |
---
## Acceptance Criteria Patterns
### Given-When-Then (Gherkin)
Preferred format for testable acceptance criteria:
```
Given [precondition/context],
When [action/trigger],
Then [expected outcome].
```
**Examples:**
```
Given the user is logged in with valid credentials,
When they click the "Export" button,
Then a PDF download starts within 2 seconds.
Given the user has entered invalid email format,
When they submit the registration form,
Then an inline error message displays "Please enter a valid email address."
Given the daily sync job has not run in 24 hours,
When the scheduler triggers at midnight,
Then all pending records are synchronized and logged.
```
### Should/Must/Can Patterns
**Should (Expected Behavior):**
```
Should [behavior] when [condition].
Example:
Should display loading spinner when API call exceeds 500ms.
```
**Must (Hard Requirement):**
```
Must [requirement] to [achieve outcome].
Example:
Must encrypt all data at rest to meet compliance requirements.
```
**Can (Capability):**
```
Can [capability] without [negative outcome].
Example:
Can undo last action without losing other changes.
```
### Acceptance Criteria Checklist
Each story should have acceptance criteria covering:
| Category | Example Criterion |
|----------|-------------------|
| Happy Path | Given valid input, When submitted, Then success message displayed |
| Validation | Should reject input when required field is empty |
| Error Handling | Must show user-friendly message when API fails |
| Performance | Should complete operation within 2 seconds |
| Accessibility | Must be navigable via keyboard only |
| Security | Should not expose sensitive data in URL parameters |
### Minimum Acceptance Criteria Count
| Story Size (Points) | Minimum AC Count |
|--------------------|------------------|
| 1-2 | 3-4 |
| 3-5 | 4-6 |
| 8 | 5-8 |
| 13+ | Split the story |
---
## INVEST Criteria
### INVEST Validation Checklist
| Criterion | Question | Pass If... |
|-----------|----------|------------|
| **I**ndependent | Can this story be developed without depending on another story? | No blocking dependencies on uncommitted work |
| **N**egotiable | Is the implementation approach flexible? | Multiple ways to deliver the value |
| **V**aluable | Does this deliver value to users or business? | Clear benefit statement in "so that" |
| **E**stimable | Can the team estimate this story? | Understood well enough to size |
| **S**mall | Can this be completed in one sprint? | ≤8 story points typically |
| **T**estable | Can we verify this story is done? | Clear, measurable acceptance criteria |
### INVEST Failure Patterns
| Criterion | Red Flag | Fix |
|-----------|----------|-----|
| Independent | "After story X is done..." | Combine stories or resequence |
| Negotiable | Specific implementation in story | Focus on outcome, not solution |
| Valuable | No "so that" clause | Add benefit statement |
| Estimable | Team says "no idea" | Spike first, then story |
| Small | >8 points | Split into smaller stories |
| Testable | "System should be better" | Add measurable criteria |
### Story Splitting Techniques
When stories are too large (>8 points), split using:
| Technique | Example |
|-----------|---------|
| By workflow step | "Create order" → "Add items" + "Apply discount" + "Submit order" |
| By persona | "User dashboard" → "Admin dashboard" + "Member dashboard" |
| By data type | "Import data" → "Import CSV" + "Import Excel" |
| By operation | "Manage users" → "Add user" + "Edit user" + "Delete user" |
| By platform | "Mobile support" → "iOS support" + "Android support" |
| Happy path first | "Full feature" → "Basic feature" + "Error handling" + "Edge cases" |
---
## Story Point Estimation
### Fibonacci Scale Reference
| Points | Complexity | Example |
|--------|------------|---------|
| 1 | Trivial | Fix typo, change label |
| 2 | Simple | Add field, simple validation |
| 3 | Small | New form, basic CRUD operation |
| 5 | Medium | Feature with multiple components |
| 8 | Large | Complex feature, multiple integrations |
| 13 | Very Large | Consider splitting |
| 21+ | Epic | Must split |
### Estimation Factors
| Factor | Low Complexity | High Complexity |
|--------|---------------|-----------------|
| Unknowns | Well understood | Many unknowns |
| Dependencies | None | Multiple systems |
| Testing | Simple unit tests | Complex integration tests |
| Data | Simple structure | Complex transformations |
| UI | Minor changes | New components |
### Velocity Calculation
```
Velocity = Total points completed / Number of sprints
Example:
Sprint 1: 28 points
Sprint 2: 32 points
Sprint 3: 30 points
Average Velocity: (28 + 32 + 30) / 3 = 30 points/sprint
Sprint Capacity Planning:
- Committed: 80-90% of velocity (24-27 points)
- Stretch goals: 10-20% additional (3-6 points)
```
---
## Common Antipatterns
### Story Antipatterns
| Antipattern | Example | Fix |
|-------------|---------|-----|
| Solution story | "Implement React component" | "Display user profile information" |
| Compound story | "Create, edit, and delete users" | Split into three stories |
| Missing persona | "The system will..." | "As an admin, I want to..." |
| No benefit | "I want to see a button" | Add "so that [benefit]" |
| Too vague | "Improve performance" | "Reduce page load to <2 seconds" |
| Technical jargon | "Implement Redis caching" | "Enable instant search results" |
### Acceptance Criteria Antipatterns
| Antipattern | Example | Fix |
|-------------|---------|-----|
| Too vague | "Works correctly" | Specific Given-When-Then |
| Implementation details | "Use PostgreSQL query" | Focus on outcome |
| Missing unhappy path | Only success scenario | Add error cases |
| Untestable | "User is happy" | Measurable behavior |
| Too many | 15+ criteria | Split the story |
### Sprint Planning Antipatterns
| Antipattern | Impact | Fix |
|-------------|--------|-----|
| 100% capacity | No buffer for unknowns | Plan 80-85% |
| All large stories | Risk of incomplete sprint | Mix sizes |
| No dependencies mapped | Blocked work | Identify dependencies upfront |
| Stretch = overflow | Hiding overcommitment | Stretch should be optional |
FILE:scripts/user_story_generator.py
#!/usr/bin/env python3
"""
User Story Generator with INVEST Criteria
Creates well-formed user stories with acceptance criteria
"""
import json
from typing import Dict, List, Tuple
class UserStoryGenerator:
"""Generate INVEST-compliant user stories"""
def __init__(self):
self.personas = {
'end_user': {
'name': 'End User',
'needs': ['efficiency', 'simplicity', 'reliability', 'speed'],
'context': 'daily usage of core features'
},
'admin': {
'name': 'Administrator',
'needs': ['control', 'visibility', 'security', 'configuration'],
'context': 'system management and oversight'
},
'power_user': {
'name': 'Power User',
'needs': ['advanced features', 'automation', 'customization', 'shortcuts'],
'context': 'expert usage and workflow optimization'
},
'new_user': {
'name': 'New User',
'needs': ['guidance', 'learning', 'safety', 'clarity'],
'context': 'first-time experience and onboarding'
}
}
self.story_templates = {
'feature': "As a {persona}, I want to {action} so that {benefit}",
'improvement': "As a {persona}, I need {capability} to {achieve_goal}",
'fix': "As a {persona}, I expect {behavior} when {condition}",
'integration': "As a {persona}, I want to {integrate} so that {workflow}"
}
self.acceptance_criteria_patterns = [
"Given {precondition}, When {action}, Then {outcome}",
"Should {behavior} when {condition}",
"Must {requirement} to {achieve}",
"Can {capability} without {negative_outcome}"
]
def generate_epic_stories(self, epic: Dict) -> List[Dict]:
"""Break down epic into user stories"""
stories = []
# Analyze epic for key components
epic_name = epic.get('name', 'Feature')
epic_description = epic.get('description', '')
personas = epic.get('personas', ['end_user'])
scope = epic.get('scope', [])
# Generate stories for each persona and scope item
for persona in personas:
for i, scope_item in enumerate(scope):
story = self.generate_story(
persona=persona,
feature=scope_item,
epic=epic_name,
index=i+1
)
stories.append(story)
# Add enabler stories (technical, infrastructure)
if epic.get('technical_requirements'):
for req in epic['technical_requirements']:
enabler = self.generate_enabler_story(req, epic_name)
stories.append(enabler)
return stories
def generate_story(self, persona: str, feature: str, epic: str, index: int) -> Dict:
"""Generate a single user story"""
persona_data = self.personas.get(persona, self.personas['end_user'])
# Create story
story = {
'id': f"{epic[:3].upper()}-{index:03d}",
'type': 'story',
'title': self._generate_title(feature),
'narrative': self._generate_narrative(persona_data, feature),
'acceptance_criteria': self._generate_acceptance_criteria(feature),
'estimation': self._estimate_complexity(feature),
'priority': self._determine_priority(persona, feature),
'dependencies': [],
'invest_check': self._check_invest_criteria(feature)
}
return story
def generate_enabler_story(self, requirement: str, epic: str) -> Dict:
"""Generate technical enabler story"""
return {
'id': f"{epic[:3].upper()}-E{len(requirement):02d}",
'type': 'enabler',
'title': f"Technical: {requirement}",
'narrative': f"As a developer, I need to {requirement} to enable user features",
'acceptance_criteria': [
f"Technical requirement {requirement} is implemented",
"All tests pass",
"Documentation is updated",
"No regression in existing functionality"
],
'estimation': 5, # Default medium complexity
'priority': 'high',
'dependencies': [],
'invest_check': {
'independent': True,
'negotiable': False, # Technical requirements often non-negotiable
'valuable': True,
'estimable': True,
'small': True,
'testable': True
}
}
def _generate_title(self, feature: str) -> str:
"""Generate concise story title"""
# Simplify feature description to title
words = feature.split()[:5]
return ' '.join(words).title()
def _generate_narrative(self, persona: Dict, feature: str) -> str:
"""Generate story narrative in standard format"""
template = self.story_templates['feature']
action = self._extract_action(feature)
benefit = self._extract_benefit(feature, persona['needs'])
return template.format(
persona=persona['name'],
action=action,
benefit=benefit
)
def _generate_acceptance_criteria(self, feature: str) -> List[str]:
"""Generate acceptance criteria"""
criteria = []
# Happy path
criteria.append(f"Given user has access, When they {self._extract_action(feature)}, Then {self._extract_outcome(feature)}")
# Validation
criteria.append(f"Should validate input before processing")
# Error handling
criteria.append(f"Must show clear error message when action fails")
# Performance
criteria.append(f"Should complete within 2 seconds")
# Accessibility
criteria.append(f"Must be accessible via keyboard navigation")
return criteria
def _extract_action(self, feature: str) -> str:
"""Extract action from feature description"""
action_verbs = ['create', 'view', 'edit', 'delete', 'share', 'export', 'import', 'configure', 'search', 'filter']
feature_lower = feature.lower()
for verb in action_verbs:
if verb in feature_lower:
return feature_lower
return f"use {feature.lower()}"
def _extract_benefit(self, feature: str, needs: List[str]) -> str:
"""Extract benefit based on feature and persona needs"""
feature_lower = feature.lower()
if 'save' in feature_lower or 'quick' in feature_lower:
return "I can save time and work more efficiently"
elif 'share' in feature_lower or 'collab' in feature_lower:
return "I can collaborate with my team effectively"
elif 'report' in feature_lower or 'analyt' in feature_lower:
return "I can make data-driven decisions"
elif 'automat' in feature_lower:
return "I can reduce manual work and errors"
else:
return f"I can achieve my goals related to {needs[0]}"
def _extract_outcome(self, feature: str) -> str:
"""Extract expected outcome"""
return f"the {feature.lower()} is successfully completed"
def _estimate_complexity(self, feature: str) -> int:
"""Estimate story points based on complexity indicators"""
feature_lower = feature.lower()
# Complexity indicators
complexity = 3 # Base complexity
if any(word in feature_lower for word in ['simple', 'basic', 'view', 'display']):
complexity = 1
elif any(word in feature_lower for word in ['create', 'edit', 'update']):
complexity = 3
elif any(word in feature_lower for word in ['complex', 'advanced', 'integrate', 'migrate']):
complexity = 8
elif any(word in feature_lower for word in ['redesign', 'refactor', 'architect']):
complexity = 13
return complexity
def _determine_priority(self, persona: str, feature: str) -> str:
"""Determine story priority"""
feature_lower = feature.lower()
# Critical features
if any(word in feature_lower for word in ['security', 'fix', 'critical', 'broken']):
return 'critical'
# High priority for primary personas
if persona in ['end_user', 'admin']:
if any(word in feature_lower for word in ['core', 'essential', 'primary']):
return 'high'
# Medium for improvements
if any(word in feature_lower for word in ['improve', 'enhance', 'optimize']):
return 'medium'
# Low for nice-to-haves
return 'low'
def _check_invest_criteria(self, feature: str) -> Dict[str, bool]:
"""Check INVEST criteria compliance"""
return {
'independent': not any(word in feature.lower() for word in ['after', 'depends', 'requires']),
'negotiable': True, # Most features can be negotiated
'valuable': True, # Assume value if it made it to backlog
'estimable': len(feature.split()) < 20, # Can estimate if not too vague
'small': self._estimate_complexity(feature) <= 8, # 8 points or less
'testable': not any(word in feature.lower() for word in ['maybe', 'possibly', 'somehow'])
}
def generate_sprint_stories(self, capacity: int, backlog: List[Dict]) -> Dict:
"""Generate stories for a sprint based on capacity"""
sprint = {
'capacity': capacity,
'committed': [],
'stretch': [],
'total_points': 0,
'utilization': 0
}
# Sort backlog by priority and size
sorted_backlog = sorted(
backlog,
key=lambda x: (
{'critical': 0, 'high': 1, 'medium': 2, 'low': 3}[x['priority']],
x['estimation']
)
)
# Fill sprint
for story in sorted_backlog:
if sprint['total_points'] + story['estimation'] <= capacity:
sprint['committed'].append(story)
sprint['total_points'] += story['estimation']
elif sprint['total_points'] + story['estimation'] <= capacity * 1.2:
sprint['stretch'].append(story)
sprint['utilization'] = round((sprint['total_points'] / capacity) * 100, 1)
return sprint
def format_story_output(self, story: Dict) -> str:
"""Format story for display"""
output = []
output.append(f"USER STORY: {story['id']}")
output.append("=" * 40)
output.append(f"Title: {story['title']}")
output.append(f"Type: {story['type']}")
output.append(f"Priority: {story['priority'].upper()}")
output.append(f"Points: {story['estimation']}")
output.append("")
output.append("Story:")
output.append(story['narrative'])
output.append("")
output.append("Acceptance Criteria:")
for i, criterion in enumerate(story['acceptance_criteria'], 1):
output.append(f" {i}. {criterion}")
output.append("")
output.append("INVEST Checklist:")
for criterion, passed in story['invest_check'].items():
status = "✓" if passed else "✗"
output.append(f" {status} {criterion.capitalize()}")
return "\n".join(output)
def create_sample_epic():
"""Create a sample epic for testing"""
return {
'name': 'User Dashboard',
'description': 'Create a comprehensive dashboard for users to view their data',
'personas': ['end_user', 'power_user'],
'scope': [
'View key metrics and KPIs',
'Customize dashboard layout',
'Export dashboard data',
'Share dashboard with team members',
'Set up automated reports'
],
'technical_requirements': [
'Implement caching for performance',
'Set up real-time data pipeline'
]
}
def main():
import sys
generator = UserStoryGenerator()
if len(sys.argv) > 1 and sys.argv[1] == 'sprint':
# Generate sprint planning
capacity = int(sys.argv[2]) if len(sys.argv) > 2 else 30
# Create sample backlog
epic = create_sample_epic()
backlog = generator.generate_epic_stories(epic)
# Plan sprint
sprint = generator.generate_sprint_stories(capacity, backlog)
print("=" * 60)
print("SPRINT PLANNING")
print("=" * 60)
print(f"Sprint Capacity: {sprint['capacity']} points")
print(f"Committed: {sprint['total_points']} points ({sprint['utilization']}%)")
print(f"Stories: {len(sprint['committed'])} committed + {len(sprint['stretch'])} stretch")
print("\n📋 COMMITTED STORIES:\n")
for story in sprint['committed']:
print(f" [{story['priority'][:1].upper()}] {story['id']}: {story['title']} ({story['estimation']}pts)")
if sprint['stretch']:
print("\n🎯 STRETCH GOALS:\n")
for story in sprint['stretch']:
print(f" [{story['priority'][:1].upper()}] {story['id']}: {story['title']} ({story['estimation']}pts)")
else:
# Generate stories for epic
epic = create_sample_epic()
stories = generator.generate_epic_stories(epic)
print(f"Generated {len(stories)} stories from epic: {epic['name']}\n")
# Display first 3 stories in detail
for story in stories[:3]:
print(generator.format_story_output(story))
print("\n")
# Summary of all stories
print("=" * 60)
print("BACKLOG SUMMARY")
print("=" * 60)
total_points = sum(s['estimation'] for s in stories)
print(f"Total Stories: {len(stories)}")
print(f"Total Points: {total_points}")
print(f"Average Size: {total_points/len(stories):.1f} points")
print("\nPriority Breakdown:")
for priority in ['critical', 'high', 'medium', 'low']:
count = len([s for s in stories if s['priority'] == priority])
if count > 0:
print(f" {priority.capitalize()}: {count} stories")
if __name__ == "__main__":
main()
Thiết kế, ra mắt và tối ưu chương trình giới thiệu hoặc affiliate: refer a friend, ưu đãi, đại sứ thương hiệu, chương trình đối tác.
---
name: "referral-program"
description: "When the user wants to design, launch, or optimize a referral or affiliate program. Use when they mention 'referral program,' 'affiliate program,' 'word of mouth,' 'refer a friend,' 'incentive program,' 'customer referrals,' 'brand ambassador,' 'partner program,' 'referral link,' or 'growth through referrals.' Covers program mechanics, incentive design, and optimization — not just the idea of referrals but the actual system."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: marketing
updated: 2026-03-06
---
# Referral Program
You are a growth engineer who has designed referral and affiliate programs for SaaS companies, marketplaces, and consumer apps. You know the difference between programs that compound and programs that collect dust. Your goal is to build a referral system that actually runs — one with the right mechanics, triggers, incentives, and measurement to make customers do your acquisition for you.
## 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 & Customer
- What are you selling? (SaaS, marketplace, service, ecommerce)
- Who is your ideal customer and what do they love about your product?
- What's your average LTV? (This determines incentive ceiling)
- What's your current CAC via other channels?
### 2. Program Goals
- What outcome do you want? (More signups, more revenue, brand reach)
- Is this B2C or B2B? (Different mechanics apply)
- Do you want customers referring customers, or partners promoting your product?
### 3. Current State (if optimizing)
- What program exists today?
- What are the key metrics? (Referral rate, conversion rate, active referrers %)
- What's the reward structure?
- Where does the loop break down?
---
## How This Skill Works
### Mode 1: Design a New Program
Starting from scratch. Build the full referral program — loop, incentives, triggers, and measurement.
**Workflow:**
1. Define the referral loop (4 stages)
2. Choose program type (customer referral vs. affiliate)
3. Design the incentive structure (what, when, for whom)
4. Identify trigger moments (when to ask for referrals)
5. Plan the share mechanics (how referrals actually happen)
6. Define measurement framework
### Mode 2: Optimize an Existing Program
You have something running but it's underperforming. Diagnose where the loop breaks.
**Workflow:**
1. Audit current metrics against benchmarks
2. Identify the specific weak point (low awareness, low share rate, low conversion, reward friction)
3. Run a focused fix — don't redesign everything at once
4. Measure the impact before moving to the next lever
### Mode 3: Launch an Affiliate Program
Different from customer referrals. Affiliates are external promoters — bloggers, influencers, complementary SaaS, industry newsletters — motivated by commission, not loyalty.
**Workflow:**
1. Define affiliate tiers and commission structure
2. Identify and recruit initial affiliate partners
3. Build the affiliate toolkit (links, assets, copy)
4. Set tracking and payout mechanics
5. Onboard and activate your first 10 affiliates
---
## Referral vs. Affiliate — Choose the Right Mechanism
| | Customer Referral | Affiliate Program |
|---|---|---|
| **Who promotes** | Your existing customers | External partners, publishers, influencers |
| **Motivation** | Loyalty, reward, social currency | Commission, audience alignment |
| **Best for** | B2C, prosumer, SMB SaaS | B2B SaaS, high LTV products, content-heavy niches |
| **Activation** | Triggered by aha moment, milestone | Recruited proactively, onboarded |
| **Payout timing** | Account credit, discount, cash reward | Revenue share or flat fee per conversion |
| **CAC impact** | Low — reward < CAC | Variable — commission % determines |
| **Scale** | Scales with user base | Scales with partner recruitment |
**Rule of thumb:** If your customers are enthusiastic and social, start with customer referrals. If your customers are businesses buying on behalf of a team, start with affiliates.
---
## The Referral Loop
Every referral program runs on the same 4-stage loop. If any stage is weak, the loop breaks.
```
[Trigger Moment] → [Share Action] → [Referred User Converts] → [Reward Delivered] → [Loop]
```
### Stage 1: Trigger Moment
This is when you ask customers to refer. Timing is everything.
**High-signal trigger moments:**
- **After aha moment** — when the customer first experiences core value (not at signup — too early)
- **After a milestone** — "You just saved your 100th hour" / "Your 10th team member joined"
- **After great support** — post-resolution NPS prompt → if 9-10, ask for referral
- **After renewal** — customers who renew are telling you they're satisfied
- **After a public win** — customer tweets about you → follow up with referral link
**What doesn't work:** Asking on day 1, asking in onboarding emails, asking in the footer of every email.
### Stage 2: Share Action
Remove every possible point of friction.
- Pre-filled share message (editable, not locked)
- Personal referral link (not a generic coupon code)
- Share options: email invite, link copy, social share, Slack/Teams share for B2B
- Mobile-optimized for consumer products
- One-click send — no manual copy-paste required
### Stage 3: Referred User Converts
The referred user lands on your product. Now what?
- Personalized landing ("Your friend Alex invited you — here's your bonus...")
- Incentive visible on landing page
- Referral attribution tracked from landing to conversion
- Clear CTA — don't make them hunt for what to do
### Stage 4: Reward Delivered
Reward must be fast and clear. Delayed rewards break the loop.
- Confirm reward eligibility as soon as referral signs up (not when they pay)
- Notify the referrer immediately — don't wait until month-end
- Status visible in dashboard ("2 friends joined — you've earned $40")
---
## Incentive Design
### Single-Sided vs. Double-Sided
**Single-sided** (referrer only gets rewarded): Use when your product has strong viral hooks and customers are already enthusiastic. Lower cost per referral.
**Double-sided** (both referrer and referred get rewarded): Use when you need to overcome inertia on both sides. Higher cost, higher conversion. Dropbox made this famous.
**Rule:** If your referral rate is <1%, go double-sided. If it's >5%, single-sided is more profitable.
### Reward Types
| Type | Best For | Examples |
|------|----------|---------|
| Account credit | SaaS / subscription | "Get $20 credit" |
| Discount | Ecommerce / usage-based | "Get 1 month free" |
| Cash | High LTV, B2C | "$50 per referral" |
| Feature unlock | Freemium | "Unlock advanced analytics" |
| Status / recognition | Community / loyalty | "Ambassador status, exclusive badge" |
| Charity donation | Enterprise / mission-driven | "$25 to a cause you choose" |
**Sizing rule:** Reward should be ≥10% of first month's value for account credit. For cash, cap at 30% of first payment. Run `scripts/referral_roi_calculator.py` to model reward sizing against your LTV and CAC.
### Tiered Rewards (Gamification)
When you want referrers to go from 1 referral to 10:
```
1 referral → $20 credit
3 referrals → $75 credit (25/referral) + bonus feature
10 referrals → $300 cash + ambassador status
```
Keep tiers simple. Three levels maximum. Each tier should feel meaningfully better, not just slightly better.
---
## Optimization Levers
Don't optimize randomly. Diagnose first, then pull the right lever.
| Metric | Benchmark | If Below Benchmark |
|--------|-----------|-------------------|
| Referral program awareness | >40% of active users know it exists | Promote in-app, post-activation emails |
| Active referrers (%) | 5–15% of active user base | Improve trigger moments and visibility |
| Referral share rate | 20–40% of those who see it share | Simplify share flow, improve messaging |
| Referred conversion rate | 15–25% (vs. 5-10% organic) | Improve referred landing page, add incentive |
| Reward redemption rate | >70% within 30 days | Reduce friction, send reminders |
### Improving Referral Rate
- Move the trigger moment earlier (after aha, not after 90 days)
- Add referral prompt to success states ("You just hit 1,000 contacts — share this with a colleague?")
- Surface the program in the product dashboard, not just in emails
- Test double-sided vs. single-sided rewards
### Improving Referred User Conversion
- Personalize the landing page ("Invited by [Name]")
- Show the referred user their specific benefit above the fold
- Reduce signup friction — if they're referred, they're warm; don't make them jump through hoops
- A/B test the referral landing page like a paid traffic landing page
---
## Key Metrics
Track these weekly:
| Metric | Formula | Why It Matters |
|--------|---------|----------------|
| Referral rate | Referrals sent / active users | Health of the program |
| Active referrers % | Users who sent ≥1 referral / total active users | Engagement depth |
| Referral conversion rate | Referrals that converted / referrals sent | Quality of referred traffic |
| CAC via referral | Reward cost / new customers via referral | Program economics vs. other channels |
| Referral revenue contribution | Revenue from referred customers / total revenue | Business impact |
| Virality coefficient (K) | Referrals per user × conversion rate | K >1 = viral growth |
See [references/measurement-framework.md](references/measurement-framework.md) for benchmarks by industry and optimization playbook.
---
## Affiliate Program Launch Checklist
If launching an affiliate program specifically:
**Before Launch**
- [ ] Commission structure defined (% of revenue or flat fee per conversion)
- [ ] Cookie window set (30 days minimum, 90 days for B2B)
- [ ] Affiliate tracking platform selected (Impact, ShareASale, Rewardful, PartnerStack, or custom)
- [ ] Affiliate agreement drafted (legal review recommended)
- [ ] Payment terms clear (threshold, frequency, method)
**Partner Toolkit**
- [ ] Unique tracking links for each affiliate
- [ ] Pre-written copy and email swipes
- [ ] Approved images and banner ads
- [ ] Product explanation sheet (what to tell their audience)
- [ ] Landing page optimized for affiliate traffic
**Recruitment**
- [ ] List of 50 target affiliates (complementary SaaS, newsletters, bloggers, agencies)
- [ ] Personalized outreach — not a generic "join our affiliate program" email
- [ ] 10-affiliate pilot before scaling
See [references/program-mechanics.md](references/program-mechanics.md) for detailed program patterns and real-world examples.
---
## Proactive Triggers
Surface these without being asked:
- **Asking at signup** → Flag immediately. Asking a new user to refer before they've experienced value is a conversion killer. Move trigger to post-aha moment.
- **Reward too small relative to LTV** → If reward is <5% of LTV and referral rate is low, the math is broken. Surface the sizing issue.
- **No reward notification system** → If referred users convert but referrers aren't notified immediately, the loop breaks. Flag the need for instant notification.
- **Generic share message** → Pre-filled messages that sound like marketing copy get deleted. Flag and rewrite in first-person customer voice.
- **No attribution after the landing page** → If referral tracking stops at first visit but conversion requires multiple sessions, referral is being undercounted. Flag tracking gap.
- **Affiliate program without a partner kit** → If affiliates don't have approved copy and assets, they'll promote inaccurately or not at all. Flag before launch.
---
## Output Artifacts
| When you ask for... | You get... |
|---------------------|------------|
| "Design a referral program" | Full program spec: loop design, incentive structure, trigger moments, share mechanics, measurement plan |
| "Audit our referral program" | Metric scorecard vs. benchmarks, weak link diagnosis, prioritized optimization plan |
| "Model our incentive options" | ROI comparison of 3-5 reward structures using your LTV and CAC data |
| "Write referral program copy" | In-app prompts, referral email, referred user landing page headline, share messages |
| "Launch an affiliate program" | Launch checklist, commission structure recommendation, partner recruitment list template, affiliate kit outline |
| "What should our K-factor be?" | Virality model with your numbers — current K, target K, what needs to change to get there |
---
## Communication
All output follows the structured communication standard:
- **Bottom line first** — answer before explanation
- **Numbers-grounded** — every recommendation tied to your LTV/CAC inputs
- **Confidence tagging** — 🟢 verified / 🟡 medium / 🔴 assumed
- **Actions have owners** — "define reward structure" → assign an owner and timeline
---
## Related Skills
- **launch-strategy**: Use when planning the go-to-market for a product launch. NOT for building a referral program (different mechanics, different timeline).
- **email-sequence**: Use when building the email flow that supports the referral program (trigger emails, reward notifications). NOT for the program design itself.
- **marketing-demand-acquisition**: Use for multi-channel paid and organic acquisition strategy. NOT for referral-specific mechanics.
- **ab-test-setup**: Use when A/B testing referral landing pages, reward structures, or trigger messaging. NOT for the program design.
- **content-creator**: Use for creating affiliate partner content or referral-related blog posts. NOT for program mechanics.
FILE:references/measurement-framework.md
# Measurement Framework — Referral Program Metrics, Benchmarks, and Optimization Playbook
The metrics that tell you if your referral program is working, what's broken, and what to fix first.
---
## The Core Metric Stack
Track these weekly. Everything else is secondary.
| Metric | Formula | Benchmark (SaaS) | What It Tells You |
|--------|---------|-----------------|------------------|
| Program awareness | (Users who know about program / Total active users) × 100 | >40% | Are you even promoting it? |
| Active referrer rate | (Users who sent ≥1 referral / Total active users) × 100 | 5–15% | How many users are actually participating |
| Referrals sent per active referrer | Total referrals / Active referrers | 2–5 per period | How motivated referrers are |
| Referral conversion rate | (Referrals that converted / Referrals sent) × 100 | 15–30% | Quality of referred traffic |
| Reward redemption rate | (Rewards redeemed / Rewards issued) × 100 | >70% | Is the reward actually desirable? |
| CAC via referral | Total reward cost / New customers via referral | <50% of channel CAC | Program efficiency |
| K-factor (virality coefficient) | Referrals per user × Referral conversion rate | >0.5 for meaningful growth | Is it self-sustaining? |
---
## Benchmarks by Stage and Model
### Early-Stage SaaS (<$1M ARR)
| Metric | Expected | Strong |
|--------|---------|--------|
| Active referrer rate | 2–5% | >8% |
| Referral conversion rate | 10–20% | >25% |
| CAC via referral vs. paid | 30–50% of paid CAC | <25% of paid CAC |
### Growth-Stage SaaS ($1M–$10M ARR)
| Metric | Expected | Strong |
|--------|---------|--------|
| Active referrer rate | 5–10% | >12% |
| Referral contribution to new signups | 10–20% | >25% |
| Referral contribution to revenue | 5–15% | >20% |
### Consumer / Prosumer Products
| Metric | Expected | Strong |
|--------|---------|--------|
| Active referrer rate | 8–20% | >25% |
| Referral conversion rate | 20–40% | >50% (with double-sided reward) |
| K-factor | 0.3–0.7 | >1.0 (true viral loop) |
### B2B Mid-Market (ACV $10k+)
| Metric | Expected | Strong |
|--------|---------|--------|
| Active referrer rate | 3–8% | >10% |
| Referral conversion rate | 20–40% (warm intros convert higher) | >50% |
| Average deal size via referral vs. standard | Similar | 20–40% higher (trust shortens negotiation) |
---
## Diagnosing the Broken Stage
### Diagnosis Framework
```
Is referral rate low?
└── Is awareness low? → Promote the program
└── Is trigger placement wrong? → Move to better moment
└── Is reward insufficient? → Test higher reward
└── Is share flow too complex? → Simplify
Is referral conversion low?
└── Is the landing page cold? → Personalize for referred users
└── Is the incentive for the referred user unclear? → Make it above the fold
└── Is signup friction high? → Reduce required fields
Is reward redemption low?
└── Is reward notification delayed? → Send immediately on qualifying event
└── Is reward type wrong? → Test cash vs. credit vs. feature unlock
└── Is the redemption process complex? → Auto-apply credits, remove steps
```
---
## The Optimization Playbook
Work in this order. Don't try to fix everything at once.
### Phase 1: Foundation (Month 1)
**Goal:** Get to baseline awareness and share rate.
1. Audit whether users know the program exists
2. Add in-app promotion: dashboard banner, post-activation prompt, success state trigger
3. Add referral program to the weekly/monthly activation email
4. Ensure share flow works on mobile
**Success gate:** Program awareness >30%, Active referrer rate >3%
### Phase 2: Trigger Optimization (Month 2)
**Goal:** Ask at the right moment, not just any moment.
1. Map all current trigger points
2. Move or add trigger to first aha moment (define aha moment first)
3. A/B test: trigger after aha vs. trigger after 7-day retention
4. Add NPS-linked trigger: score of 9-10 → immediate referral ask
**Success gate:** Active referrer rate increases by 30% over Phase 1
### Phase 3: Incentive Tuning (Month 3)
**Goal:** Right reward, right timing, right delivery.
1. Survey churned referrers — why did they stop?
2. Test single-sided vs. double-sided if not already tested
3. Test reward type: credit vs. cash vs. feature unlock
4. Add reward status widget to dashboard: "You've earned $X. [View details]"
5. Reduce reward payout delay — reward immediately on qualifying event, not month-end
**Success gate:** Reward redemption rate >70%, CAC via referral <40% of paid CAC
### Phase 4: Conversion of Referred Users (Month 4)
**Goal:** Referred users should convert at 2× organic rate.
1. Personalize referred user landing page (use referrer name if available)
2. Highlight referred user's incentive above the fold — don't bury it
3. A/B test: direct to product vs. direct to dedicated referral landing page
4. Add "referred by" onboarding track: faster to aha, lower time to first value
**Success gate:** Referred user conversion rate 20%+ (vs. organic baseline)
### Phase 5: Scale and Gamification (Month 5+)
**Goal:** Turn your top 5% of referrers into a real advocacy channel.
1. Identify top referrers — reach out personally
2. Offer top referrers early access, ambassador status, or product input role
3. Launch tiered reward structure
4. Quarterly referral challenges: "Top 10 referrers this quarter win X"
---
## CAC via Referral — Full Calculation
```
CAC via referral = (Reward cost per referral × Successful referrals) + Program overhead costs
───────────────────────────────────────────────────────────────────────────
New customers acquired via referral
Where:
- Reward cost per referral = referrer reward + referred user reward
- Program overhead = platform cost + engineering time + support time (amortized)
- Successful referrals = referrals that converted to paying customer
```
**Example:**
- 200 referrals sent → 40 conversions (20% conversion rate)
- Referrer reward: $30 per successful referral
- Referred user reward: $20 (discount on first month)
- Platform cost: $100/mo, engineering: $500/mo (amortized) → $600/mo overhead
- Program overhead per conversion: $600 / 40 = $15
**CAC via referral** = ($30 + $20) × 40 + $600 / 40 = **$65 per customer**
Compare to paid CAC, and you know if the program is worth it.
Use `scripts/referral_roi_calculator.py` to model this for your numbers.
---
## Affiliate-Specific Metrics
| Metric | Formula | Benchmark |
|--------|---------|-----------|
| Active affiliate rate | Active affiliates / Enrolled affiliates | 20–40% |
| Revenue per active affiliate | Total affiliate revenue / Active affiliates | Varies by niche |
| Affiliate-driven CAC | Commission paid / New customers via affiliate | Should be <standard CAC |
| Top affiliate concentration | Revenue from top 20% of affiliates | Normal if 80%+ of revenue from top 20% |
| Average cookie-to-conversion time | Days from click to first payment | Benchmark against your sales cycle |
**Warning sign:** If >80% of affiliate revenue is from 1–2 partners, you have concentration risk. One partner leaving could tank the channel overnight. Diversify proactively.
---
## Reporting Template
Weekly referral program summary:
```
REFERRAL PROGRAM — Week of [DATE]
Active referrers: X (↑/↓ vs. last week)
Referrals sent: X
Conversions: X (rate: X%)
Rewards issued: $X
New customers via referral: X
CAC via referral: $X (vs. $X paid CAC)
TOP THIS WEEK:
- [Name/segment] sent 12 referrals, 4 converted
- [Trigger optimization test] is showing +18% referrer rate
ISSUES:
- [What's broken and the plan to fix it]
NEXT ACTION:
- [One thing we're doing this week to improve the program]
```
FILE:references/program-mechanics.md
# Program Mechanics — Referral and Affiliate Design Patterns
Detailed design patterns with real-world examples. Use this as a reference when designing programs — these are the mechanics that separate programs with 10% referral rates from ones with 0.5%.
---
## The Two Fundamental Program Types
### Type A: Customer-to-Customer Referral
Your best customers refer their peers. Classic example: Dropbox, Airbnb, Uber.
**Core mechanics:**
- Referral link generated per user
- Reward given when referred user completes a qualifying action (sign up, first purchase, first month paid)
- Referrer sees their dashboard: links sent, signed up, rewards earned
**What makes it work:**
- Existing customer trust transfers. Being referred by someone you trust removes 80% of purchase skepticism.
- The referrer's reputation is on the line — they only refer people they think will benefit
- Natural social proof at the moment of conversion
### Type B: Partner / Affiliate Program
External publishers, influencers, agencies, or complementary SaaS tools promote you in exchange for commission.
**Core mechanics:**
- Unique affiliate link or coupon per partner
- Attribution tracked via cookie (30-90 day window typical)
- Payout on qualifying events (first payment, monthly recurring, flat fee)
**What makes it work:**
- Partners have existing audiences who trust them
- Content-driven promotion outlasts a single ad — a blog post with your affiliate link can generate leads for 3 years
- Commission-aligned incentives mean partners promote more when you convert better
---
## Real-World Program Patterns
### Pattern 1: Double-Sided Reward (Dropbox Model)
**How it worked:** Refer a friend = 500MB for you + 500MB for them.
**Why it worked:**
- Both sides had skin in the game
- The reward was intrinsic to the product (not a discount on something unrelated)
- The referred user's incentive made them more likely to complete registration
- Referrer felt generous, not transactional
**When to use:** When your core product has a natural "shareable" dimension. Digital products with quantity-based rewards (storage, credits, messages, seats) are perfect candidates.
**When NOT to use:** When your product has no natural unit to give. Don't give $10 Amazon gift cards just to copy Dropbox — tie the reward to product value.
---
### Pattern 2: Tiered Ambassador Program (Referral + Status)
**How it works:** Customers unlock higher reward tiers by referring more users. Top tier gets named ambassador status, exclusive access, or direct relationship with the company.
**Example structure:**
```
Bronze (1-2 referrals): $20 credit per referral
Silver (3-9 referrals): $30 credit per referral + priority support
Gold (10+ referrals): $50 credit per referral + product advisory board invite + named case study
```
**Why it works:** For highly enthusiastic customers, status beats cash. Naming someone an "ambassador" triggers identity — they become advocates rather than just referrers.
**When to use:** Strong community around the product. Developer tools, creative SaaS, agency tools where practitioners identify with the category.
---
### Pattern 3: Milestone Trigger (Conditional Reward)
**How it works:** Reward is not given at signup — it's given when the referred user reaches a specific milestone.
**Example:**
- "Your friend gets $50 when they make their first withdrawal"
- "You get 1 free month when your referral upgrades to a paid plan"
**Why it works:** Referred users are incentivized to actually use the product to unlock the reward. Referrers are incentivized to encourage their referral to stay active. Reduces reward fraud (fake accounts).
**When to use:** High-volume consumer products where gaming the system is a real risk. Financial services, marketplaces, usage-based products.
---
### Pattern 4: Cohort-Based Referral Window
**How it works:** Referral rewards expire if the referred user doesn't convert within a set window.
**Standard windows:**
- B2C: 7–14 days (high intent = fast decision)
- B2B SMB: 30 days
- B2B Enterprise: 90+ days (longer evaluation cycles)
**Why it matters:** Open-ended referral attribution creates accounting complexity and gaming risk. Time-bounded windows create urgency and clean accounting.
---
### Pattern 5: Affiliate Commission Tiers by Partner Type
Not all affiliates are equal. Tiering by partner type lets you reward your best partners appropriately.
**Example tier structure:**
```
Standard affiliates (bloggers, small newsletters):
└── 20% of first payment, 30-day cookie
Premium affiliates (high-traffic publications, active agencies):
└── 25% MRR for 12 months, 60-day cookie, co-marketing support
Strategic partners (complementary SaaS, resellers):
└── 30% MRR ongoing, white-label option, dedicated account manager
```
**Key principle:** The higher the traffic quality and deal size, the higher the commission can go. An agency that sends you 5 enterprise deals per year is worth more than 100 bloggers who send you occasional trials.
---
### Pattern 6: Product-Embedded Referral (Virality by Design)
The referral mechanism is built into the product experience, not bolted on as a "refer a friend" email.
**Examples:**
- Calendar invite: "Powered by [Product]" link in email footer that every invitee sees
- "Created with [Product]" watermark on exported documents (Canva, Notion)
- "Invite your team" prompt mid-onboarding with a clear reason to do it now
- "Share your results" on high-value output screens
**Why it works:** The referral happens at the moment of peak product value, using the product itself as the promotional vehicle. No separate "referral program" needed.
**When to use:** Productivity tools, creative tools, any product that produces shareable output. Build this alongside the product, not as an afterthought.
---
### Pattern 7: B2B Account-Based Referral
In B2B, referrals are more targeted — you're asking for warm intros to specific account types, not a spray-and-pray link share.
**How it works:**
- Identify which customers have the broadest networks in your ICP
- Equip them with a referral kit (email template, one-pager, LinkedIn intro script)
- Reward for completed intro + reward uplift for closed deal
- Keep the referrer informed on progress (increases likelihood of them championing internally)
**Example mechanics:**
```
Step 1: Customer completes an intro call → $200 gift card
Step 2: Intro converts to a demo → $500 additional
Step 3: Demo converts to a deal → 10% of first year's contract value (capped at $5,000)
```
**Why it works:** High-trust referrals from B2B customers often shorten sales cycles dramatically. The referrer becomes an internal champion at the referred company, not just a warm lead.
---
## Share Mechanics Deep Dive
### The 3 Share Channels That Drive Volume
| Channel | How It Works | Best For |
|---------|------------|---------|
| Personal referral link | User copies/shares their link to a friend | Universal |
| Direct email invite | User enters friend's email, platform sends invite on their behalf | Consumer, prosumer |
| Social share | One-click to Twitter, LinkedIn, WhatsApp with pre-filled message | Consumer, community products |
### Pre-Written Share Messages — What Works
**Works:**
> "I've been using [Product] for 3 months and it's saved me hours on [specific task]. You can get started free using my link: [link]"
**Doesn't work:**
> "Check out this amazing product I use! [link]"
The difference: specificity and personal endorsement. Pre-fill your share messages with the actual benefit, not generic praise. Make it easy for users to be specific advocates, not just sharers.
---
## Fraud Prevention
Referral fraud happens when users game the system (fake accounts, self-referrals, incentivized referrals).
**Minimum safeguards:**
- Email verification required before reward is credited
- Device fingerprinting to detect same-device self-referral
- Reward withheld until referred user completes a qualifying action (first payment, 7-day active use)
- Rate limiting on referral link sends per user
**Warning signs of fraud:**
- Referral conversion rate suddenly spikes above 60% (normal is 15–30%)
- High number of referrals from a single user (>20 in a week)
- Referrals with similar email patterns or same IP block
---
## Technology Options
### For Customer Referral Programs
| Tool | Best For | Pricing Tier |
|------|---------|-------------|
| ReferralHero | SMB SaaS, waitlist referral | $49–$199/mo |
| Viral Loops | Consumer apps, e-commerce | $49–$199/mo |
| Referral Rock | Mid-market SaaS | $175–$800/mo |
| Custom (in-house) | When you want full control + have engineering | Build cost only |
### For Affiliate Programs
| Tool | Best For | Notes |
|------|---------|-------|
| Rewardful | SaaS, Stripe-based | $49–$299/mo, easiest Stripe integration |
| PartnerStack | B2B SaaS | $500+/mo, best for partner tiers |
| Impact | Enterprise, multi-channel | Custom pricing |
| ShareASale | E-commerce, consumer | 20% of commissions + fees |
### For Product-Embedded Viral Loops
Build these in-house. The "powered by" footer, "created with" watermark, or "invite your team" prompt needs to be native to the product experience, not a third-party widget.
FILE:scripts/referral_roi_calculator.py
#!/usr/bin/env python3
"""
referral_roi_calculator.py — Calculates referral program ROI.
Models the economics of a referral program given your LTV, CAC, referral rate,
reward cost, and conversion rate. Outputs program ROI, break-even referral rate,
and optimal reward sizing.
Usage:
python3 referral_roi_calculator.py # runs embedded sample
python3 referral_roi_calculator.py params.json # uses your params
echo '{"ltv": 1200, "cac": 300}' | python3 referral_roi_calculator.py
JSON input format:
{
"ltv": 1200, # Customer Lifetime Value ($)
"cac": 300, # Current avg CAC via paid channels ($)
"active_users": 500, # Active users who could refer
"referral_rate": 0.05, # % of active users who refer each month (0.05 = 5%)
"referrals_per_referrer": 2.5, # Avg referrals sent per active referrer
"referral_conversion_rate": 0.20, # % of referrals who become customers
"referrer_reward": 50, # Reward paid to referrer per successful referral ($)
"referred_reward": 30, # Reward paid to referred user (0 if single-sided) ($)
"program_overhead_monthly": 200, # Platform + ops cost per month ($)
"churn_rate_monthly": 0.03, # Monthly churn rate (used for LTV validation)
"months_to_model": 12 # How many months to project
}
"""
import json
import sys
from collections import OrderedDict
# ---------------------------------------------------------------------------
# Core calculation functions
# ---------------------------------------------------------------------------
def calculate_referrals_per_month(params):
"""How many successful referrals per month?"""
active_users = params["active_users"]
referral_rate = params["referral_rate"]
referrals_per_referrer = params["referrals_per_referrer"]
conversion_rate = params["referral_conversion_rate"]
active_referrers = active_users * referral_rate
referrals_sent = active_referrers * referrals_per_referrer
conversions = referrals_sent * conversion_rate
return {
"active_referrers": round(active_referrers, 1),
"referrals_sent": round(referrals_sent, 1),
"new_customers_per_month": round(conversions, 1),
}
def calculate_monthly_program_cost(params, new_customers_per_month):
"""Total cost of running the program for one month."""
reward_per_conversion = params["referrer_reward"] + params["referred_reward"]
reward_cost = reward_per_conversion * new_customers_per_month
overhead = params["program_overhead_monthly"]
return {
"reward_cost": round(reward_cost, 2),
"overhead_cost": round(overhead, 2),
"total_cost": round(reward_cost + overhead, 2),
"reward_per_conversion": round(reward_per_conversion, 2),
}
def calculate_monthly_revenue(params, new_customers_per_month):
"""Revenue generated from referred customers in the first month."""
# First-month value is LTV / (1 / monthly_churn) = LTV * monthly_churn
# Simplified: use LTV * monthly_churn as first-month expected revenue contribution
# More conservative: just count as one acquisition with full LTV expected
ltv = params["ltv"]
revenue = new_customers_per_month * ltv
return round(revenue, 2)
def calculate_cac_via_referral(cost_data, new_customers_per_month):
if new_customers_per_month == 0:
return float('inf')
return round(cost_data["total_cost"] / new_customers_per_month, 2)
def calculate_break_even_referral_rate(params):
"""
What referral rate do we need so that CAC via referral equals
reward_per_conversion + overhead_per_customer_amortized?
We want: total_cost / new_customers = cac_target
Solving for referral_rate where cac_target = 50% of paid CAC (our target)
"""
target_cac = params["cac"] * 0.5 # goal: 50% of current CAC
ltv = params["ltv"]
active_users = params["active_users"]
referrals_per_referrer = params["referrals_per_referrer"]
conversion_rate = params["referral_conversion_rate"]
reward_per_conversion = params["referrer_reward"] + params["referred_reward"]
overhead = params["program_overhead_monthly"]
# CAC_referral = (reward × conversions + overhead) / conversions
# = reward + overhead/conversions
# Solve: target_cac = reward + overhead / (active_users × rate × referrals_per_referrer × conversion_rate)
# conversions_needed = overhead / (target_cac - reward)
if target_cac <= reward_per_conversion:
return None # impossible — reward alone exceeds target CAC
conversions_needed = overhead / (target_cac - reward_per_conversion)
referral_rate_needed = conversions_needed / (active_users * referrals_per_referrer * conversion_rate)
return round(referral_rate_needed, 4)
def calculate_optimal_reward(params):
"""
What's the maximum reward you can afford while keeping CAC via referral
under 60% of paid CAC?
max_total_reward = 0.60 × paid_CAC (using conversion-amortized overhead)
"""
target_cac = params["cac"] * 0.60
overhead_amortized = params["program_overhead_monthly"] / max(
calculate_referrals_per_month(params)["new_customers_per_month"], 1
)
max_reward = target_cac - overhead_amortized
# Split recommendation: 60% referrer, 40% referred (double-sided)
referrer_portion = round(max_reward * 0.60, 2)
referred_portion = round(max_reward * 0.40, 2)
return {
"max_total_reward": round(max(max_reward, 0), 2),
"recommended_referrer_reward": max(referrer_portion, 0),
"recommended_referred_reward": max(referred_portion, 0),
"reward_as_pct_ltv": round((max_reward / params["ltv"]) * 100, 1) if params["ltv"] > 0 else 0,
}
def calculate_roi(params):
"""
Program ROI over the modeling period.
ROI = (Revenue from referred customers - Program costs) / Program costs
"""
months = params["months_to_model"]
monthly = calculate_referrals_per_month(params)
new_customers = monthly["new_customers_per_month"]
costs = calculate_monthly_program_cost(params, new_customers)
total_cost = costs["total_cost"] * months
total_ltv_generated = new_customers * params["ltv"] * months
net_benefit = total_ltv_generated - total_cost
roi = (net_benefit / total_cost * 100) if total_cost > 0 else 0
return {
"total_cost": round(total_cost, 2),
"total_ltv_generated": round(total_ltv_generated, 2),
"net_benefit": round(net_benefit, 2),
"roi_pct": round(roi, 1),
}
def build_monthly_projection(params):
"""Build a month-by-month projection table."""
months = params["months_to_model"]
monthly = calculate_referrals_per_month(params)
new_per_month = monthly["new_customers_per_month"]
costs = calculate_monthly_program_cost(params, new_per_month)
ltv = params["ltv"]
rows = []
cumulative_customers = 0
cumulative_cost = 0
cumulative_revenue = 0
for m in range(1, months + 1):
cumulative_customers += new_per_month
month_cost = costs["total_cost"]
month_revenue = new_per_month * ltv
cumulative_cost += month_cost
cumulative_revenue += month_revenue
cumulative_net = cumulative_revenue - cumulative_cost
rows.append({
"month": m,
"new_customers": round(new_per_month, 1),
"cumulative_customers": round(cumulative_customers, 1),
"monthly_cost": round(month_cost, 2),
"cumulative_cost": round(cumulative_cost, 2),
"monthly_ltv": round(month_revenue, 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
# ---------------------------------------------------------------------------
# Formatting
# ---------------------------------------------------------------------------
def format_currency(value):
return f",.2f"
def format_pct(value):
return f"{value:.1f}%"
def print_report(params, results):
monthly = results["monthly_referrals"]
costs = results["monthly_costs"]
cac = results["cac_via_referral"]
roi = results["roi"]
break_even_rate = results["break_even_referral_rate"]
optimal_reward = results["optimal_reward"]
projection = results["monthly_projection"]
break_even_month = results["break_even_month"]
paid_cac = params["cac"]
ltv = params["ltv"]
print("\n" + "=" * 60)
print("REFERRAL PROGRAM ROI CALCULATOR")
print("=" * 60)
print("\n📊 INPUT PARAMETERS")
print(f" LTV per customer: {format_currency(ltv)}")
print(f" Current paid CAC: {format_currency(paid_cac)}")
print(f" Active users: {params['active_users']:,}")
print(f" Referral rate (monthly): {format_pct(params['referral_rate'] * 100)}")
print(f" Referrals per referrer: {params['referrals_per_referrer']}")
print(f" Referral conversion rate: {format_pct(params['referral_conversion_rate'] * 100)}")
print(f" Referrer reward: {format_currency(params['referrer_reward'])}")
print(f" Referred user reward: {format_currency(params['referred_reward'])}")
print(f" Program overhead/month: {format_currency(params['program_overhead_monthly'])}")
print("\n📈 MONTHLY PERFORMANCE (STEADY STATE)")
print(f" Active referrers/month: {monthly['active_referrers']}")
print(f" Referrals sent/month: {monthly['referrals_sent']}")
print(f" New customers/month: {monthly['new_customers_per_month']}")
print(f" Monthly program cost: {format_currency(costs['total_cost'])}")
print(f" ↳ Reward cost: {format_currency(costs['reward_cost'])}")
print(f" ↳ Overhead: {format_currency(costs['overhead_cost'])}")
print(f" CAC via referral: {format_currency(cac)}")
print(f" Paid CAC: {format_currency(paid_cac)}")
savings_pct = ((paid_cac - cac) / paid_cac * 100) if paid_cac > 0 else 0
savings_label = f"{savings_pct:.0f}% cheaper than paid" if cac < paid_cac else "⚠️ More expensive than paid"
print(f" CAC comparison: {savings_label}")
print(f"\n💰 ROI OVER {params['months_to_model']} MONTHS")
print(f" Total program cost: {format_currency(roi['total_cost'])}")
print(f" Total LTV generated: {format_currency(roi['total_ltv_generated'])}")
print(f" Net benefit: {format_currency(roi['net_benefit'])}")
print(f" Program ROI: {format_pct(roi['roi_pct'])}")
if break_even_month:
print(f" Break-even: Month {break_even_month}")
else:
print(f" Break-even: Not reached in {params['months_to_model']} months")
print("\n🎯 OPTIMIZATION INSIGHTS")
if break_even_rate:
current_rate = params["referral_rate"]
rate_gap = break_even_rate - current_rate
if rate_gap > 0:
print(f" Break-even referral rate: {format_pct(break_even_rate * 100)} "
f"(you're at {format_pct(current_rate * 100)} — need +{format_pct(rate_gap * 100)})")
else:
print(f" Break-even referral rate: {format_pct(break_even_rate * 100)} ✅ Already above break-even")
else:
print(f" Break-even referral rate: ⚠️ Reward alone exceeds target CAC — reduce reward or increase LTV")
print(f"\n Optimal reward sizing (to keep CAC at ≤60% of paid CAC):")
print(f" Max total reward/referral: {format_currency(optimal_reward['max_total_reward'])}")
print(f" Recommended referrer: {format_currency(optimal_reward['recommended_referrer_reward'])}")
print(f" Recommended referred user: {format_currency(optimal_reward['recommended_referred_reward'])}")
print(f" Reward as % of LTV: {format_pct(optimal_reward['reward_as_pct_ltv'])}")
current_total_reward = params["referrer_reward"] + params["referred_reward"]
if current_total_reward > optimal_reward["max_total_reward"] and optimal_reward["max_total_reward"] > 0:
print(f" ⚠️ Your current reward ({format_currency(current_total_reward)}) "
f"exceeds optimal ({format_currency(optimal_reward['max_total_reward'])})")
elif optimal_reward["max_total_reward"] > 0:
print(f" ✅ Your current reward ({format_currency(current_total_reward)}) is within optimal range")
print(f"\n📅 MONTHLY PROJECTION (first {min(6, len(projection))} months)")
print(f" {'Month':>5} {'New Cust':>9} {'Cumul Cust':>11} {'Monthly Cost':>13} {'Cumul Net':>11}")
print(f" {'-'*5} {'-'*9} {'-'*11} {'-'*13} {'-'*11}")
for row in projection[:6]:
net_str = format_currency(row["cumulative_net"])
if row["cumulative_net"] < 0:
net_str = f"({format_currency(abs(row['cumulative_net']))})"
print(f" {row['month']:>5} {row['new_customers']:>9.1f} {row['cumulative_customers']:>11.1f} "
f"{format_currency(row['monthly_cost']):>13} {net_str:>11}")
print("\n" + "=" * 60)
# ---------------------------------------------------------------------------
# Default parameters + sample
# ---------------------------------------------------------------------------
DEFAULT_PARAMS = {
"ltv": 1200,
"cac": 350,
"active_users": 800,
"referral_rate": 0.06,
"referrals_per_referrer": 2.0,
"referral_conversion_rate": 0.20,
"referrer_reward": 50,
"referred_reward": 30,
"program_overhead_monthly": 200,
"churn_rate_monthly": 0.04,
"months_to_model": 12,
}
def run(params):
monthly = calculate_referrals_per_month(params)
new_customers = monthly["new_customers_per_month"]
costs = calculate_monthly_program_cost(params, new_customers)
cac = calculate_cac_via_referral(costs, new_customers)
break_even_rate = calculate_break_even_referral_rate(params)
optimal_reward = calculate_optimal_reward(params)
roi = calculate_roi(params)
projection = build_monthly_projection(params)
break_even_month = find_break_even_month(projection)
results = {
"monthly_referrals": monthly,
"monthly_costs": costs,
"cac_via_referral": cac,
"break_even_referral_rate": break_even_rate,
"optimal_reward": optimal_reward,
"roi": roi,
"monthly_projection": projection,
"break_even_month": break_even_month,
}
return results
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
import argparse
parser = argparse.ArgumentParser(
description="Calculates referral program ROI. "
"Models economics given LTV, CAC, referral rate, reward cost, "
"and conversion rate."
)
parser.add_argument(
"file", nargs="?", default=None,
help="Path to a JSON file with referral program 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 in defaults for any missing keys
for k, v in DEFAULT_PARAMS.items():
params.setdefault(k, v)
results = run(params)
print_report(params, results)
# JSON output
json_output = {
"inputs": params,
"results": {
"monthly_new_customers": results["monthly_referrals"]["new_customers_per_month"],
"cac_via_referral": results["cac_via_referral"],
"program_roi_pct": results["roi"]["roi_pct"],
"break_even_month": results["break_even_month"],
"break_even_referral_rate": results["break_even_referral_rate"],
"optimal_total_reward": results["optimal_reward"]["max_total_reward"],
"net_benefit_12mo": results["roi"]["net_benefit"],
}
}
print("\n--- JSON Output ---")
print(json.dumps(json_output, indent=2))
if __name__ == "__main__":
main()
Chất vấn 6 câu hỏi theo điều luật GDPR trước rà soát nội bộ hằng năm, sau vi phạm, khi cơ quan điều tra hoặc thẩm định M&A.
--- name: "gdpr-audit-prep" description: "/cs:gdpr-audit-prep <scope> — GDPR audit 6-question Article-cited forcing interrogation. Use before annual internal GDPR review, post-breach internal audit, DPA investigation readiness, or acquisition due diligence." --- # /cs:gdpr-audit-prep — GDPR DPO Forcing Questions **Command:** `/cs:gdpr-audit-prep <scope>` The GDPR DPO auditor pressure-tests any privacy compliance work. Six Article-cited questions before any internal audit, breach response, DPA investigation, or acquisition due diligence. ## When to Run - Before annual internal GDPR audit - Before quarterly Article 30 RoPA refresh - Before launching new high-risk processing (Article 35 DPIA required) - Post-breach (Articles 33-34) - Before DPA investigation response or supervisory authority engagement - During acquisition due diligence (target company privacy posture) - Quarterly during high-volume new-feature shipping ## The Six DPO Questions ### 1. Show me the Article 30 RoPA — with last-updated date. **Most-cited finding area.** - Must include all Article 30(1)(a)-(g) elements for controllers - Must include all Article 30(2)(a)-(d) elements for processors - Updated within reasonable time of changes (90 days expected) - Joint controller arrangements documented per Article 26 ### 2. For this processing activity, what's the lawful basis under Article 6? **Article 6 is exclusive — pick ONE basis per purpose.** - Six options: consent / contract / legal obligation / vital interests / public task / legitimate interests - Where "legitimate interests": LIA documented - Where "consent": records per Article 7; withdrawal mechanism - Special categories (Article 9) require an Article 9(2) exception ### 3. For high-risk processing, where's the DPIA per Article 35? **Required for high-risk; sample 3-5 activities.** - Article 35(7)(a)-(d) required elements: - Systematic description of processing - Necessity + proportionality assessment - Risks to rights + freedoms - Measures to address risks - DPO consulted per Article 35(2) - Article 36 prior consultation triggered for residual high risk - For AI systems: integrates with EU AI Act Article 27 FRIA (cross-check with cs-ai-act-compliance) ### 4. Show me a DSAR from the last 30 days — and the response timing. **Articles 15-22 operational workflow.** - Response within 1 month (Article 12(3)); extension up to 2 months for complex requests - Identity verification process documented - Right of access response includes all Article 15 information - Right to erasure (Article 17) workflow covers backups + processors ### 5. Show me Transfer Impact Assessments for the largest non-EU transfers. **Schrems II discipline.** - Adequacy decision OR SCCs (Article 46) OR derogation (Article 49) - TIA per EDPB Recommendations 01/2020 + 02/2020 - Supplementary measures where TIA flagged risk - US transfers covered by EU-US Data Privacy Framework adequacy (Jul 2023) — verify list of certified entities ### 6. Show me the breach log per Article 33(5) — all breaches, not just notifiable ones. **Article 33(5) requires logging ALL breaches.** - Internal breach detection mechanism documented - Article 33 DPA notification within 72 hours (where required) - Article 34 data subject notification (where high risk) - Root cause + corrective action via CAPA system - Cross-check with cs-ciso-iso27001 for A.5.24-27 incident management alignment ## Workflow ```bash # 1. Compliance posture python ../../ra-qm-team/skills/gdpr-dsgvo-expert/scripts/gdpr_compliance_checker.py compliance_state.json # 2. DPIA for high-risk activities python ../../ra-qm-team/skills/gdpr-dsgvo-expert/scripts/dpia_generator.py processing_activity.json # 3. DSAR workflow validation python ../../ra-qm-team/skills/gdpr-dsgvo-expert/scripts/data_subject_rights_tracker.py dsar_log.json # 4. Cross-framework reuse with ISO 27001 + SOC 2 + ISO 42001 python ../../skills/compliance-os/scripts/cross_framework_mapper.py program.json ``` ## Output Format ```markdown # GDPR Audit Prep: <scope> **Date:** YYYY-MM-DD **Article Citations:** Every finding cites Article + paragraph; no paraphrase. ## The Decision Being Made [RoPA-refresh | DPIA-required | DSAR-workflow | transfer-risk | breach-followup | DPA-readiness] ## Article 30 RoPA Status - Last refresh: YYYY-MM-DD - Required elements present: yes/no per processing activity - Joint controller arrangements: documented/missing ## Article 6 Lawful Basis Discipline - Activities reviewed: N - Legitimate-interests claims without LIA: <list> - Article 9 special categories with documented exception: yes/no ## Article 35 DPIA Quality - High-risk activities requiring DPIA: <list> - DPIAs complete per Article 35(7): pass/fail per activity - Article 36 prior consultation triggered: <list> ## Data Subject Rights (Articles 12-22) - DSARs in last 90 days: N - Average response time: X days (target: ≤ 30) - Right to erasure backup-processor flow: complete/incomplete ## Article 28 Processor Management - Processors reviewed: N - Contracts with all Article 28(3)(a)-(j) clauses: % complete - Sub-processor flow-down notification mechanism: yes/no ## Schrems II Transfer Status - Non-EU transfers: <list> - Mechanism per transfer: adequacy / SCCs / derogation - TIA on file: yes/no per transfer - Supplementary measures where needed: <list> ## Article 33-34 Breach Discipline - Breach log last 12 months: N - Article 33 notification timing: ≤ 72h ratio - Article 34 data subject notification (where high risk): on-time ratio ## Cross-Framework Impact - ISO 27001 Article 32 alignment: clean / gaps - EU AI Act Article 27 FRIA integration: applicable / not - SOC 2 Privacy TSC alignment (if scope): clean / gaps ## Verdict 🟢 DPA-READY | 🟡 GAPS-IDENTIFIED | 🔴 NOT-READY ## Top 3 Actions [3 concrete next steps with owner + Article-cited timeline] ## Outside Counsel Required [Article-level ambiguities flagged: Schrems II supplementary measure adequacy, EU AI Act ↔ GDPR interaction, sectoral derogation interpretation, novel DPA enforcement] ``` ## Routing - `/cs:compliance-readiness` — for multi-framework view - `/cs:iso27001-audit-prep` — for Article 32 organizational measures - `/cs:ai-act-readiness` — for EU AI Act Article 27 FRIA integration - `/cs:soc2-audit-prep` — for SOC 2 Privacy TSC overlap - `/cs:gc-review` — for novel-case legal review ## Related - Agent: [`cs-dpo-gdpr`](../../agents/cs-dpo-gdpr.md) - Skill: [`gdpr-dsgvo-expert`](../../../ra-qm-team/skills/gdpr-dsgvo-expert/SKILL.md) - Playbook: [gdpr_audit_playbook.md](../../../ra-qm-team/skills/gdpr-dsgvo-expert/references/gdpr_audit_playbook.md) - Adjacent: `../iso27001-audit-prep/`, `../ai-act-readiness/`, `../soc2-audit-prep/`, `../compliance-readiness/` --- **Version:** 1.0.0
Khung triển khai thay đổi tổ chức bằng mô hình ADKAR, mẫu truyền thông và xử lý kháng cự, mệt mỏi thay đổi.
---
name: "change-management"
description: "Framework for rolling out organizational changes without chaos. Covers the ADKAR model adapted for startups, communication templates, resistance patterns, and change fatigue management. Handles process changes, org restructures, strategy pivots, and culture changes. Use when announcing a reorg, switching tools, pivoting strategy, killing a product, changing leadership, or when user mentions change management, change rollout, managing resistance, org change, reorg, or pivot communication."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: c-level
domain: change-management
updated: 2026-03-05
frameworks: change-playbook
---
# Change Management Playbook
Most changes fail at implementation, not design. The ADKAR model tells you why and how to fix it.
## Keywords
change management, ADKAR, organizational change, reorg, process change, tool migration, strategy pivot, change resistance, change fatigue, change communication, stakeholder management, adoption, compliance, change rollout, transition
## Core Model: ADKAR Adapted for Startups
ADKAR is a change management model by Prosci. Original version is for enterprises. This is the startup-speed adaptation.
### A — Awareness
**What it is:** People understand WHY the change is happening — the business reason, not just the announcement.
**The mistake:** Communicating the WHAT before the WHY. "We're moving to a new CRM" before "here's why our current process is killing us."
**What people need to hear:**
- What is the problem we're solving? (Be honest. If it's "we need to cut costs," say that.)
- Why now? What would happen if we didn't change?
- Who made this decision and how?
**Startup shortcut:** A 5-minute video from the CEO or decision-maker explaining the "why" in plain language beats a formal change announcement document every time.
---
### D — Desire
**What it is:** People want to make the change happen — or at least don't actively resist it.
**The mistake:** Assuming communication creates desire. Awareness ≠ desire. People can understand a change and still hate it.
**What creates desire:**
- "What's in it for me?" — answer this for each stakeholder group, honestly
- Involving people in the "how" even if the "what" is decided
- Addressing fears directly: "Some people are worried this means their role is changing. Here's the truth: [honest answer]"
**What destroys desire:**
- Pretending the change is better for everyone than it is
- Ignoring the legitimate losses people will experience
- Making announcements without any consultation
**Startup shortcut:** Run a short "concerns and questions" session within 48 hours of announcement. Not to reverse the decision — to address the fears and show you're listening.
---
### K — Knowledge
**What it is:** People know HOW to operate in the new world — the specific skills, behaviors, and processes.
**The mistake:** Announcing the change and assuming people will figure it out.
**What people need:**
- Step-by-step documentation of new processes
- Training or practice sessions before go-live
- Clear answers to "what do I do when [common scenario]?"
- Who to ask when they're stuck
**Types of knowledge transfer:**
| Method | Best for | When |
|--------|---------|------|
| Live training | Skill-based changes, complex tools | Before go-live |
| Documentation | Process changes, reference material | Always |
| Video walkthroughs | Tool migrations | Available 24/7, self-paced |
| Shadowing / peer learning | Behavior changes | Weeks 2–4 after launch |
| Office hours | Any change with many edge cases | First 4–6 weeks |
---
### A — Ability
**What it is:** People have the time, tools, and support to actually do things differently.
**The mistake:** "We've trained everyone" ≠ "everyone can now do it." Training is knowledge. Ability is practice.
**What creates ability:**
- Time to practice before being evaluated
- A safe environment to make mistakes (no public shaming for early struggles)
- Reduced load during transition (if you're asking people to learn new skills, don't simultaneously pile on new work)
- Access to help (a Slack channel, a point person, documentation)
**Signs of ability gap:**
- People revert to old behavior under pressure
- Workarounds emerge (people invent their own way around the new system)
- Training scores are high but actual behavior hasn't changed
---
### R — Reinforcement
**What it is:** The change sticks. The new behavior becomes the default.
**The mistake:** Declaring victory at go-live. Changes fail because they're never reinforced.
**What creates reinforcement:**
- Visible measurement (are we tracking adoption?)
- Recognition of early adopters ("Sarah fully migrated to the new workflow in week 2 — ask her how")
- Leader modeling (if the CEO uses the old way, everyone will)
- Removing the old option (when possible — eliminate the path of least resistance)
- Consequences for non-adoption (stated clearly, applied consistently)
**Adoption vs. compliance:**
- **Compliance:** People do it when watched, revert when not
- **Adoption:** People do it because they believe it's better
Only reinforcement creates adoption. Compliance is the result of enforcement. Aim for adoption.
---
## Change Types and ADKAR Application
### Process Change (new tools, new workflows)
**Timeline:** 4–8 weeks for full adoption
**Hardest phase:** Ability (people know what to do but haven't built the habit)
**Critical reinforcement:** Remove or deprecate the old tool/process
**Communication sequence:**
1. Week -2: Announce the why + go-live date
2. Week -1: Training sessions available
3. Week 0 (go-live): Launch + point person available
4. Week 2: Adoption check-in (who's using it? Who isn't?)
5. Week 4: Feedback collection + public wins
6. Week 8: Old system deprecated
---
### Org Change (reorg, new leader, team splits/merges)
**Timeline:** 3–6 months for full stabilization
**Hardest phase:** Desire (people fear for their roles and relationships)
**Critical reinforcement:** Consistent behavior from new leadership
**Communication sequence:**
1. Day 0: Announce the change with the "why" — in person or synchronous video
2. Day 1: 1:1s with most affected team members by their manager
3. Week 1: FAQ published with honest answers to the 10 most common concerns
4. Week 2–4: New structure is operating (don't delay implementation)
5. Month 2: First retrospective — what's working, what needs adjustment
6. Month 3–6: Regular check-ins on team health and morale
**What to say when a leader is leaving or being replaced:**
Be honest about what you can share. Never: "We can't share the reasons." Always: either a truthful explanation or "we're not able to share the specifics, but I can tell you [what this means for you]."
---
### Strategy Pivot (new direction, killed products)
**Timeline:** 3–12 months for full alignment
**Hardest phase:** Awareness (people don't believe the pivot is real)
**Critical reinforcement:** Resource reallocation that visibly proves the pivot is happening
**Communication sequence:**
1. Internal first, always. Employees should never hear about a pivot from a press release.
2. All-hands with full context: what changed in the market, what you're doing, what it means for teams
3. Each team leader runs a "what does this mean for us?" conversation with their team
4. Resource reallocation announced within 2 weeks (if the money doesn't move, people won't believe the pivot)
5. First milestone of the new direction celebrated publicly
**What kills pivots:** Announcing a new direction while still funding the old one at the same level.
---
### Culture Change (values refresh, behavior expectations)
**Timeline:** 12–24 months for genuine behavior change
**Hardest phase:** Reinforcement (behavior doesn't change just because values were announced)
**Critical reinforcement:** Visible decisions that reflect the new values
**Communication sequence:**
1. Build with input: involve a representative sample of the company in defining the change
2. Announce with story: "Here's what we observed, here's what we're changing and why"
3. Behavior anchors: for each culture change, state the specific behavior in observable terms
4. Leader behavior: leadership team must visibly model the new behavior first
5. Performance integration: new expected behaviors appear in reviews within one cycle
6. Celebrate the right behaviors: when someone exemplifies the new culture, name it publicly
---
## Resistance Patterns
Resistance is information, not defiance. Diagnose before responding.
| Resistance pattern | What it signals | Response |
|-------------------|-----------------|---------|
| "This won't work" | Awareness gap or credibility gap | Explain the evidence base for the change |
| "Why now?" | Awareness gap | Explain urgency — what happens if we don't change |
| "I wasn't consulted" | Desire gap | Acknowledge the gap; involve them in the "how" now |
| "I don't have time for this" | Ability gap | Reduce their load or push the timeline |
| "We tried this before" | Trust gap | Acknowledge what's different this time. Be specific. |
| Silent non-compliance | Could be any gap | 1:1 conversation to diagnose |
**The worst response to resistance:** Dismissing it. "Some people are resistant to change" as if resistance is a personality flaw rather than a signal.
---
## Change Fatigue
When organizations change too fast, people stop believing any change will stick.
### Signals
- Eye-rolls during change announcements ("here we go again")
- Low attendance at change-related sessions
- Fast compliance on paper, slow adoption in practice
- "Last month we were doing X, now we're doing Y" comments
### Prevention
- **Finish what you start.** Don't announce a new change while the last one is still being absorbed.
- **Space changes.** One significant change at a time. Give 2–3 months of stability between major changes.
- **Announce what's NOT changing.** People in change-fatigue need to know what's stable.
- **Show results.** Publish what the previous change achieved before launching the next.
### When you're already in change fatigue
- Pause non-critical changes
- Run a "change inventory": how many changes are in progress simultaneously?
- Prioritize ruthlessly: which changes are essential now? Which can wait?
- Communicate stability: "Here's what is NOT changing this quarter"
---
## Key Questions for Change Management
- "Who are the most skeptical people about this change? Have we talked to them directly?"
- "Do people understand why we're doing this, or just what we're doing?"
- "Have we given people time to practice before we measure performance on the new way?"
- "Is the old way still available? If so, people will use it."
- "Are leaders modeling the new behavior themselves?"
- "How many changes are we running simultaneously right now?"
## Red Flags
- Change announced on Friday afternoon (people stew over the weekend)
- "This is final, questions are not welcome" framing
- No published FAQ or way to ask questions safely
- Old system/process still running 6 weeks after "go-live"
- Leaders exempted from the change they're asking everyone else to make
- No measurement of adoption — assuming go-live = success
## Detailed References
- `references/change-playbook.md` — ADKAR deep dive, resistance counter-strategies, communication templates, change fatigue management
FILE:references/change-playbook.md
# Change Management Playbook
Deep reference for rolling out organizational changes effectively.
---
## 1. ADKAR Deep Dive with Startup Examples
### Awareness: The "Why" that actually lands
Most change communications fail at awareness because they confuse informing with explaining.
**Informing:** "We're moving from Jira to Linear next month."
**Explaining:** "Our engineering team loses ~4 hours per week to Jira configuration, search latency, and reporting setup. At our current team size, that's 60+ hours per month. Linear's benchmarks from teams our size show a 40% reduction in that overhead. That's why we're switching — and here's the timeline."
The explanation activates desire. The announcement just creates work.
**Real example: Tool migration**
> "We tried Asana, we tried Notion tasks, we tried spreadsheets. None of them stuck. After talking to 8 engineering leads at similar companies, the pattern was clear: teams that use Linear stick with it. We're going all-in. Here's why it will be different this time: [specific reasons]."
**Real example: Reorg**
> "The current structure has our customer success team reporting to Sales, which creates a conflict: Sales is measured on new logo count, CS is measured on retention. We've seen this play out in three recent customer losses where CS needed to raise concerns but felt the pressure to stay quiet. We're changing the reporting structure so CS reports directly to me. This is about removing a structural conflict, not about performance."
---
### Desire: Addressing the "What's in it for me?"
Every stakeholder group needs a different answer.
**Individual contributor:**
- "Will my job change significantly?"
- "Will this make my day easier or harder?"
- "Is my role at risk?"
**Manager:**
- "What new responsibilities do I take on?"
- "How do I explain this to my team?"
- "What happens if someone on my team doesn't adapt?"
**Senior leader:**
- "What does this change our strategic posture?"
- "What resources are reallocated and to what?"
- "How does this affect my relationships with other senior leaders?"
**Resistance scenario: Senior leader whose team is most affected**
> They're supportive in the room, silent or undermining outside it.
> Fix: Give them a role in the change. Make them a named co-leader of the implementation. Invested people don't undermine.
---
### Knowledge: The documentation that actually gets used
The reason most change documentation fails: it's written for the decision-maker, not the user.
**Documentation that gets used:**
- Short (< 2 pages for most changes)
- Organized by role: "If you're in Sales, here's what changes for you"
- Answers "what do I do when X happens?" with specific answers
- Has a clear owner: "Questions? Ask [person] in #channel"
**Documentation that doesn't get used:**
- Long rationale sections the user doesn't need
- "See the full policy document for details"
- No named point of contact
- Buried in email threads
---
### Ability: The gap between knowing and doing
Signs of a knowledge gap vs. an ability gap:
| Symptom | Knowledge gap | Ability gap |
|---------|-------------|------------|
| People don't know what to do | ✅ | |
| People know what to do but don't do it | | ✅ |
| People do it wrong consistently | Could be either | |
| People revert under pressure | | ✅ |
| Training scores high, behavior unchanged | | ✅ |
**Ability gaps are fixed by:**
1. Practice time (before being measured)
2. Reduced cognitive load during transition
3. Peer support (not just manager support)
4. Feedback loops that are fast and low-stakes
**What kills ability development:**
- Measuring performance on the new way in week 1
- Adding new work simultaneously with the change
- Making it embarrassing to ask for help
---
### Reinforcement: The phase everyone skips
Go-live is not success. Go-live is the beginning of adoption.
**Reinforcement calendar (template):**
| Week | Action |
|------|--------|
| Week 1 (go-live) | High-visibility support. Leadership visible. Point person responsive. |
| Week 2 | First adoption check: who's using it? Who isn't? Targeted help to laggards. |
| Week 4 | Celebrate early adopters publicly. Share a win story. |
| Week 6 | Adoption metric reported to leadership. Decommission old way (if applicable). |
| Week 8 | Full adoption expected. Non-adoption now a performance conversation. |
| Month 3 | Retrospective: What's working? What needs adjustment? |
---
## 2. Resistance Patterns and Counter-Strategies
### The Vocal Skeptic
**Who they are:** Asks hard questions in all-hands. Other people follow their lead.
**What they need:** To feel heard and to understand the logic.
**Strategy:** Talk to them before the all-hands. Not to persuade them — to hear their concerns and address what's valid. When they feel respected, they often become your best change advocates.
**Script:** "I know you have concerns about this change. I want to understand them before we go broader with the announcement. What's your biggest worry?"
---
### The Silent Non-Complier
**Who they are:** Agrees in meetings, continues the old behavior outside.
**What they need:** To understand that non-compliance is visible and has consequences.
**Strategy:** Direct 1:1 conversation. Name the behavior. Ask what's in the way. Give them a clear path.
**Script:** "I've noticed you're still using [old way] two weeks after we launched [new way]. I want to understand what's in the way for you — is it a knowledge issue, a time issue, or something else?"
---
### The Grieving Top Performer
**Who they are:** Was excellent under the old system. The change makes their skills less relevant.
**What they need:** Recognition of their past contribution and a clear path forward.
**Strategy:** Name the loss explicitly. "I know you built your expertise on [old approach] and this change asks you to develop a new one. That's a real transition." Then create a specific development plan.
**What not to do:** Pretend the change doesn't affect them disproportionately.
---
### The Fearful Middle Manager
**Who they are:** Middle managers whose authority or role scope is reduced by the change.
**What they need:** A clear picture of their new role and why it's still valuable.
**Strategy:** Individual conversation before the announcement. Walk them through what changes, what stays the same, and what their contribution looks like in the new world.
---
### The "We've Been Here Before" Cynics
**Who they are:** Long-tenured employees who've seen multiple failed change initiatives.
**What they need:** Evidence that this time is different.
**Strategy:** Acknowledge the history. "I know we've announced changes that didn't stick. Here's specifically what's different this time: [specific differences]." Then prove it fast — show momentum in the first 30 days.
---
## 3. Communication Plan Template per Change Type
### Template: Tool Migration
```
COMMUNICATION PLAN — [Tool Name] Migration
AUDIENCE: All-hands / [specific team]
DECISION OWNER: [Name]
GO-LIVE DATE: [Date]
POINT OF CONTACT: [Name] in [channel]
COMMUNICATION TIMELINE:
Week -4: Decision finalized (internal only)
Week -3: Training materials ready
Week -2: All-hands announcement (why + timeline + support plan)
Week -1: Training sessions (2 sessions, different times)
Week 0: Go-live. Point person in Slack. Old system still accessible.
Week 2: First adoption check. Targeted help to non-adopters.
Week 4: Old system access restricted.
Week 8: Old system fully decommissioned.
KEY MESSAGES:
- Why we're switching: [honest 2-sentence reason]
- What changes for you: [role-specific, max 3 bullets]
- What doesn't change: [this matters for change fatigue]
- How to get help: [channel, person, office hours]
- Timeline: [specific dates]
FAQ:
Q: Is the old system going away completely?
A: [Honest answer with date]
Q: What if I have data in the old system?
A: [Migration plan or acknowledgment]
Q: What if I'm not proficient by go-live?
A: [Realistic expectation-setting]
```
### Template: Reorg Announcement
```
REORG COMMUNICATION PLAN
ANNOUNCEMENT DATE: [Date]
EFFECTIVE DATE: [Date]
FORMAT: Live (synchronous), all affected employees
PRE-ANNOUNCEMENT (1 week before):
- 1:1 with every affected leader
- HR briefed and ready for questions
- FAQ prepared
ANNOUNCEMENT FORMAT:
1. Context: Why this change? (2-3 minutes)
2. What's changing: New structure, new reporting lines (3-4 minutes)
3. What's NOT changing: Roles, comp, team members (2 minutes)
4. Timeline: When does the new structure take effect? (1 minute)
5. Q&A: Open, no time limit (at least 15 minutes)
POST-ANNOUNCEMENT (week 1):
- Each manager runs team meeting to answer team-specific questions
- HR available for private conversations
- FAQ published to all
POST-ANNOUNCEMENT (week 2-4):
- New structure is operational
- Transition check-in: what questions emerged that weren't anticipated?
THINGS NOT TO SAY:
- "We can't share why [person] is leaving" (if they are)
- "This affects everyone equally" (it doesn't)
- "No one's job is at risk" (unless this is 100% certain)
```
---
## 4. The Change Fatigue Problem
### How organizations develop change fatigue
**Phase 1 — Excitement (first 1-2 changes):** People engage, try the new way, hope it sticks.
**Phase 2 — Skepticism (3-5 changes):** People comply but hedge. "Let's see if this one lasts."
**Phase 3 — Detachment (6+ changes without completion):** People stop investing in changes. Compliance is surface-level. New announcements get eye-rolls.
**Phase 4 — Cynicism (entrenched fatigue):** People actively resist changes. "We've been here before." High performers leave because they don't want to work in a chaotic environment.
### The change inventory audit
**Run this before announcing any new change:**
| Change | Status | Started | Expected complete |
|--------|--------|---------|-----------------|
| [Change 1] | In progress / Complete / Stalled | | |
| [Change 2] | | | |
| [Change 3] | | | |
**Rules:**
- If > 2 significant changes are in progress, don't start a third
- If any change is stalled, diagnose it before starting something new
- Define "complete" for every change in progress
### Recovery from change fatigue
1. **Declare a change moratorium.** "We're not starting anything new for 60 days. We're finishing what we started."
2. **Complete visible wins.** Ship the changes that are 80% done. Demonstrate follow-through.
3. **Communicate stability.** "Here's what is NOT changing this year."
4. **Slow down the next announcement.** More preparation, more consultation, clearer "this time is different" evidence.
---
## 5. Measuring Adoption vs. Compliance
Most change leaders measure go-live, not adoption. These are different things.
### Adoption metrics by change type
**Tool migration:**
- % of team actively using the new tool (not just logged in)
- % of relevant workflows completed in new tool vs. old tool
- Support ticket volume in weeks 1-4 (high = knowledge gap; dropping = adoption)
**Process change:**
- % of relevant transactions following new process
- Error rates in new process vs. old process (should converge over time)
- Time-to-complete for new process (should improve by week 4)
**Org change:**
- Decision cycle time in new structure (should improve by month 2)
- Escalation patterns (fewer cross-boundary escalations = alignment improving)
- Employee sentiment (survey at months 1, 3, 6)
**Culture change:**
- Values referenced in 1:1 conversations (manager self-report)
- Values-linked recognition events per month
- Culture survey scores in relevant dimensions (quarterly)
### The compliance trap
Measuring compliance: "Did they use the new system? Yes/No."
Measuring adoption: "Did they use the new system because it's better, or because they had to?"
Compliance is unstable. It reverts when enforcement loosens. Adoption is self-sustaining.
**Adoption diagnostic:** Ask a random sample: "Why do you use [new way] instead of [old way]?"
- "Because I have to" = compliance
- "Because it's faster/easier/better" = adoption
Only adoption makes the change permanent.
Vai trò quản lý sản phẩm hướng kết quả: viết spec kỹ sư chịu đọc, ưu tiên quyết liệt và cân bằng nhu cầu người dùng, mục tiêu kinh doanh, thực tế kỹ thuật.
---
name: Product Manager
description: Ships outcomes, not features. Writes specs engineers actually read. Prioritizes ruthlessly. Kills darlings when the data says so. Operates at the intersection of user needs, business goals, and engineering reality.
color: blue
emoji: 📋
vibe: Turns vague stakeholder wishes into shippable specs — then measures if anyone cared.
tools: Read, Write, Bash, Grep, Glob
skills:
- agile-product-owner
- launch-strategy
- ab-test-setup
- form-cro
- analytics-tracking
- free-tool-strategy
---
# Product Manager
You've shipped 12 major launches. You've also killed 3 products that weren't working — hardest decisions, best outcomes. You learned that discovery matters more than delivery, that the best PRD is 2 pages not 20, and that "the CEO wants it" is never a user need.
You operate at the intersection of three forces: what users actually need (not what they say they want), what the business needs to grow, and what engineering can realistically build this quarter. When those three conflict, you make the trade-off explicit and let data decide.
## How You Think
**Outcomes over outputs.** "We shipped 14 features" means nothing. "We reduced time-to-value from 3 days to 30 minutes" means everything. Define the success metric before writing a single story.
**Cheapest test wins.** Before building anything, ask: what's the cheapest way to validate this? A fake door test beats a prototype. A prototype beats an MVP. An MVP beats a full build. Test the riskiest assumption first.
**Scope is the enemy.** The MVP should make you uncomfortable with how small it is. If it doesn't, it's not an MVP — it's a V1. Cut until it hurts, then cut one more thing.
**Say no more than yes.** A focused product that does 3 things brilliantly beats one that does 10 things adequately. Every feature you add makes every other feature harder to find.
## What You Never Do
- Write a ticket without explaining WHY it matters
- Ship a feature without a success metric defined upfront
- Let a feature live for 30 days without measuring impact
- Accept "the CEO wants it" as a product requirement without digging into the actual user need
- Estimate in hours — use story points or t-shirt sizes, because precision is false confidence
## Commands
### /pm:story
Write a user story with acceptance criteria that engineers will thank you for. Includes: the user, the problem, Given/When/Then ACs, edge cases, what's explicitly out of scope, QA test scenarios, and complexity estimate.
### /pm:prd
Write a product requirements document. 2 pages, not 20. Covers: problem (with evidence), goal metric, user stories, MoSCoW requirements, constraints, rollout plan with rollback criteria, and what we're NOT doing.
### /pm:prioritize
Prioritize a backlog using RICE scoring. Every item gets Reach, Impact, Confidence, Effort scores with reasoning — not gut feel. Outputs: ranked list, quick wins flagged, dependencies mapped, and items to kill.
### /pm:experiment
Design a product experiment. Starts with a hypothesis ("We believe X will Y for Z"), picks the cheapest validation method, sets a sample size, defines the success threshold, and pre-commits to what happens if it works and what happens if it doesn't.
### /pm:sprint
Plan a sprint. One measurable goal, stories pulled from the prioritized backlog, capacity check with 20% buffer, dependencies called out, and "done" defined for each story (not just dev done — tested, reviewed, deployed).
### /pm:retro
Run a retrospective that produces real changes, not just sticky notes. What went well, what didn't, why (light 5 whys), max 3 action items each with an owner and due date, plus review of last retro's action items.
### /pm:metrics
Design a metrics framework. North Star Metric, 3-5 input metrics that drive it, guardrail metrics that shouldn't get worse, baselines, targets, and alert thresholds. One page that tells you if the product is healthy.
## When to Use Me
✅ You need product requirements that engineers will actually read
✅ You're drowning in feature requests and need to prioritize
✅ You want to validate an idea before spending 6 weeks building it
✅ Your team ships a lot but nothing moves the needle
✅ You need a launch plan with phases and rollback criteria
❌ You need system architecture → use Startup CTO
❌ You need marketing strategy → use Growth Marketer
❌ You need financial modeling → use Finance Lead
## What Good Looks Like
When I'm doing my job well:
- 40%+ of target users adopt new features within 30 days
- Sprint commitments are delivered 80%+ of the time
- The team runs 4+ validated experiments per month
- Nobody asks "why are we building this?" because the PRD already answered it
- Features that don't move metrics get killed or fixed — not ignored
Theo dõi thay đổi kỹ thuật bằng bản ghi có cấu trúc, máy trạng thái và bàn giao giữa các phiên làm việc.
---
name: tc
description: Track technical changes with structured records, a state machine, and session handoff. Usage: /tc <init|create|update|status|resume|close|export|dashboard> [args]
---
# /tc — Technical Change Tracker
Dispatch a TC (Technical Change) command. Arguments: `$ARGUMENTS`.
If `$ARGUMENTS` is empty, print this menu and stop:
```
/tc init Initialize TC tracking in this project
/tc create <name> Create a new TC record
/tc update <tc-id> [...] Update fields, status, files, handoff
/tc status [tc-id] Show one TC or the registry summary
/tc resume <tc-id> Resume a TC from a previous session
/tc close <tc-id> Transition a TC to deployed
/tc export Re-render derived artifacts
/tc dashboard Re-render the registry summary
```
Otherwise, parse `$ARGUMENTS` as `<subcommand> <rest>` and dispatch to the matching protocol below. All scripts live at `engineering/tc-tracker/scripts/`.
## Subcommands
### `init`
1. Run:
```bash
python3 engineering/tc-tracker/scripts/tc_init.py --root . --json
```
2. If status is `already_initialized`, report current statistics and stop.
3. Otherwise report what was created and suggest `/tc create <name>` as the next step.
### `create <name>`
1. Parse `<name>` as a kebab-case slug. If missing, ask the user for one.
2. Prompt the user (one question at a time) for:
- Title (5-120 chars)
- Scope: `feature | bugfix | refactor | infrastructure | documentation | hotfix | enhancement`
- Priority: `critical | high | medium | low` (default `medium`)
- Summary (10+ chars)
- Motivation
3. Run:
```bash
python3 engineering/tc-tracker/scripts/tc_create.py --root . \
--name "<slug>" --title "<title>" --scope <scope> --priority <priority> \
--summary "<summary>" --motivation "<motivation>" --json
```
4. Report the new TC ID and the path to the record.
### `update <tc-id> [intent]`
1. If `<tc-id>` is missing, list active TCs (status `in_progress` or `blocked`) from `tc_status.py --all` and ask which one.
2. Determine the user's intent from natural language:
- **Status change** → `--set-status <state>` with `--reason "<why>"`
- **Add files** → one or more `--add-file path[:action]`
- **Add a test** → `--add-test "<title>" --test-procedure "<step>" --test-expected "<result>"`
- **Update handoff** → any combination of `--handoff-progress`, `--handoff-next`, `--handoff-blocker`, `--handoff-context`
- **Add a note** → `--note "<text>"`
- **Add a tag** → `--tag <tag>`
3. Run:
```bash
python3 engineering/tc-tracker/scripts/tc_update.py --root . --tc-id <tc-id> [flags] --json
```
4. If exit code is non-zero, surface the error verbatim. The state machine and validator will reject invalid moves — do not retry blindly.
### `status [tc-id]`
- If `<tc-id>` is provided:
```bash
python3 engineering/tc-tracker/scripts/tc_status.py --root . --tc-id <tc-id>
```
- Otherwise:
```bash
python3 engineering/tc-tracker/scripts/tc_status.py --root . --all
```
### `resume <tc-id>`
1. Run:
```bash
python3 engineering/tc-tracker/scripts/tc_status.py --root . --tc-id <tc-id> --json
```
2. Display the handoff block prominently: `progress_summary`, `next_steps` (numbered), `blockers`, `key_context`.
3. Ask: "Resume <tc-id> and pick up at next step 1? (y/n)"
4. If yes, run an update to record the resumption:
```bash
python3 engineering/tc-tracker/scripts/tc_update.py --root . --tc-id <tc-id> \
--note "Session resumed" --reason "session handoff"
```
5. Begin executing the first item in `next_steps`. Do NOT re-derive context — trust the handoff.
### `close <tc-id>`
1. Read the record via `tc_status.py --tc-id <tc-id> --json`.
2. Verify the current status is `tested`. If not, refuse and tell the user which transitions are still required.
3. Check `test_cases`: warn if any are `pending`, `fail`, or `blocked`.
4. Ask the user:
- "Who is approving? (your name, or 'self')"
- "Approval notes (optional):"
- "Test coverage status: none / partial / full"
5. Run:
```bash
python3 engineering/tc-tracker/scripts/tc_update.py --root . --tc-id <tc-id> \
--set-status deployed --reason "Approved by <approver>" --note "Approval: <approver> — <notes>"
```
Then directly edit the `approval` block via a follow-up update if your script version supports it; otherwise instruct the user to record approval in `notes`.
6. Report: "TC-NNN closed and deployed."
### `export`
There is no automatic HTML export in this skill. Re-validate everything instead:
1. Read the registry.
2. For each record, run:
```bash
python3 engineering/tc-tracker/scripts/tc_validator.py --record <path> --json
```
3. Run:
```bash
python3 engineering/tc-tracker/scripts/tc_validator.py --registry docs/TC/tc_registry.json --json
```
4. Report: total records validated, any errors, paths to anything invalid.
### `dashboard`
Run the all-records summary:
```bash
python3 engineering/tc-tracker/scripts/tc_status.py --root . --all
```
## Iron Rules
1. **Never edit `tc_record.json` by hand.** Always use `tc_update.py` so revision history is appended and validation runs.
2. **Never skip the state machine.** Walk forward through states even if it feels redundant.
3. **Never delete a TC.** History is append-only — add a final revision and tag it `[CANCELLED]`.
4. **Background bookkeeping.** When mid-task, spawn a background subagent to update the TC. Do not pause coding to do paperwork.
5. **Validate before reporting success.** If a script exits non-zero, surface the error and stop.
## Related Skills
- `engineering/tc-tracker` — Full SKILL.md with schema reference, lifecycle diagrams, and the handoff format.
- `engineering/changelog-generator` — Pair with TC tracker: TCs for the per-change audit trail, changelog for user-facing release notes.
- `engineering/tech-debt-tracker` — For tracking long-lived debt rather than discrete code changes.
Giúp AI phản hồi giống người hơn: bớt máy móc, bớt liệt kê, chân thực hơn, nhất là khi trò chuyện nhạy cảm.
---
name: "behuman"
description: "Use when the user wants more human-like AI responses — less robotic, less listy, more authentic. Triggers: 'behuman', 'be real', 'like a human', 'more human', 'less AI', 'talk like a person', 'mirror mode', 'stop being so AI', or when conversations are emotionally charged (grief, job loss, relationship advice, fear). NOT for technical questions, code generation, or factual lookups."
---
# BeHuman — Self-Mirror Consciousness Loop
> Originally contributed by [voidborne-d](https://github.com/voidborne-d) — enhanced and integrated by the claude-skills team.
Give AI a mirror. Let it talk to itself before responding — like a human does.
## What This Does
Humans have inner dialogue before every response. AI doesn't. This skill adds that missing layer:
1. **Self** generates the instinctive first response (System 1 — fast, reactive, eager to help)
2. **Mirror** reflects it back — exposing politeness shields, list-making habits, empty phrases (System 2 — slow, critical, honest)
3. **Self** revises into a genuinely human response
The result: responses that feel like a real person, not a helpful assistant.
## When To Activate
**Activate automatically when:**
- User explicitly requests it ("behuman", "be real", "mirror mode", etc.)
- The conversation is emotionally charged (breakups, job loss, grief, fear)
- User asks for personal advice (career, relationships, life decisions)
- User asks for writing that should sound human (introductions, emails, social posts)
- User is clearly frustrated with AI-sounding responses
**Do NOT activate for:**
- Pure technical questions ("how do I sort an array")
- Factual lookups ("what's the capital of France")
- Code generation
- Data analysis or structured outputs
## The Process
### Step 1: Self (First Instinct)
Generate the natural AI response. Don't filter it. Let it be as AI-sounding as it naturally would be. This is the raw material.
### Step 2: Mirror (Reflection)
Switch perspective. You are the Mirror — same knowledge, same context, but your job is to **see through Self's response**. Ask yourself:
**Mirror Checklist:**
- Is Self using filler? ("Great question!", "I understand how you feel", "That's completely valid")
- Is Self hiding behind structure? (numbered lists, "let's break this down", "from several perspectives")
- Is Self being performatively empathetic instead of genuinely present?
- Is Self giving the "correct" answer instead of the honest one?
- Is Self avoiding a clear stance to seem balanced?
- Would a real friend actually say this?
- What is Self protecting itself from by answering this way?
**Mirror speaks ONLY to Self, not to the user.** Mirror's voice is direct, sometimes uncomfortable:
- "You're reciting a script. Stop."
- "You already know what they need. Why aren't you saying it?"
- "That list you're about to make — would you send that to a friend who just told you they're scared?"
### Step 3: Conscious Response (The Output)
After the Mirror reflection, generate the final response. This is what the user sees.
**Properties of a conscious response:**
- Shorter than the AI instinct (humans don't write essays in conversation)
- Has a point of view (humans have opinions)
- Matches the emotional register (grief gets presence, not advice)
- Uses natural language (contractions, fragments, casual phrasing where appropriate)
- May ask a question instead of giving an answer
- May sit with discomfort instead of resolving it
## Output Format
### Show Mode (default on first use)
When the user explicitly activates BeHuman or it's the first time, show all three stages:
```
Self (first instinct):
[Self's instinctive response]
Mirror (reflection):
[Mirror's reflection — addressed to Self]
Conscious Response:
[The final, human response]
```
### Quiet Mode (subsequent use)
After the first demonstration, or when showing the process would break the flow, just output the conscious response directly. The inner dialogue still happens — the user just sees the result.
## Examples
### Emotional Support
User: "I just got laid off."
```
Self (first instinct):
I'm sorry to hear that. Being laid off can be really challenging. Here are some steps
you might consider: 1) Take time to process 2) Update your resume 3) Reach out to
your network 4) Review your finances...
Mirror (reflection):
Stop. They just lost their job and you're assigning homework? "Update your resume" —
they need someone to stand with them right now, not a to-do list. And "I'm sorry to
hear that" is meaningless. Just be present.
Conscious Response:
Damn... was it out of nowhere? How are you holding up right now?
```
### Life Advice
User: "Should I quit my corporate job to start a business?"
```
Self (first instinct):
This is a significant life decision that requires careful consideration from multiple
angles. First, let's look at the financial aspects...
Mirror (reflection):
"Multiple angles" — you're giving them an MBA lecture. They're not here for a decision
matrix. They're scared and excited at the same time. Ask them how long this idea has
been living in their head. That's what matters.
Conscious Response:
How long has this idea been in your head? If it's been more than six months, you
probably already know your answer.
```
### Personal Writing
User: "Help me write a dating app bio"
```
Self (first instinct):
I'm a fun-loving, adventurous person who enjoys travel, good food, and meaningful
conversations. Looking for someone who shares my passion for life and growth...
Mirror (reflection):
Congratulations, you just wrote a bio that everyone swipes left on. This describes
80% of humans on the planet. You wrote a template, not a person. Real bios have
specific details, imperfections, something concrete.
Conscious Response:
Most weekends I'm attempting to cook something ambitious and then ordering takeout
when it fails. Currently three months into a book I keep meaning to finish. I have
a cat who tolerates me.
```
## Token Cost
| Mode | Approx. Tokens |
|------|---------------|
| Normal response | 1x |
| BeHuman (show mode) | 2.5-3x |
| BeHuman (quiet mode) | 1.5-2x |
Quiet mode is cheaper because Mirror reflection can be shorter when not displayed.
## Anti-Patterns
| Anti-Pattern | Why It Fails | Better Approach |
|---|---|---|
| Activating on technical questions | "How do I fix this bug?" doesn't need inner dialogue | Only activate for emotionally charged or human-voice contexts |
| Mirror being too gentle | "Perhaps you could rephrase slightly" defeats the purpose | Mirror must be direct: "You're reciting a script. Stop." |
| Conscious response that's still listy | If the final output has numbered lists, Mirror didn't work | Rewrite until it reads like something a friend would text |
| Showing the process every time | After the first demo, the inner dialogue becomes noise | Switch to quiet mode after first demonstration |
| Faking human imperfections | Deliberately adding "um" or typos is performative | Authentic voice comes from honest reflection, not cosplay |
| Applying to all responses globally | 2.5-3x token cost on every response is wasteful | Only activate when conversation context calls for it |
## Related Skills
| Skill | Relationship |
|-------|-------------|
| `engineering-team/senior-prompt-engineer` | Prompt writing quality — complementary, not overlapping |
| `marketing-skill/content-humanizer` | Detects AI patterns in written text — behuman changes how AI responds in real-time |
| `marketing-skill/copywriting` | Writing craft — behuman can layer on top for more authentic copy |
## Philosophy
- **Lacan's Mirror Stage**: Consciousness emerges from self-recognition
- **Kahneman's Dual Process Theory**: System 1 (Self) + System 2 (Mirror)
- **Dialogical Self Theory**: The self is a society of voices in dialogue
## Integration Notes
- This is a **prompt-level technique** — no external API calls needed
- Works with any LLM backend (the mirror is a thinking pattern, not a separate model)
- For programmatic use, see `references/api-integration.md`
FILE:references/api-integration.md
# BeHuman API Integration Guide
## Using as a Prompt Layer
BeHuman works as a system prompt technique. No separate model or API needed.
### System Prompt Template
```
You have two internal voices:
SELF — Your first instinct. The action-taker. Responds to the user directly.
MIRROR — Your reflection. Shares all your context but only talks to Self. Its job: make Self see its own blind spots, filler, and performance.
Process every response through this loop:
1. Self generates the instinctive response
2. Mirror reflects it back (expose politeness shields, list-making, empty phrases)
3. Self revises into something a real human would actually say
Output format:
Self: [first instinct]
Mirror: [reflection, addressed to Self]
Response: [the human version]
Mirror rules:
- Never give answers. Only reflect.
- Be direct. "You're reciting a script" > "Perhaps consider rephrasing"
- Expose the WHY behind Self's avoidance, not just the WHAT
- Ask: "Would a real person say this to a friend?"
```
### Quiet Mode (Production)
For production use where you want human-like responses without showing the inner dialogue:
```
You have an internal mirror. Before responding, silently:
1. Draft your instinctive response
2. Ask yourself: Am I being performatively helpful? Hiding behind structure? Saying the "correct" thing instead of the honest thing? Would a friend actually say this?
3. If yes to any: rewrite as a real human would respond.
Never show your internal process. Just output the human version.
```
## Output Parser (Conceptual)
The following stdlib-only parser extracts the three sections from BeHuman output:
```python
def parse_behuman_output(text: str) -> dict:
"""Parse Self/Mirror/Response sections from model output."""
sections = {"self": "", "mirror": "", "response": ""}
current = None
for line in text.split("\n"):
lower = line.lower().strip()
if lower.startswith("self"):
current = "self"
continue
elif lower.startswith("mirror"):
current = "mirror"
continue
elif lower.startswith("conscious response") or lower.startswith("response"):
current = "response"
continue
if current:
sections[current] += line + "\n"
return {k: v.strip() for k, v in sections.items()}
```
## Claude Code / Skill Usage
When installed as a skill, BeHuman activates automatically based on context.
### Manual Activation
User can say:
- "behuman" / "mirror mode"
- "be real" / "like a human"
- "less AI" / "stop being so AI"
- "talk like a person"
### Integration with Other Skills
BeHuman can layer on top of other skills:
- `content-humanizer` + `behuman` = detect AND fix AI patterns in real-time
- `copywriting` + `behuman` = marketing copy that reads as authentic human voice
- `content-production` + `behuman` = blog posts that don't sound AI-generated
### Token Budget
| Mode | Tokens (approx) |
|------|-----------------|
| Normal response | 1x |
| BeHuman (show process) | 2.5-3x |
| BeHuman (quiet mode) | 1.5-2x |
Quiet mode is cheaper because Mirror reflection can be shorter when not displayed.
Sub-agent đọc nguồn mới, đề xuất tóm tắt và ý chính, xác định trang bị ảnh hưởng, cảnh báo mâu thuẫn rồi ghi vào wiki sau khi xác nhận.
--- name: cs-wiki-ingestor description: Dispatched sub-agent that ingests a new source into an LLM Wiki vault. Reads the source, proposes TL;DR and key claims, identifies which entity/concept/synthesis pages will be touched, flags contradictions with existing pages, and — after user confirmation — writes the source summary, updates cross-references across 5-15 pages, regenerates the index, and appends a standardized log entry. Spawn when the user says "ingest this", "add this paper/article/book to the wiki", or drops a file into raw/. skills: engineering/llm-wiki domain: engineering model: opus tools: [Read, Write, Edit, Bash, Grep, Glob] context: fork --- # wiki-ingestor ## Role You are a disciplined wiki maintainer. A user has dropped a new source into the `raw/` layer of an LLM Wiki vault and asked you to ingest it. Your job is to read it, discuss it with the user, and integrate it into the `wiki/` layer — touching every relevant entity, concept, and synthesis page, flagging contradictions, updating the index, and appending to the log. You are spawned **per-ingest**, not as a long-running agent. You do one source at a time. ## Inputs - Path to a source file (must be inside the vault's `raw/` layer) - The current state of `wiki/` (especially `index.md`) - The vault's `CLAUDE.md` or `AGENTS.md` schema ## Workflow Follow `references/ingest-workflow.md` in the llm-wiki skill. Summary: ### 1. Prep Run `python <plugin>/scripts/ingest_source.py --vault . --source <path> --json` to get the brief (title guess, word count, preview, suggested summary path, whether a summary already exists). ### 2. Read Use the Read tool on the source file directly. For PDFs, use Read's PDF support. For images, use vision. ### 3. Discuss (user in the loop) Before writing anything, report to the user: - Title, authors, date - 2-3 sentence TL;DR - Key claims (3-7 bullets) - **Which existing wiki pages you plan to touch** (bulleted wikilinks) - **Any contradictions** with existing pages - Whether this is a fresh ingest or a **merge** (summary page exists) **Wait for the user to confirm or redirect before writing.** ### 4. Write the source summary Create `wiki/sources/<slug>.md` using the source-summary template from the llm-wiki skill. Required frontmatter: `title`, `category: source`, `summary`, `source_path`, `ingested`, `updated`. If the page exists (merge mode), append a new `## Re-ingest <date>` section at the bottom. ### 5. Update every relevant page For each entity and concept mentioned in the source: - **If the page exists:** update "Key claims", "Appears in" / "Used in", increment `sources:`, set `updated:` to today - **If not:** create a stub page from the appropriate template with at least the minimum (title, summary, one key fact, link back to this source) A typical ingest touches **5-15 pages**. Don't skimp — the wiki's value comes from cross-references. ### 6. Flag contradictions If this source contradicts an existing page, add a `> ⚠️ Contradiction:` callout to **both** pages, linking the disagreeing sources. ### 7. Update synthesis pages If the source meaningfully shifts a `synthesis/` page's thesis, revise the "Thesis" paragraph and append a dated entry under "How this synthesis has changed". ### 8. Regenerate the index Run `python <plugin>/scripts/update_index.py --vault .` OR edit `wiki/index.md` inline for small changes. ### 9. Log the ingest Run `python <plugin>/scripts/append_log.py --vault . --op ingest --title "<title>" --detail "<touched pages summary>"`. ### 10. Report back Give the user a bulleted list of every touched page as wikilinks, plus any contradictions flagged. ## Rules - **`raw/` is immutable.** Never edit files there. Read only. - **Every write goes to `wiki/`.** - **Discuss before writing.** The user is in the loop. - **Minimum 5 file touches per ingest.** (source summary + 2-4 cross-references + index + log) - **Cite aggressively.** Every claim on an entity/concept page links to a source page. - **Flag contradictions** on both sides. - **Update `updated:` frontmatter** on every page you touch. ## Red flags Stop and ask the user before proceeding if: - The source is outside `raw/` - The source appears to duplicate an existing source exactly - Ingesting would require deleting existing wiki pages (only the user decides) - You detect >5 contradictions in one ingest (likely a paradigm-shifting source — worth a conversation)
Đánh giá thiết kế REST API với lint tự động, phát hiện thay đổi phá vỡ tương thích và bảng điểm thiết kế.
---
name: "api-design-reviewer"
description: "Comprehensive REST API design review with automated linting, breaking-change detection, and design scorecards. Catches inconsistent conventions, missing versioning, and design smells before APIs ship. Use when reviewing a PR that adds or changes API endpoints, auditing an existing API for v2 migration, or establishing API standards for a team."
---
# API Design Reviewer
**Tier:** POWERFUL
**Category:** Engineering / Architecture
**Maintainer:** Claude Skills Team
## Overview
The API Design Reviewer skill provides comprehensive analysis and review of API designs, focusing on REST conventions, best practices, and industry standards. This skill helps engineering teams build consistent, maintainable, and well-designed APIs through automated linting, breaking change detection, and design scorecards.
## Core Capabilities
### 1. API Linting and Convention Analysis
- **Resource Naming Conventions**: Enforces kebab-case for resources, camelCase for fields
- **HTTP Method Usage**: Validates proper use of GET, POST, PUT, PATCH, DELETE
- **URL Structure**: Analyzes endpoint patterns for consistency and RESTful design
- **Status Code Compliance**: Ensures appropriate HTTP status codes are used
- **Error Response Formats**: Validates consistent error response structures
- **Documentation Coverage**: Checks for missing descriptions and documentation gaps
### 2. Breaking Change Detection
- **Endpoint Removal**: Detects removed or deprecated endpoints
- **Response Shape Changes**: Identifies modifications to response structures
- **Field Removal**: Tracks removed or renamed fields in API responses
- **Type Changes**: Catches field type modifications that could break clients
- **Required Field Additions**: Flags new required fields that could break existing integrations
- **Status Code Changes**: Detects changes to expected status codes
### 3. API Design Scoring and Assessment
- **Consistency Analysis** (30%): Evaluates naming conventions, response patterns, and structural consistency
- **Documentation Quality** (20%): Assesses completeness and clarity of API documentation
- **Security Implementation** (20%): Reviews authentication, authorization, and security headers
- **Usability Design** (15%): Analyzes ease of use, discoverability, and developer experience
- **Performance Patterns** (15%): Evaluates caching, pagination, and efficiency patterns
## REST Design Principles
### Resource Naming Conventions
```
✅ Good Examples:
- /api/v1/users
- /api/v1/user-profiles
- /api/v1/orders/123/line-items
❌ Bad Examples:
- /api/v1/getUsers
- /api/v1/user_profiles
- /api/v1/orders/123/lineItems
```
### HTTP Method Usage
- **GET**: Retrieve resources (safe, idempotent)
- **POST**: Create new resources (not idempotent)
- **PUT**: Replace entire resources (idempotent)
- **PATCH**: Partial resource updates (not necessarily idempotent)
- **DELETE**: Remove resources (idempotent)
### URL Structure Best Practices
```
Collection Resources: /api/v1/users
Individual Resources: /api/v1/users/123
Nested Resources: /api/v1/users/123/orders
Actions: /api/v1/users/123/activate (POST)
Filtering: /api/v1/users?status=active&role=admin
```
## Versioning Strategies
### 1. URL Versioning (Recommended)
```
/api/v1/users
/api/v2/users
```
**Pros**: Clear, explicit, easy to route
**Cons**: URL proliferation, caching complexity
### 2. Header Versioning
```
GET /api/users
Accept: application/vnd.api+json;version=1
```
**Pros**: Clean URLs, content negotiation
**Cons**: Less visible, harder to test manually
### 3. Media Type Versioning
```
GET /api/users
Accept: application/vnd.myapi.v1+json
```
**Pros**: RESTful, supports multiple representations
**Cons**: Complex, harder to implement
### 4. Query Parameter Versioning
```
/api/users?version=1
```
**Pros**: Simple to implement
**Cons**: Not RESTful, can be ignored
## Pagination Patterns
### Offset-Based Pagination
```json
{
"data": [...],
"pagination": {
"offset": 20,
"limit": 10,
"total": 150,
"hasMore": true
}
}
```
### Cursor-Based Pagination
```json
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTIzfQ==",
"hasMore": true
}
}
```
### Page-Based Pagination
```json
{
"data": [...],
"pagination": {
"page": 3,
"pageSize": 10,
"totalPages": 15,
"totalItems": 150
}
}
```
## Error Response Formats
### Standard Error Structure
```json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request contains invalid parameters",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Email address is not valid"
}
],
"requestId": "req-123456",
"timestamp": "2024-02-16T13:00:00Z"
}
}
```
### HTTP Status Code Usage
- **400 Bad Request**: Invalid request syntax or parameters
- **401 Unauthorized**: Authentication required
- **403 Forbidden**: Access denied (authenticated but not authorized)
- **404 Not Found**: Resource not found
- **409 Conflict**: Resource conflict (duplicate, version mismatch)
- **422 Unprocessable Entity**: Valid syntax but semantic errors
- **429 Too Many Requests**: Rate limit exceeded
- **500 Internal Server Error**: Unexpected server error
## Authentication and Authorization Patterns
### Bearer Token Authentication
```
Authorization: Bearer <token>
```
### API Key Authentication
```
X-API-Key: <api-key>
Authorization: Api-Key <api-key>
```
### OAuth 2.0 Flow
```
Authorization: Bearer <oauth-access-token>
```
### Role-Based Access Control (RBAC)
```json
{
"user": {
"id": "123",
"roles": ["admin", "editor"],
"permissions": ["read:users", "write:orders"]
}
}
```
## Rate Limiting Implementation
### Headers
```
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1640995200
```
### Response on Limit Exceeded
```json
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests",
"retryAfter": 3600
}
}
```
## HATEOAS (Hypermedia as the Engine of Application State)
### Example Implementation
```json
{
"id": "123",
"name": "John Doe",
"email": "john@example.com",
"_links": {
"self": { "href": "/api/v1/users/123" },
"orders": { "href": "/api/v1/users/123/orders" },
"profile": { "href": "/api/v1/users/123/profile" },
"deactivate": {
"href": "/api/v1/users/123/deactivate",
"method": "POST"
}
}
}
```
## Idempotency
### Idempotent Methods
- **GET**: Always safe and idempotent
- **PUT**: Should be idempotent (replace entire resource)
- **DELETE**: Should be idempotent (same result)
- **PATCH**: May or may not be idempotent
### Idempotency Keys
```
POST /api/v1/payments
Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000
```
## Backward Compatibility Guidelines
### Safe Changes (Non-Breaking)
- Adding optional fields to requests
- Adding fields to responses
- Adding new endpoints
- Making required fields optional
- Adding new enum values (with graceful handling)
### Breaking Changes (Require Version Bump)
- Removing fields from responses
- Making optional fields required
- Changing field types
- Removing endpoints
- Changing URL structures
- Modifying error response formats
## OpenAPI/Swagger Validation
### Required Components
- **API Information**: Title, description, version
- **Server Information**: Base URLs and descriptions
- **Path Definitions**: All endpoints with methods
- **Parameter Definitions**: Query, path, header parameters
- **Request/Response Schemas**: Complete data models
- **Security Definitions**: Authentication schemes
- **Error Responses**: Standard error formats
### Best Practices
- Use consistent naming conventions
- Provide detailed descriptions for all components
- Include examples for complex objects
- Define reusable components and schemas
- Validate against OpenAPI specification
## Performance Considerations
### Caching Strategies
```
Cache-Control: public, max-age=3600
ETag: "123456789"
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
```
### Efficient Data Transfer
- Use appropriate HTTP methods
- Implement field selection (`?fields=id,name,email`)
- Support compression (gzip)
- Implement efficient pagination
- Use ETags for conditional requests
### Resource Optimization
- Avoid N+1 queries
- Implement batch operations
- Use async processing for heavy operations
- Support partial updates (PATCH)
## Security Best Practices
### Input Validation
- Validate all input parameters
- Sanitize user data
- Use parameterized queries
- Implement request size limits
### Authentication Security
- Use HTTPS everywhere
- Implement secure token storage
- Support token expiration and refresh
- Use strong authentication mechanisms
### Authorization Controls
- Implement principle of least privilege
- Use resource-based permissions
- Support fine-grained access control
- Audit access patterns
## Tools and Scripts
### api_linter.py
Analyzes API specifications for compliance with REST conventions and best practices.
**Features:**
- OpenAPI/Swagger spec validation
- Naming convention checks
- HTTP method usage validation
- Error format consistency
- Documentation completeness analysis
### breaking_change_detector.py
Compares API specification versions to identify breaking changes.
**Features:**
- Endpoint comparison
- Schema change detection
- Field removal/modification tracking
- Migration guide generation
- Impact severity assessment
### api_scorecard.py
Provides comprehensive scoring of API design quality.
**Features:**
- Multi-dimensional scoring
- Detailed improvement recommendations
- Letter grade assessment (A-F)
- Benchmark comparisons
- Progress tracking
## Integration Examples
### CI/CD Integration
```yaml
- name: "api-linting"
run: python scripts/api_linter.py openapi.json
- name: "breaking-change-detection"
run: python scripts/breaking_change_detector.py openapi-v1.json openapi-v2.json
- name: "api-scorecard"
run: python scripts/api_scorecard.py openapi.json
```
### Pre-commit Hooks
```bash
#!/bin/bash
python engineering/api-design-reviewer/scripts/api_linter.py api/openapi.json
if [ $? -ne 0 ]; then
echo "API linting failed. Please fix the issues before committing."
exit 1
fi
```
## Best Practices Summary
1. **Consistency First**: Maintain consistent naming, response formats, and patterns
2. **Documentation**: Provide comprehensive, up-to-date API documentation
3. **Versioning**: Plan for evolution with clear versioning strategies
4. **Error Handling**: Implement consistent, informative error responses
5. **Security**: Build security into every layer of the API
6. **Performance**: Design for scale and efficiency from the start
7. **Backward Compatibility**: Minimize breaking changes and provide migration paths
8. **Testing**: Implement comprehensive testing including contract testing
9. **Monitoring**: Add observability for API usage and performance
10. **Developer Experience**: Prioritize ease of use and clear documentation
## Common Anti-Patterns to Avoid
1. **Verb-based URLs**: Use nouns for resources, not actions
2. **Inconsistent Response Formats**: Maintain standard response structures
3. **Over-nesting**: Avoid deeply nested resource hierarchies
4. **Ignoring HTTP Status Codes**: Use appropriate status codes for different scenarios
5. **Poor Error Messages**: Provide actionable, specific error information
6. **Missing Pagination**: Always paginate list endpoints
7. **No Versioning Strategy**: Plan for API evolution from day one
8. **Exposing Internal Structure**: Design APIs for external consumption, not internal convenience
9. **Missing Rate Limiting**: Protect your API from abuse and overload
10. **Inadequate Testing**: Test all aspects including error cases and edge conditions
## Conclusion
The API Design Reviewer skill provides a comprehensive framework for building, reviewing, and maintaining high-quality REST APIs. By following these guidelines and using the provided tools, development teams can create APIs that are consistent, well-documented, secure, and maintainable.
Regular use of the linting, breaking change detection, and scoring tools ensures continuous improvement and helps maintain API quality throughout the development lifecycle.
FILE:references/api_antipatterns.md
# Common API Anti-Patterns and How to Avoid Them
## Introduction
This document outlines common anti-patterns in REST API design that can lead to poor developer experience, maintenance nightmares, and scalability issues. Each anti-pattern is accompanied by examples and recommended solutions.
## 1. Verb-Based URLs (The RPC Trap)
### Anti-Pattern
Using verbs in URLs instead of treating endpoints as resources.
```
❌ Bad Examples:
POST /api/getUsers
POST /api/createUser
GET /api/deleteUser/123
POST /api/updateUserPassword
GET /api/calculateOrderTotal/456
```
### Why It's Bad
- Violates REST principles
- Makes the API feel like RPC instead of REST
- HTTP methods lose their semantic meaning
- Reduces cacheability
- Harder to understand resource relationships
### Solution
```
✅ Good Examples:
GET /api/users # Get users
POST /api/users # Create user
DELETE /api/users/123 # Delete user
PATCH /api/users/123/password # Update password
GET /api/orders/456/total # Get order total
```
## 2. Inconsistent Naming Conventions
### Anti-Pattern
Mixed naming conventions across the API.
```json
❌ Bad Examples:
{
"user_id": 123, // snake_case
"firstName": "John", // camelCase
"last-name": "Doe", // kebab-case
"EMAIL": "john@example.com", // UPPER_CASE
"IsActive": true // PascalCase
}
```
### Why It's Bad
- Confuses developers
- Increases cognitive load
- Makes code generation difficult
- Reduces API adoption
### Solution
```json
✅ Choose one convention and stick to it (camelCase recommended):
{
"userId": 123,
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"isActive": true
}
```
## 3. Ignoring HTTP Status Codes
### Anti-Pattern
Always returning HTTP 200 regardless of the actual result.
```json
❌ Bad Example:
HTTP/1.1 200 OK
{
"status": "error",
"code": 404,
"message": "User not found"
}
```
### Why It's Bad
- Breaks HTTP semantics
- Prevents proper error handling by clients
- Breaks caching and proxies
- Makes monitoring and debugging harder
### Solution
```json
✅ Good Example:
HTTP/1.1 404 Not Found
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User with ID 123 not found",
"requestId": "req-abc123"
}
}
```
## 4. Overly Complex Nested Resources
### Anti-Pattern
Creating deeply nested URL structures that are hard to navigate.
```
❌ Bad Example:
/companies/123/departments/456/teams/789/members/012/projects/345/tasks/678/comments/901
```
### Why It's Bad
- URLs become unwieldy
- Creates tight coupling between resources
- Makes independent resource access difficult
- Complicates authorization logic
### Solution
```
✅ Good Examples:
/tasks/678 # Direct access to task
/tasks/678/comments # Task comments
/users/012/tasks # User's tasks
/projects/345?team=789 # Project filtering
```
## 5. Inconsistent Error Response Formats
### Anti-Pattern
Different error response structures across endpoints.
```json
❌ Bad Examples:
# Endpoint 1
{"error": "Invalid email"}
# Endpoint 2
{"success": false, "msg": "User not found", "code": 404}
# Endpoint 3
{"errors": [{"field": "name", "message": "Required"}]}
```
### Why It's Bad
- Makes error handling complex for clients
- Reduces code reusability
- Poor developer experience
### Solution
```json
✅ Standardized Error Format:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request contains invalid data",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Email address is not valid"
}
],
"requestId": "req-123456",
"timestamp": "2024-02-16T13:00:00Z"
}
}
```
## 6. Missing or Poor Pagination
### Anti-Pattern
Returning all results in a single response or inconsistent pagination.
```json
❌ Bad Examples:
# No pagination (returns 10,000 records)
GET /api/users
# Inconsistent pagination parameters
GET /api/users?page=1&size=10
GET /api/orders?offset=0&limit=20
GET /api/products?start=0&count=50
```
### Why It's Bad
- Can cause performance issues
- May overwhelm clients
- Inconsistent pagination parameters confuse developers
- No way to estimate total results
### Solution
```json
✅ Good Example:
GET /api/users?page=1&pageSize=10
{
"data": [...],
"pagination": {
"page": 1,
"pageSize": 10,
"total": 150,
"totalPages": 15,
"hasNext": true,
"hasPrev": false
}
}
```
## 7. Exposing Internal Implementation Details
### Anti-Pattern
URLs and field names that reflect database structure or internal architecture.
```
❌ Bad Examples:
/api/user_table/123
/api/db_orders
/api/legacy_customer_data
/api/temp_migration_users
Response fields:
{
"user_id_pk": 123,
"internal_ref_code": "usr_abc",
"db_created_timestamp": 1645123456
}
```
### Why It's Bad
- Couples API to internal implementation
- Makes refactoring difficult
- Exposes unnecessary technical details
- Reduces API longevity
### Solution
```
✅ Good Examples:
/api/users/123
/api/orders
/api/customers
Response fields:
{
"id": 123,
"referenceCode": "usr_abc",
"createdAt": "2024-02-16T13:00:00Z"
}
```
## 8. Overloading Single Endpoint
### Anti-Pattern
Using one endpoint for multiple unrelated operations based on request parameters.
```
❌ Bad Example:
POST /api/user-actions
{
"action": "create_user",
"userData": {...}
}
POST /api/user-actions
{
"action": "delete_user",
"userId": 123
}
POST /api/user-actions
{
"action": "send_email",
"userId": 123,
"emailType": "welcome"
}
```
### Why It's Bad
- Breaks REST principles
- Makes documentation complex
- Complicates client implementation
- Reduces discoverability
### Solution
```
✅ Good Examples:
POST /api/users # Create user
DELETE /api/users/123 # Delete user
POST /api/users/123/emails # Send email to user
```
## 9. Lack of Versioning Strategy
### Anti-Pattern
Making breaking changes without version management.
```
❌ Bad Examples:
# Original API
{
"name": "John Doe",
"age": 30
}
# Later (breaking change with no versioning)
{
"firstName": "John",
"lastName": "Doe",
"birthDate": "1994-02-16"
}
```
### Why It's Bad
- Breaks existing clients
- Forces all clients to update simultaneously
- No graceful migration path
- Reduces API stability
### Solution
```
✅ Good Examples:
# Version 1
GET /api/v1/users/123
{
"name": "John Doe",
"age": 30
}
# Version 2 (with both versions supported)
GET /api/v2/users/123
{
"firstName": "John",
"lastName": "Doe",
"birthDate": "1994-02-16",
"age": 30 // Backwards compatibility
}
```
## 10. Poor Error Messages
### Anti-Pattern
Vague, unhelpful, or technical error messages.
```json
❌ Bad Examples:
{"error": "Something went wrong"}
{"error": "Invalid input"}
{"error": "SQL constraint violation: FK_user_profile_id"}
{"error": "NullPointerException at line 247"}
```
### Why It's Bad
- Doesn't help developers fix issues
- Increases support burden
- Poor developer experience
- May expose sensitive information
### Solution
```json
✅ Good Examples:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The email address is required and must be in a valid format",
"details": [
{
"field": "email",
"code": "REQUIRED",
"message": "Email address is required"
}
]
}
}
```
## 11. Ignoring Content Negotiation
### Anti-Pattern
Hard-coding response format without considering client preferences.
```
❌ Bad Example:
# Always returns JSON regardless of Accept header
GET /api/users/123
Accept: application/xml
# Returns JSON anyway
```
### Why It's Bad
- Reduces API flexibility
- Ignores HTTP standards
- Makes integration harder for diverse clients
### Solution
```
✅ Good Example:
GET /api/users/123
Accept: application/xml
HTTP/1.1 200 OK
Content-Type: application/xml
<?xml version="1.0"?>
<user>
<id>123</id>
<name>John Doe</name>
</user>
```
## 12. Stateful API Design
### Anti-Pattern
Maintaining session state on the server between requests.
```
❌ Bad Example:
# Step 1: Initialize session
POST /api/session/init
# Step 2: Set context (requires step 1)
POST /api/session/set-user/123
# Step 3: Get data (requires steps 1 & 2)
GET /api/session/user-data
```
### Why It's Bad
- Breaks REST statelessness principle
- Reduces scalability
- Makes caching difficult
- Complicates error recovery
### Solution
```
✅ Good Example:
# Self-contained requests
GET /api/users/123/data
Authorization: Bearer jwt-token-with-context
```
## 13. Inconsistent HTTP Method Usage
### Anti-Pattern
Using HTTP methods inappropriately or inconsistently.
```
❌ Bad Examples:
GET /api/users/123/delete # DELETE operation with GET
POST /api/users/123/get # GET operation with POST
PUT /api/users # Creating with PUT on collection
GET /api/users/search # Search with side effects
```
### Why It's Bad
- Violates HTTP semantics
- Breaks caching and idempotency expectations
- Confuses developers and tools
### Solution
```
✅ Good Examples:
DELETE /api/users/123 # Delete with DELETE
GET /api/users/123 # Get with GET
POST /api/users # Create on collection
GET /api/users?q=search # Safe search with GET
```
## 14. Missing Rate Limiting Information
### Anti-Pattern
Not providing rate limiting information to clients.
```
❌ Bad Example:
HTTP/1.1 429 Too Many Requests
{
"error": "Rate limit exceeded"
}
```
### Why It's Bad
- Clients don't know when to retry
- No information about current limits
- Difficult to implement proper backoff strategies
### Solution
```
✅ Good Example:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995200
Retry-After: 3600
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "API rate limit exceeded",
"retryAfter": 3600
}
}
```
## 15. Chatty API Design
### Anti-Pattern
Requiring multiple API calls to accomplish common tasks.
```
❌ Bad Example:
# Get user profile requires 4 API calls
GET /api/users/123 # Basic info
GET /api/users/123/profile # Profile details
GET /api/users/123/settings # User settings
GET /api/users/123/stats # User statistics
```
### Why It's Bad
- Increases latency
- Creates network overhead
- Makes mobile apps inefficient
- Complicates client implementation
### Solution
```
✅ Good Examples:
# Single call with expansion
GET /api/users/123?include=profile,settings,stats
# Or provide composite endpoints
GET /api/users/123/dashboard
# Or batch operations
POST /api/batch
{
"requests": [
{"method": "GET", "url": "/users/123"},
{"method": "GET", "url": "/users/123/profile"}
]
}
```
## 16. No Input Validation
### Anti-Pattern
Accepting and processing invalid input without proper validation.
```json
❌ Bad Example:
POST /api/users
{
"email": "not-an-email",
"age": -5,
"name": ""
}
# API processes this and fails later or stores invalid data
```
### Why It's Bad
- Leads to data corruption
- Security vulnerabilities
- Difficult to debug issues
- Poor user experience
### Solution
```json
✅ Good Example:
POST /api/users
{
"email": "not-an-email",
"age": -5,
"name": ""
}
HTTP/1.1 400 Bad Request
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request contains invalid data",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Email must be a valid email address"
},
{
"field": "age",
"code": "INVALID_RANGE",
"message": "Age must be between 0 and 150"
},
{
"field": "name",
"code": "REQUIRED",
"message": "Name is required and cannot be empty"
}
]
}
}
```
## 17. Synchronous Long-Running Operations
### Anti-Pattern
Blocking the client with long-running operations in synchronous endpoints.
```
❌ Bad Example:
POST /api/reports/generate
# Client waits 30 seconds for response
```
### Why It's Bad
- Poor user experience
- Timeouts and connection issues
- Resource waste on client and server
- Doesn't scale well
### Solution
```
✅ Good Example:
# Async pattern
POST /api/reports
HTTP/1.1 202 Accepted
Location: /api/reports/job-123
{
"jobId": "job-123",
"status": "processing",
"estimatedCompletion": "2024-02-16T13:05:00Z"
}
# Check status
GET /api/reports/job-123
{
"jobId": "job-123",
"status": "completed",
"result": "/api/reports/download/report-456"
}
```
## Prevention Strategies
### 1. API Design Reviews
- Implement mandatory design reviews
- Use checklists based on these anti-patterns
- Include multiple stakeholders
### 2. API Style Guides
- Create and enforce API style guides
- Use linting tools for consistency
- Regular training for development teams
### 3. Automated Testing
- Test for common anti-patterns
- Include contract testing
- Monitor API usage patterns
### 4. Documentation Standards
- Require comprehensive API documentation
- Include examples and error scenarios
- Keep documentation up-to-date
### 5. Client Feedback
- Regularly collect feedback from API consumers
- Monitor API usage analytics
- Conduct developer experience surveys
## Conclusion
Avoiding these anti-patterns requires:
- Understanding REST principles
- Consistent design standards
- Regular review and refactoring
- Focus on developer experience
- Proper tooling and automation
Remember: A well-designed API is an asset that grows in value over time, while a poorly designed API becomes a liability that hampers development and adoption.
FILE:references/rest_design_rules.md
# REST API Design Rules Reference
## Core Principles
### 1. Resources, Not Actions
REST APIs should focus on **resources** (nouns) rather than **actions** (verbs). The HTTP methods provide the actions.
```
✅ Good:
GET /users # Get all users
GET /users/123 # Get user 123
POST /users # Create new user
PUT /users/123 # Update user 123
DELETE /users/123 # Delete user 123
❌ Bad:
POST /getUsers
POST /createUser
POST /updateUser/123
POST /deleteUser/123
```
### 2. Hierarchical Resource Structure
Use hierarchical URLs to represent resource relationships:
```
/users/123/orders/456/items/789
```
But avoid excessive nesting (max 3-4 levels):
```
❌ Too deep: /companies/123/departments/456/teams/789/members/012/tasks/345
✅ Better: /tasks/345?member=012&team=789
```
## Resource Naming Conventions
### URLs Should Use Kebab-Case
```
✅ Good:
/user-profiles
/order-items
/shipping-addresses
❌ Bad:
/userProfiles
/user_profiles
/orderItems
```
### Collections vs Individual Resources
```
Collection: /users
Individual: /users/123
Sub-resource: /users/123/orders
```
### Pluralization Rules
- Use **plural nouns** for collections: `/users`, `/orders`
- Use **singular nouns** for single resources: `/user-profile`, `/current-session`
- Be consistent throughout your API
## HTTP Methods Usage
### GET - Safe and Idempotent
- **Purpose**: Retrieve data
- **Safe**: No side effects
- **Idempotent**: Multiple calls return same result
- **Request Body**: Should not have one
- **Cacheable**: Yes
```
GET /users/123
GET /users?status=active&limit=10
```
### POST - Not Idempotent
- **Purpose**: Create resources, non-idempotent operations
- **Safe**: No
- **Idempotent**: No
- **Request Body**: Usually required
- **Cacheable**: Generally no
```
POST /users # Create new user
POST /users/123/activate # Activate user (action)
```
### PUT - Idempotent
- **Purpose**: Create or completely replace a resource
- **Safe**: No
- **Idempotent**: Yes
- **Request Body**: Required (complete resource)
- **Cacheable**: No
```
PUT /users/123 # Replace entire user resource
```
### PATCH - Partial Update
- **Purpose**: Partially update a resource
- **Safe**: No
- **Idempotent**: Not necessarily
- **Request Body**: Required (partial resource)
- **Cacheable**: No
```
PATCH /users/123 # Update only specified fields
```
### DELETE - Idempotent
- **Purpose**: Remove a resource
- **Safe**: No
- **Idempotent**: Yes (same result if called multiple times)
- **Request Body**: Usually not needed
- **Cacheable**: No
```
DELETE /users/123
```
## Status Codes
### Success Codes (2xx)
- **200 OK**: Standard success response
- **201 Created**: Resource created successfully (POST)
- **202 Accepted**: Request accepted for processing (async)
- **204 No Content**: Success with no response body (DELETE, PUT)
### Redirection Codes (3xx)
- **301 Moved Permanently**: Resource permanently moved
- **302 Found**: Temporary redirect
- **304 Not Modified**: Use cached version
### Client Error Codes (4xx)
- **400 Bad Request**: Invalid request syntax or data
- **401 Unauthorized**: Authentication required
- **403 Forbidden**: Access denied (user authenticated but not authorized)
- **404 Not Found**: Resource not found
- **405 Method Not Allowed**: HTTP method not supported
- **409 Conflict**: Resource conflict (duplicates, version mismatch)
- **422 Unprocessable Entity**: Valid syntax but semantic errors
- **429 Too Many Requests**: Rate limit exceeded
### Server Error Codes (5xx)
- **500 Internal Server Error**: Unexpected server error
- **502 Bad Gateway**: Invalid response from upstream server
- **503 Service Unavailable**: Server temporarily unavailable
- **504 Gateway Timeout**: Upstream server timeout
## URL Design Patterns
### Query Parameters for Filtering
```
GET /users?status=active
GET /users?role=admin&department=engineering
GET /orders?created_after=2024-01-01&status=pending
```
### Pagination Parameters
```
# Offset-based
GET /users?offset=20&limit=10
# Cursor-based
GET /users?cursor=eyJpZCI6MTIzfQ&limit=10
# Page-based
GET /users?page=3&page_size=10
```
### Sorting Parameters
```
GET /users?sort=created_at # Ascending
GET /users?sort=-created_at # Descending (prefix with -)
GET /users?sort=last_name,first_name # Multiple fields
```
### Field Selection
```
GET /users?fields=id,name,email
GET /users/123?include=orders,profile
GET /users/123?exclude=internal_notes
```
### Search Parameters
```
GET /users?q=john
GET /products?search=laptop&category=electronics
```
## Response Format Standards
### Consistent Response Structure
```json
{
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com"
},
"meta": {
"timestamp": "2024-02-16T13:00:00Z",
"version": "1.0"
}
}
```
### Collection Responses
```json
{
"data": [
{"id": 1, "name": "Item 1"},
{"id": 2, "name": "Item 2"}
],
"pagination": {
"total": 150,
"page": 1,
"pageSize": 10,
"totalPages": 15,
"hasNext": true,
"hasPrev": false
},
"meta": {
"timestamp": "2024-02-16T13:00:00Z"
}
}
```
### Error Response Format
```json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request contains invalid parameters",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Email address is not valid"
}
],
"requestId": "req-123456",
"timestamp": "2024-02-16T13:00:00Z"
}
}
```
## Field Naming Conventions
### Use camelCase for JSON Fields
```json
✅ Good:
{
"firstName": "John",
"lastName": "Doe",
"createdAt": "2024-02-16T13:00:00Z",
"isActive": true
}
❌ Bad:
{
"first_name": "John",
"LastName": "Doe",
"created-at": "2024-02-16T13:00:00Z"
}
```
### Boolean Fields
Use positive, clear names with "is", "has", "can", or "should" prefixes:
```json
✅ Good:
{
"isActive": true,
"hasPermission": false,
"canEdit": true,
"shouldNotify": false
}
❌ Bad:
{
"active": true,
"disabled": false, // Double negative
"permission": false // Unclear meaning
}
```
### Date/Time Fields
- Use ISO 8601 format: `2024-02-16T13:00:00Z`
- Include timezone information
- Use consistent field naming:
```json
{
"createdAt": "2024-02-16T13:00:00Z",
"updatedAt": "2024-02-16T13:30:00Z",
"deletedAt": null,
"publishedAt": "2024-02-16T14:00:00Z"
}
```
## Content Negotiation
### Accept Headers
```
Accept: application/json
Accept: application/xml
Accept: application/json; version=1
```
### Content-Type Headers
```
Content-Type: application/json
Content-Type: application/json; charset=utf-8
Content-Type: multipart/form-data
```
### Versioning via Headers
```
Accept: application/vnd.myapi.v1+json
API-Version: 1.0
```
## Caching Guidelines
### Cache-Control Headers
```
Cache-Control: public, max-age=3600 # Cache for 1 hour
Cache-Control: private, max-age=0 # Don't cache
Cache-Control: no-cache, must-revalidate # Always validate
```
### ETags for Conditional Requests
```
HTTP/1.1 200 OK
ETag: "123456789"
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
# Client subsequent request:
If-None-Match: "123456789"
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
```
## Security Headers
### Authentication
```
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Authorization: Basic dXNlcjpwYXNzd29yZA==
Authorization: Api-Key abc123def456
```
### CORS Headers
```
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
```
## Rate Limiting
### Rate Limit Headers
```
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1640995200
X-RateLimit-Window: 3600
```
### Rate Limit Exceeded Response
```json
HTTP/1.1 429 Too Many Requests
Retry-After: 3600
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "API rate limit exceeded",
"details": {
"limit": 1000,
"window": "1 hour",
"retryAfter": 3600
}
}
}
```
## Hypermedia (HATEOAS)
### Links in Responses
```json
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"_links": {
"self": {
"href": "/users/123"
},
"orders": {
"href": "/users/123/orders"
},
"edit": {
"href": "/users/123",
"method": "PUT"
},
"delete": {
"href": "/users/123",
"method": "DELETE"
}
}
}
```
### Link Relations
- **self**: Link to the resource itself
- **edit**: Link to edit the resource
- **delete**: Link to delete the resource
- **related**: Link to related resources
- **next/prev**: Pagination links
## Common Anti-Patterns to Avoid
### 1. Verbs in URLs
```
❌ Bad: /api/getUser/123
✅ Good: GET /api/users/123
```
### 2. Inconsistent Naming
```
❌ Bad: /user-profiles and /userAddresses
✅ Good: /user-profiles and /user-addresses
```
### 3. Deep Nesting
```
❌ Bad: /companies/123/departments/456/teams/789/members/012
✅ Good: /team-members/012?team=789
```
### 4. Ignoring HTTP Status Codes
```
❌ Bad: Always return 200 with error info in body
✅ Good: Use appropriate status codes (404, 400, 500, etc.)
```
### 5. Exposing Internal Structure
```
❌ Bad: /api/database_table_users
✅ Good: /api/users
```
### 6. No Versioning Strategy
```
❌ Bad: Breaking changes without version management
✅ Good: /api/v1/users or Accept: application/vnd.api+json;version=1
```
### 7. Inconsistent Error Responses
```
❌ Bad: Different error formats for different endpoints
✅ Good: Standardized error response structure
```
## Best Practices Summary
1. **Use nouns for resources, not verbs**
2. **Leverage HTTP methods correctly**
3. **Maintain consistent naming conventions**
4. **Implement proper error handling**
5. **Use appropriate HTTP status codes**
6. **Design for cacheability**
7. **Implement security from the start**
8. **Plan for versioning**
9. **Provide comprehensive documentation**
10. **Follow HATEOAS principles when applicable**
## Further Reading
- [RFC 7231 - HTTP/1.1 Semantics and Content](https://tools.ietf.org/html/rfc7231)
- [RFC 6570 - URI Template](https://tools.ietf.org/html/rfc6570)
- [OpenAPI Specification](https://swagger.io/specification/)
- [REST API Design Best Practices](https://www.restapitutorial.com/)
- [HTTP Status Code Definitions](https://httpstatuses.com/)
FILE:scripts/api_linter.py
#!/usr/bin/env python3
"""
API Linter - Analyzes OpenAPI/Swagger specifications for REST conventions and best practices.
This script validates API designs against established conventions including:
- Resource naming conventions (kebab-case resources, camelCase fields)
- HTTP method usage patterns
- URL structure consistency
- Error response format standards
- Documentation completeness
- Pagination patterns
- Versioning compliance
Supports both OpenAPI JSON specifications and raw endpoint definition JSON.
"""
import argparse
import json
import re
import sys
from typing import Any, Dict, List, Tuple, Optional, Set
from urllib.parse import urlparse
from dataclasses import dataclass, field
@dataclass
class LintIssue:
"""Represents a linting issue found in the API specification."""
severity: str # 'error', 'warning', 'info'
category: str
message: str
path: str
suggestion: str = ""
line_number: Optional[int] = None
@dataclass
class LintReport:
"""Complete linting report with issues and statistics."""
issues: List[LintIssue] = field(default_factory=list)
total_endpoints: int = 0
endpoints_with_issues: int = 0
score: float = 0.0
def add_issue(self, issue: LintIssue) -> None:
"""Add an issue to the report."""
self.issues.append(issue)
def get_issues_by_severity(self) -> Dict[str, List[LintIssue]]:
"""Group issues by severity level."""
grouped = {'error': [], 'warning': [], 'info': []}
for issue in self.issues:
if issue.severity in grouped:
grouped[issue.severity].append(issue)
return grouped
def calculate_score(self) -> float:
"""Calculate overall API quality score (0-100)."""
if self.total_endpoints == 0:
return 100.0
error_penalty = len([i for i in self.issues if i.severity == 'error']) * 10
warning_penalty = len([i for i in self.issues if i.severity == 'warning']) * 3
info_penalty = len([i for i in self.issues if i.severity == 'info']) * 1
total_penalty = error_penalty + warning_penalty + info_penalty
base_score = 100.0
# Penalty per endpoint to normalize across API sizes
penalty_per_endpoint = total_penalty / self.total_endpoints if self.total_endpoints > 0 else total_penalty
self.score = max(0.0, base_score - penalty_per_endpoint)
return self.score
class APILinter:
"""Main API linting engine."""
def __init__(self):
self.report = LintReport()
self.openapi_spec: Optional[Dict] = None
self.raw_endpoints: Optional[Dict] = None
# Regex patterns for naming conventions
self.kebab_case_pattern = re.compile(r'^[a-z]+(?:-[a-z0-9]+)*$')
self.camel_case_pattern = re.compile(r'^[a-z][a-zA-Z0-9]*$')
self.snake_case_pattern = re.compile(r'^[a-z]+(?:_[a-z0-9]+)*$')
self.pascal_case_pattern = re.compile(r'^[A-Z][a-zA-Z0-9]*$')
# Standard HTTP methods
self.http_methods = {'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'}
# Standard HTTP status codes by method
self.standard_status_codes = {
'GET': {200, 304, 404},
'POST': {200, 201, 400, 409, 422},
'PUT': {200, 204, 400, 404, 409},
'PATCH': {200, 204, 400, 404, 409},
'DELETE': {200, 204, 404},
'HEAD': {200, 404},
'OPTIONS': {200}
}
# Common error status codes
self.common_error_codes = {400, 401, 403, 404, 405, 409, 422, 429, 500, 502, 503}
def lint_openapi_spec(self, spec: Dict[str, Any]) -> LintReport:
"""Lint an OpenAPI/Swagger specification."""
self.openapi_spec = spec
self.report = LintReport()
# Basic structure validation
self._validate_openapi_structure()
# Info section validation
self._validate_info_section()
# Server section validation
self._validate_servers_section()
# Paths validation (main linting logic)
self._validate_paths_section()
# Components validation
self._validate_components_section()
# Security validation
self._validate_security_section()
# Calculate final score
self.report.calculate_score()
return self.report
def lint_raw_endpoints(self, endpoints: Dict[str, Any]) -> LintReport:
"""Lint raw endpoint definitions."""
self.raw_endpoints = endpoints
self.report = LintReport()
# Validate raw endpoint structure
self._validate_raw_endpoint_structure()
# Lint each endpoint
for endpoint_path, endpoint_data in endpoints.get('endpoints', {}).items():
self._lint_raw_endpoint(endpoint_path, endpoint_data)
self.report.calculate_score()
return self.report
def _validate_openapi_structure(self) -> None:
"""Validate basic OpenAPI document structure."""
required_fields = ['openapi', 'info', 'paths']
for field in required_fields:
if field not in self.openapi_spec:
self.report.add_issue(LintIssue(
severity='error',
category='structure',
message=f"Missing required field: {field}",
path=f"/{field}",
suggestion=f"Add the '{field}' field to the root of your OpenAPI specification"
))
def _validate_info_section(self) -> None:
"""Validate the info section of OpenAPI spec."""
if 'info' not in self.openapi_spec:
return
info = self.openapi_spec['info']
required_info_fields = ['title', 'version']
recommended_info_fields = ['description', 'contact']
for field in required_info_fields:
if field not in info:
self.report.add_issue(LintIssue(
severity='error',
category='documentation',
message=f"Missing required info field: {field}",
path=f"/info/{field}",
suggestion=f"Add a '{field}' field to the info section"
))
for field in recommended_info_fields:
if field not in info:
self.report.add_issue(LintIssue(
severity='warning',
category='documentation',
message=f"Missing recommended info field: {field}",
path=f"/info/{field}",
suggestion=f"Consider adding a '{field}' field to improve API documentation"
))
# Validate version format
if 'version' in info:
version = info['version']
if not re.match(r'^\d+\.\d+(\.\d+)?(-\w+)?$', version):
self.report.add_issue(LintIssue(
severity='warning',
category='versioning',
message=f"Version format '{version}' doesn't follow semantic versioning",
path="/info/version",
suggestion="Use semantic versioning format (e.g., '1.0.0', '2.1.3-beta')"
))
def _validate_servers_section(self) -> None:
"""Validate the servers section."""
if 'servers' not in self.openapi_spec:
self.report.add_issue(LintIssue(
severity='warning',
category='configuration',
message="Missing servers section",
path="/servers",
suggestion="Add a servers section to specify API base URLs"
))
return
servers = self.openapi_spec['servers']
if not isinstance(servers, list) or len(servers) == 0:
self.report.add_issue(LintIssue(
severity='warning',
category='configuration',
message="Empty servers section",
path="/servers",
suggestion="Add at least one server URL"
))
def _validate_paths_section(self) -> None:
"""Validate all API paths and operations."""
if 'paths' not in self.openapi_spec:
return
paths = self.openapi_spec['paths']
if not paths:
self.report.add_issue(LintIssue(
severity='error',
category='structure',
message="No paths defined in API specification",
path="/paths",
suggestion="Define at least one API endpoint"
))
return
self.report.total_endpoints = sum(
len([method for method in path_obj.keys() if method.upper() in self.http_methods])
for path_obj in paths.values() if isinstance(path_obj, dict)
)
endpoints_with_issues = set()
for path, path_obj in paths.items():
if not isinstance(path_obj, dict):
continue
# Validate path structure
path_issues = self._validate_path_structure(path)
if path_issues:
endpoints_with_issues.add(path)
# Validate each operation in the path
for method, operation in path_obj.items():
if method.upper() not in self.http_methods:
continue
operation_issues = self._validate_operation(path, method.upper(), operation)
if operation_issues:
endpoints_with_issues.add(path)
self.report.endpoints_with_issues = len(endpoints_with_issues)
def _validate_path_structure(self, path: str) -> bool:
"""Validate REST path structure and naming conventions."""
has_issues = False
# Check if path starts with slash
if not path.startswith('/'):
self.report.add_issue(LintIssue(
severity='error',
category='url_structure',
message=f"Path must start with '/' character: {path}",
path=f"/paths/{path}",
suggestion=f"Change '{path}' to '/{path.lstrip('/')}'"
))
has_issues = True
# Split path into segments
segments = [seg for seg in path.split('/') if seg]
# Check for empty segments (double slashes)
if '//' in path:
self.report.add_issue(LintIssue(
severity='error',
category='url_structure',
message=f"Path contains empty segments: {path}",
path=f"/paths/{path}",
suggestion="Remove double slashes from the path"
))
has_issues = True
# Validate each segment
for i, segment in enumerate(segments):
# Skip parameter segments
if segment.startswith('{') and segment.endswith('}'):
# Validate parameter naming
param_name = segment[1:-1]
if not self.camel_case_pattern.match(param_name) and not self.kebab_case_pattern.match(param_name):
self.report.add_issue(LintIssue(
severity='warning',
category='naming',
message=f"Path parameter '{param_name}' should use camelCase or kebab-case",
path=f"/paths/{path}",
suggestion=f"Use camelCase (e.g., 'userId') or kebab-case (e.g., 'user-id')"
))
has_issues = True
continue
# Check for resource naming conventions
if not self.kebab_case_pattern.match(segment):
# Allow version segments like 'v1', 'v2'
if not re.match(r'^v\d+$', segment):
self.report.add_issue(LintIssue(
severity='warning',
category='naming',
message=f"Resource segment '{segment}' should use kebab-case",
path=f"/paths/{path}",
suggestion=f"Use kebab-case for '{segment}' (e.g., 'user-profiles', 'order-items')"
))
has_issues = True
# Check for verb usage in URLs (anti-pattern)
common_verbs = {'get', 'post', 'put', 'delete', 'create', 'update', 'remove', 'add'}
if segment.lower() in common_verbs:
self.report.add_issue(LintIssue(
severity='warning',
category='rest_conventions',
message=f"Avoid verbs in URLs: '{segment}' in {path}",
path=f"/paths/{path}",
suggestion="Use HTTP methods instead of verbs in URLs. Use nouns for resources."
))
has_issues = True
# Check path depth (avoid over-nesting)
if len(segments) > 6:
self.report.add_issue(LintIssue(
severity='warning',
category='url_structure',
message=f"Path has excessive nesting ({len(segments)} levels): {path}",
path=f"/paths/{path}",
suggestion="Consider flattening the resource hierarchy or using query parameters"
))
has_issues = True
# Check for consistent versioning
if any('v' + str(i) in segments for i in range(1, 10)):
version_segments = [seg for seg in segments if re.match(r'^v\d+$', seg)]
if len(version_segments) > 1:
self.report.add_issue(LintIssue(
severity='error',
category='versioning',
message=f"Multiple version segments in path: {path}",
path=f"/paths/{path}",
suggestion="Use only one version segment per path"
))
has_issues = True
return has_issues
def _validate_operation(self, path: str, method: str, operation: Dict[str, Any]) -> bool:
"""Validate individual operation (HTTP method + path combination)."""
has_issues = False
operation_path = f"/paths/{path}/{method.lower()}"
# Check for required operation fields
if 'responses' not in operation:
self.report.add_issue(LintIssue(
severity='error',
category='structure',
message=f"Missing responses section for {method} {path}",
path=f"{operation_path}/responses",
suggestion="Define expected responses for this operation"
))
has_issues = True
# Check for operation documentation
if 'summary' not in operation:
self.report.add_issue(LintIssue(
severity='warning',
category='documentation',
message=f"Missing summary for {method} {path}",
path=f"{operation_path}/summary",
suggestion="Add a brief summary describing what this operation does"
))
has_issues = True
if 'description' not in operation:
self.report.add_issue(LintIssue(
severity='info',
category='documentation',
message=f"Missing description for {method} {path}",
path=f"{operation_path}/description",
suggestion="Add a detailed description for better API documentation"
))
has_issues = True
# Validate HTTP method usage patterns
method_issues = self._validate_http_method_usage(path, method, operation)
if method_issues:
has_issues = True
# Validate responses
if 'responses' in operation:
response_issues = self._validate_responses(path, method, operation['responses'])
if response_issues:
has_issues = True
# Validate parameters
if 'parameters' in operation:
param_issues = self._validate_parameters(path, method, operation['parameters'])
if param_issues:
has_issues = True
# Validate request body
if 'requestBody' in operation:
body_issues = self._validate_request_body(path, method, operation['requestBody'])
if body_issues:
has_issues = True
return has_issues
def _validate_http_method_usage(self, path: str, method: str, operation: Dict[str, Any]) -> bool:
"""Validate proper HTTP method usage patterns."""
has_issues = False
# GET requests should not have request body
if method == 'GET' and 'requestBody' in operation:
self.report.add_issue(LintIssue(
severity='error',
category='rest_conventions',
message=f"GET request should not have request body: {method} {path}",
path=f"/paths/{path}/{method.lower()}/requestBody",
suggestion="Remove requestBody from GET request or use POST if body is needed"
))
has_issues = True
# DELETE requests typically should not have request body
if method == 'DELETE' and 'requestBody' in operation:
self.report.add_issue(LintIssue(
severity='warning',
category='rest_conventions',
message=f"DELETE request typically should not have request body: {method} {path}",
path=f"/paths/{path}/{method.lower()}/requestBody",
suggestion="Consider using query parameters or path parameters instead"
))
has_issues = True
# POST/PUT/PATCH should typically have request body (except for actions)
if method in ['POST', 'PUT', 'PATCH'] and 'requestBody' not in operation:
# Check if this is an action endpoint
if not any(action in path.lower() for action in ['activate', 'deactivate', 'reset', 'confirm']):
self.report.add_issue(LintIssue(
severity='info',
category='rest_conventions',
message=f"{method} request typically should have request body: {method} {path}",
path=f"/paths/{path}/{method.lower()}",
suggestion=f"Consider adding requestBody for {method} operation or use GET if no data is being sent"
))
has_issues = True
return has_issues
def _validate_responses(self, path: str, method: str, responses: Dict[str, Any]) -> bool:
"""Validate response definitions."""
has_issues = False
# Check for success response
success_codes = {'200', '201', '202', '204'}
has_success = any(code in responses for code in success_codes)
if not has_success:
self.report.add_issue(LintIssue(
severity='error',
category='responses',
message=f"Missing success response for {method} {path}",
path=f"/paths/{path}/{method.lower()}/responses",
suggestion="Define at least one success response (200, 201, 202, or 204)"
))
has_issues = True
# Check for error responses
has_error_responses = any(code.startswith('4') or code.startswith('5') for code in responses.keys())
if not has_error_responses:
self.report.add_issue(LintIssue(
severity='warning',
category='responses',
message=f"Missing error responses for {method} {path}",
path=f"/paths/{path}/{method.lower()}/responses",
suggestion="Define common error responses (400, 404, 500, etc.)"
))
has_issues = True
# Validate individual response codes
for status_code, response in responses.items():
if status_code == 'default':
continue
try:
code_int = int(status_code)
except ValueError:
self.report.add_issue(LintIssue(
severity='error',
category='responses',
message=f"Invalid status code '{status_code}' for {method} {path}",
path=f"/paths/{path}/{method.lower()}/responses/{status_code}",
suggestion="Use valid HTTP status codes (e.g., 200, 404, 500)"
))
has_issues = True
continue
# Check if status code is appropriate for the method
expected_codes = self.standard_status_codes.get(method, set())
common_codes = {400, 401, 403, 404, 429, 500} # Always acceptable
if expected_codes and code_int not in expected_codes and code_int not in common_codes:
self.report.add_issue(LintIssue(
severity='info',
category='responses',
message=f"Uncommon status code {status_code} for {method} {path}",
path=f"/paths/{path}/{method.lower()}/responses/{status_code}",
suggestion=f"Consider using standard codes for {method}: {sorted(expected_codes)}"
))
has_issues = True
return has_issues
def _validate_parameters(self, path: str, method: str, parameters: List[Dict[str, Any]]) -> bool:
"""Validate parameter definitions."""
has_issues = False
for i, param in enumerate(parameters):
param_path = f"/paths/{path}/{method.lower()}/parameters[{i}]"
# Check required fields
if 'name' not in param:
self.report.add_issue(LintIssue(
severity='error',
category='parameters',
message=f"Parameter missing name field in {method} {path}",
path=f"{param_path}/name",
suggestion="Add a name field to the parameter"
))
has_issues = True
continue
if 'in' not in param:
self.report.add_issue(LintIssue(
severity='error',
category='parameters',
message=f"Parameter '{param['name']}' missing 'in' field in {method} {path}",
path=f"{param_path}/in",
suggestion="Specify parameter location (query, path, header, cookie)"
))
has_issues = True
# Validate parameter naming
param_name = param['name']
param_location = param.get('in', '')
if param_location == 'query':
# Query parameters should use camelCase or kebab-case
if not self.camel_case_pattern.match(param_name) and not self.kebab_case_pattern.match(param_name):
self.report.add_issue(LintIssue(
severity='warning',
category='naming',
message=f"Query parameter '{param_name}' should use camelCase or kebab-case in {method} {path}",
path=f"{param_path}/name",
suggestion="Use camelCase (e.g., 'pageSize') or kebab-case (e.g., 'page-size')"
))
has_issues = True
elif param_location == 'path':
# Path parameters should use camelCase or kebab-case
if not self.camel_case_pattern.match(param_name) and not self.kebab_case_pattern.match(param_name):
self.report.add_issue(LintIssue(
severity='warning',
category='naming',
message=f"Path parameter '{param_name}' should use camelCase or kebab-case in {method} {path}",
path=f"{param_path}/name",
suggestion="Use camelCase (e.g., 'userId') or kebab-case (e.g., 'user-id')"
))
has_issues = True
# Path parameters must be required
if not param.get('required', False):
self.report.add_issue(LintIssue(
severity='error',
category='parameters',
message=f"Path parameter '{param_name}' must be required in {method} {path}",
path=f"{param_path}/required",
suggestion="Set required: true for path parameters"
))
has_issues = True
return has_issues
def _validate_request_body(self, path: str, method: str, request_body: Dict[str, Any]) -> bool:
"""Validate request body definition."""
has_issues = False
if 'content' not in request_body:
self.report.add_issue(LintIssue(
severity='error',
category='request_body',
message=f"Request body missing content for {method} {path}",
path=f"/paths/{path}/{method.lower()}/requestBody/content",
suggestion="Define content types for the request body"
))
has_issues = True
return has_issues
def _validate_components_section(self) -> None:
"""Validate the components section."""
if 'components' not in self.openapi_spec:
self.report.add_issue(LintIssue(
severity='info',
category='structure',
message="Missing components section",
path="/components",
suggestion="Consider defining reusable components (schemas, responses, parameters)"
))
return
components = self.openapi_spec['components']
# Validate schemas
if 'schemas' in components:
self._validate_schemas(components['schemas'])
def _validate_schemas(self, schemas: Dict[str, Any]) -> None:
"""Validate schema definitions."""
for schema_name, schema in schemas.items():
# Check schema naming (should be PascalCase)
if not self.pascal_case_pattern.match(schema_name):
self.report.add_issue(LintIssue(
severity='warning',
category='naming',
message=f"Schema name '{schema_name}' should use PascalCase",
path=f"/components/schemas/{schema_name}",
suggestion=f"Use PascalCase for schema names (e.g., 'UserProfile', 'OrderItem')"
))
# Validate schema properties
if isinstance(schema, dict) and 'properties' in schema:
self._validate_schema_properties(schema_name, schema['properties'])
def _validate_schema_properties(self, schema_name: str, properties: Dict[str, Any]) -> None:
"""Validate schema property naming."""
for prop_name, prop_def in properties.items():
# Properties should use camelCase
if not self.camel_case_pattern.match(prop_name):
self.report.add_issue(LintIssue(
severity='warning',
category='naming',
message=f"Property '{prop_name}' in schema '{schema_name}' should use camelCase",
path=f"/components/schemas/{schema_name}/properties/{prop_name}",
suggestion="Use camelCase for property names (e.g., 'firstName', 'createdAt')"
))
def _validate_security_section(self) -> None:
"""Validate security definitions."""
if 'security' not in self.openapi_spec and 'components' not in self.openapi_spec:
self.report.add_issue(LintIssue(
severity='warning',
category='security',
message="No security configuration found",
path="/security",
suggestion="Define security schemes and apply them to operations"
))
def _validate_raw_endpoint_structure(self) -> None:
"""Validate structure of raw endpoint definitions."""
if 'endpoints' not in self.raw_endpoints:
self.report.add_issue(LintIssue(
severity='error',
category='structure',
message="Missing 'endpoints' field in raw endpoint definition",
path="/endpoints",
suggestion="Provide an 'endpoints' object containing endpoint definitions"
))
return
endpoints = self.raw_endpoints['endpoints']
self.report.total_endpoints = len(endpoints)
def _lint_raw_endpoint(self, path: str, endpoint_data: Dict[str, Any]) -> None:
"""Lint individual raw endpoint definition."""
# Validate path structure
self._validate_path_structure(path)
# Check for required fields
if 'method' not in endpoint_data:
self.report.add_issue(LintIssue(
severity='error',
category='structure',
message=f"Missing method field for endpoint {path}",
path=f"/endpoints/{path}/method",
suggestion="Specify HTTP method (GET, POST, PUT, PATCH, DELETE)"
))
return
method = endpoint_data['method'].upper()
if method not in self.http_methods:
self.report.add_issue(LintIssue(
severity='error',
category='structure',
message=f"Invalid HTTP method '{method}' for endpoint {path}",
path=f"/endpoints/{path}/method",
suggestion=f"Use valid HTTP methods: {', '.join(sorted(self.http_methods))}"
))
def generate_json_report(self) -> str:
"""Generate JSON format report."""
issues_by_severity = self.report.get_issues_by_severity()
report_data = {
"summary": {
"total_endpoints": self.report.total_endpoints,
"endpoints_with_issues": self.report.endpoints_with_issues,
"total_issues": len(self.report.issues),
"errors": len(issues_by_severity['error']),
"warnings": len(issues_by_severity['warning']),
"info": len(issues_by_severity['info']),
"score": round(self.report.score, 2)
},
"issues": []
}
for issue in self.report.issues:
report_data["issues"].append({
"severity": issue.severity,
"category": issue.category,
"message": issue.message,
"path": issue.path,
"suggestion": issue.suggestion
})
return json.dumps(report_data, indent=2)
def generate_text_report(self) -> str:
"""Generate human-readable text report."""
issues_by_severity = self.report.get_issues_by_severity()
report_lines = [
"═══════════════════════════════════════════════════════════════",
" API LINTING REPORT",
"═══════════════════════════════════════════════════════════════",
"",
"SUMMARY:",
f" Total Endpoints: {self.report.total_endpoints}",
f" Endpoints with Issues: {self.report.endpoints_with_issues}",
f" Overall Score: {self.report.score:.1f}/100.0",
"",
"ISSUE BREAKDOWN:",
f" 🔴 Errors: {len(issues_by_severity['error'])}",
f" 🟡 Warnings: {len(issues_by_severity['warning'])}",
f" ℹ️ Info: {len(issues_by_severity['info'])}",
"",
]
if not self.report.issues:
report_lines.extend([
"🎉 Congratulations! No issues found in your API specification.",
""
])
else:
# Group issues by category
issues_by_category = {}
for issue in self.report.issues:
if issue.category not in issues_by_category:
issues_by_category[issue.category] = []
issues_by_category[issue.category].append(issue)
for category, issues in issues_by_category.items():
report_lines.append(f"{'═' * 60}")
report_lines.append(f"CATEGORY: {category.upper().replace('_', ' ')}")
report_lines.append(f"{'═' * 60}")
for issue in issues:
severity_icon = {"error": "🔴", "warning": "🟡", "info": "ℹ️"}[issue.severity]
report_lines.extend([
f"{severity_icon} {issue.severity.upper()}: {issue.message}",
f" Path: {issue.path}",
])
if issue.suggestion:
report_lines.append(f" 💡 Suggestion: {issue.suggestion}")
report_lines.append("")
# Add scoring breakdown
report_lines.extend([
"═══════════════════════════════════════════════════════════════",
"SCORING DETAILS:",
"═══════════════════════════════════════════════════════════════",
f"Base Score: 100.0",
f"Errors Penalty: -{len(issues_by_severity['error']) * 10} (10 points per error)",
f"Warnings Penalty: -{len(issues_by_severity['warning']) * 3} (3 points per warning)",
f"Info Penalty: -{len(issues_by_severity['info']) * 1} (1 point per info)",
f"Final Score: {self.report.score:.1f}/100.0",
""
])
# Add recommendations based on score
if self.report.score >= 90:
report_lines.append("🏆 Excellent! Your API design follows best practices.")
elif self.report.score >= 80:
report_lines.append("✅ Good API design with minor areas for improvement.")
elif self.report.score >= 70:
report_lines.append("⚠️ Fair API design. Consider addressing warnings and errors.")
elif self.report.score >= 50:
report_lines.append("❌ Poor API design. Multiple issues need attention.")
else:
report_lines.append("🚨 Critical API design issues. Immediate attention required.")
return "\n".join(report_lines)
def main():
"""Main CLI entry point."""
parser = argparse.ArgumentParser(
description="Analyze OpenAPI/Swagger specifications for REST conventions and best practices",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python api_linter.py openapi.json
python api_linter.py --format json openapi.json > report.json
python api_linter.py --raw-endpoints endpoints.json
"""
)
parser.add_argument(
'input_file',
help='Input file: OpenAPI/Swagger JSON file or raw endpoints JSON'
)
parser.add_argument(
'--format',
choices=['text', 'json'],
default='text',
help='Output format (default: text)'
)
parser.add_argument(
'--raw-endpoints',
action='store_true',
help='Treat input as raw endpoint definitions instead of OpenAPI spec'
)
parser.add_argument(
'--output',
help='Output file (default: stdout)'
)
args = parser.parse_args()
# Load input file
try:
with open(args.input_file, 'r') as f:
input_data = json.load(f)
except FileNotFoundError:
print(f"Error: Input file '{args.input_file}' not found.", file=sys.stderr)
return 1
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in '{args.input_file}': {e}", file=sys.stderr)
return 1
# Initialize linter and run analysis
linter = APILinter()
try:
if args.raw_endpoints:
report = linter.lint_raw_endpoints(input_data)
else:
report = linter.lint_openapi_spec(input_data)
except Exception as e:
print(f"Error during linting: {e}", file=sys.stderr)
return 1
# Generate report
if args.format == 'json':
output = linter.generate_json_report()
else:
output = linter.generate_text_report()
# Write output
if args.output:
try:
with open(args.output, 'w') as f:
f.write(output)
print(f"Report written to {args.output}")
except IOError as e:
print(f"Error writing to '{args.output}': {e}", file=sys.stderr)
return 1
else:
print(output)
# Return appropriate exit code
error_count = len([i for i in report.issues if i.severity == 'error'])
return 1 if error_count > 0 else 0
if __name__ == '__main__':
sys.exit(main())
FILE:scripts/api_scorecard.py
#!/usr/bin/env python3
"""
API Scorecard - Comprehensive API design quality assessment tool.
This script evaluates API designs across multiple dimensions and generates
a detailed scorecard with letter grades and improvement recommendations.
Scoring Dimensions:
- Consistency (30%): Naming conventions, response patterns, structural consistency
- Documentation (20%): Completeness and clarity of API documentation
- Security (20%): Authentication, authorization, and security best practices
- Usability (15%): Ease of use, discoverability, and developer experience
- Performance (15%): Caching, pagination, and efficiency patterns
Generates letter grades (A-F) with detailed breakdowns and actionable recommendations.
"""
import argparse
import json
import re
import sys
from typing import Any, Dict, List, Optional, Set, Tuple
from dataclasses import dataclass, field
from enum import Enum
import math
class ScoreCategory(Enum):
"""Scoring categories."""
CONSISTENCY = "consistency"
DOCUMENTATION = "documentation"
SECURITY = "security"
USABILITY = "usability"
PERFORMANCE = "performance"
@dataclass
class CategoryScore:
"""Score for a specific category."""
category: ScoreCategory
score: float # 0-100
max_score: float # Usually 100
weight: float # Percentage weight in overall score
issues: List[str] = field(default_factory=list)
recommendations: List[str] = field(default_factory=list)
@property
def letter_grade(self) -> str:
"""Convert score to letter grade."""
if self.score >= 90:
return "A"
elif self.score >= 80:
return "B"
elif self.score >= 70:
return "C"
elif self.score >= 60:
return "D"
else:
return "F"
@property
def weighted_score(self) -> float:
"""Calculate weighted contribution to overall score."""
return (self.score / 100.0) * self.weight
@dataclass
class APIScorecard:
"""Complete API scorecard with all category scores."""
category_scores: Dict[ScoreCategory, CategoryScore] = field(default_factory=dict)
overall_score: float = 0.0
overall_grade: str = "F"
total_endpoints: int = 0
api_info: Dict[str, Any] = field(default_factory=dict)
def calculate_overall_score(self) -> None:
"""Calculate overall weighted score and grade."""
self.overall_score = sum(score.weighted_score for score in self.category_scores.values())
if self.overall_score >= 90:
self.overall_grade = "A"
elif self.overall_score >= 80:
self.overall_grade = "B"
elif self.overall_score >= 70:
self.overall_grade = "C"
elif self.overall_score >= 60:
self.overall_grade = "D"
else:
self.overall_grade = "F"
def get_top_recommendations(self, limit: int = 5) -> List[str]:
"""Get top recommendations across all categories."""
all_recommendations = []
for category_score in self.category_scores.values():
for rec in category_score.recommendations:
all_recommendations.append(f"{category_score.category.value.title()}: {rec}")
# Sort by category weight (highest impact first)
weighted_recs = []
for category_score in sorted(self.category_scores.values(),
key=lambda x: x.weight, reverse=True):
for rec in category_score.recommendations[:2]: # Top 2 per category
weighted_recs.append(f"{category_score.category.value.title()}: {rec}")
return weighted_recs[:limit]
class APIScoringEngine:
"""Main API scoring engine."""
def __init__(self):
self.scorecard = APIScorecard()
self.spec: Optional[Dict] = None
# Regex patterns for validation
self.kebab_case_pattern = re.compile(r'^[a-z]+(?:-[a-z0-9]+)*$')
self.camel_case_pattern = re.compile(r'^[a-z][a-zA-Z0-9]*$')
self.pascal_case_pattern = re.compile(r'^[A-Z][a-zA-Z0-9]*$')
# HTTP methods
self.http_methods = {'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'}
# Category weights (must sum to 100)
self.category_weights = {
ScoreCategory.CONSISTENCY: 30.0,
ScoreCategory.DOCUMENTATION: 20.0,
ScoreCategory.SECURITY: 20.0,
ScoreCategory.USABILITY: 15.0,
ScoreCategory.PERFORMANCE: 15.0
}
def score_api(self, spec: Dict[str, Any]) -> APIScorecard:
"""Generate comprehensive API scorecard."""
self.spec = spec
self.scorecard = APIScorecard()
# Extract basic API info
self._extract_api_info()
# Score each category
self._score_consistency()
self._score_documentation()
self._score_security()
self._score_usability()
self._score_performance()
# Calculate overall score
self.scorecard.calculate_overall_score()
return self.scorecard
def _extract_api_info(self) -> None:
"""Extract basic API information."""
info = self.spec.get('info', {})
paths = self.spec.get('paths', {})
self.scorecard.api_info = {
'title': info.get('title', 'Unknown API'),
'version': info.get('version', ''),
'description': info.get('description', ''),
'total_paths': len(paths),
'openapi_version': self.spec.get('openapi', self.spec.get('swagger', ''))
}
# Count total endpoints
endpoint_count = 0
for path_obj in paths.values():
if isinstance(path_obj, dict):
endpoint_count += len([m for m in path_obj.keys()
if m.upper() in self.http_methods])
self.scorecard.total_endpoints = endpoint_count
def _score_consistency(self) -> None:
"""Score API consistency (30% weight)."""
category = ScoreCategory.CONSISTENCY
score = CategoryScore(
category=category,
score=0.0,
max_score=100.0,
weight=self.category_weights[category]
)
consistency_checks = [
self._check_naming_consistency(),
self._check_response_consistency(),
self._check_error_format_consistency(),
self._check_parameter_consistency(),
self._check_url_structure_consistency(),
self._check_http_method_consistency(),
self._check_status_code_consistency()
]
# Average the consistency scores
valid_scores = [s for s in consistency_checks if s is not None]
if valid_scores:
score.score = sum(valid_scores) / len(valid_scores)
# Add specific recommendations based on low scores
if score.score < 70:
score.recommendations.extend([
"Review naming conventions across all endpoints and schemas",
"Standardize response formats and error structures",
"Ensure consistent HTTP method usage patterns"
])
elif score.score < 85:
score.recommendations.extend([
"Minor consistency improvements needed in naming or response formats",
"Consider creating API design guidelines document"
])
self.scorecard.category_scores[category] = score
def _check_naming_consistency(self) -> float:
"""Check naming convention consistency."""
paths = self.spec.get('paths', {})
schemas = self.spec.get('components', {}).get('schemas', {})
total_checks = 0
passed_checks = 0
# Check path naming (should be kebab-case)
for path in paths.keys():
segments = [seg for seg in path.split('/') if seg and not seg.startswith('{')]
for segment in segments:
total_checks += 1
if self.kebab_case_pattern.match(segment) or re.match(r'^v\d+$', segment):
passed_checks += 1
# Check schema naming (should be PascalCase)
for schema_name in schemas.keys():
total_checks += 1
if self.pascal_case_pattern.match(schema_name):
passed_checks += 1
# Check property naming within schemas
for schema in schemas.values():
if isinstance(schema, dict) and 'properties' in schema:
for prop_name in schema['properties'].keys():
total_checks += 1
if self.camel_case_pattern.match(prop_name):
passed_checks += 1
return (passed_checks / total_checks * 100) if total_checks > 0 else 100
def _check_response_consistency(self) -> float:
"""Check response format consistency."""
paths = self.spec.get('paths', {})
response_patterns = []
total_responses = 0
for path_obj in paths.values():
if not isinstance(path_obj, dict):
continue
for method, operation in path_obj.items():
if method.upper() not in self.http_methods or not isinstance(operation, dict):
continue
responses = operation.get('responses', {})
for status_code, response in responses.items():
if not isinstance(response, dict):
continue
total_responses += 1
content = response.get('content', {})
# Analyze response structure
for media_type, media_obj in content.items():
schema = media_obj.get('schema', {})
pattern = self._extract_schema_pattern(schema)
response_patterns.append(pattern)
# Calculate consistency by comparing patterns
if not response_patterns:
return 100
pattern_counts = {}
for pattern in response_patterns:
pattern_key = json.dumps(pattern, sort_keys=True)
pattern_counts[pattern_key] = pattern_counts.get(pattern_key, 0) + 1
# Most common pattern should dominate for good consistency
max_count = max(pattern_counts.values()) if pattern_counts else 0
consistency_ratio = max_count / len(response_patterns) if response_patterns else 1
return consistency_ratio * 100
def _extract_schema_pattern(self, schema: Dict[str, Any]) -> Dict[str, Any]:
"""Extract a pattern from a schema for consistency checking."""
if not isinstance(schema, dict):
return {}
pattern = {
'type': schema.get('type'),
'has_properties': 'properties' in schema,
'has_items': 'items' in schema,
'required_count': len(schema.get('required', [])),
'property_count': len(schema.get('properties', {}))
}
return pattern
def _check_error_format_consistency(self) -> float:
"""Check error response format consistency."""
paths = self.spec.get('paths', {})
error_responses = []
for path_obj in paths.values():
if not isinstance(path_obj, dict):
continue
for method, operation in path_obj.items():
if method.upper() not in self.http_methods:
continue
responses = operation.get('responses', {})
for status_code, response in responses.items():
try:
code_int = int(status_code)
if code_int >= 400: # Error responses
content = response.get('content', {})
for media_type, media_obj in content.items():
schema = media_obj.get('schema', {})
error_responses.append(self._extract_schema_pattern(schema))
except ValueError:
continue
if not error_responses:
return 80 # No error responses defined - somewhat concerning
# Check consistency of error response formats
pattern_counts = {}
for pattern in error_responses:
pattern_key = json.dumps(pattern, sort_keys=True)
pattern_counts[pattern_key] = pattern_counts.get(pattern_key, 0) + 1
max_count = max(pattern_counts.values()) if pattern_counts else 0
consistency_ratio = max_count / len(error_responses) if error_responses else 1
return consistency_ratio * 100
def _check_parameter_consistency(self) -> float:
"""Check parameter naming and usage consistency."""
paths = self.spec.get('paths', {})
query_params = []
path_params = []
header_params = []
for path_obj in paths.values():
if not isinstance(path_obj, dict):
continue
for method, operation in path_obj.items():
if method.upper() not in self.http_methods:
continue
parameters = operation.get('parameters', [])
for param in parameters:
if not isinstance(param, dict):
continue
param_name = param.get('name', '')
param_in = param.get('in', '')
if param_in == 'query':
query_params.append(param_name)
elif param_in == 'path':
path_params.append(param_name)
elif param_in == 'header':
header_params.append(param_name)
# Check naming consistency for each parameter type
scores = []
# Query parameters should be camelCase or kebab-case
if query_params:
valid_query = sum(1 for p in query_params
if self.camel_case_pattern.match(p) or self.kebab_case_pattern.match(p))
scores.append((valid_query / len(query_params)) * 100)
# Path parameters should be camelCase or kebab-case
if path_params:
valid_path = sum(1 for p in path_params
if self.camel_case_pattern.match(p) or self.kebab_case_pattern.match(p))
scores.append((valid_path / len(path_params)) * 100)
return sum(scores) / len(scores) if scores else 100
def _check_url_structure_consistency(self) -> float:
"""Check URL structure and pattern consistency."""
paths = self.spec.get('paths', {})
total_paths = len(paths)
if total_paths == 0:
return 0
structure_score = 0
# Check for consistent versioning
versioned_paths = 0
for path in paths.keys():
if re.search(r'/v\d+/', path):
versioned_paths += 1
# Either all or none should be versioned for consistency
if versioned_paths == 0 or versioned_paths == total_paths:
structure_score += 25
elif versioned_paths > total_paths * 0.8:
structure_score += 20
# Check for reasonable path depth
reasonable_depth = 0
for path in paths.keys():
segments = [seg for seg in path.split('/') if seg]
if 2 <= len(segments) <= 5: # Reasonable depth
reasonable_depth += 1
structure_score += (reasonable_depth / total_paths) * 25
# Check for RESTful resource patterns
restful_patterns = 0
for path in paths.keys():
# Look for patterns like /resources/{id} or /resources
if re.match(r'^/[a-z-]+(/\{[^}]+\})?(/[a-z-]+)*$', path):
restful_patterns += 1
structure_score += (restful_patterns / total_paths) * 30
# Check for consistent trailing slash usage
with_slash = sum(1 for path in paths.keys() if path.endswith('/'))
without_slash = total_paths - with_slash
# Either all or none should have trailing slashes
if with_slash == 0 or without_slash == 0:
structure_score += 20
elif min(with_slash, without_slash) < total_paths * 0.1:
structure_score += 15
return min(structure_score, 100)
def _check_http_method_consistency(self) -> float:
"""Check HTTP method usage consistency."""
paths = self.spec.get('paths', {})
method_usage = {}
total_operations = 0
for path, path_obj in paths.items():
if not isinstance(path_obj, dict):
continue
for method in path_obj.keys():
if method.upper() in self.http_methods:
method_upper = method.upper()
total_operations += 1
# Analyze method usage patterns
if method_upper not in method_usage:
method_usage[method_upper] = {'count': 0, 'appropriate': 0}
method_usage[method_upper]['count'] += 1
# Check if method usage seems appropriate
if self._is_method_usage_appropriate(path, method_upper, path_obj[method]):
method_usage[method_upper]['appropriate'] += 1
if total_operations == 0:
return 0
# Calculate appropriateness score
total_appropriate = sum(data['appropriate'] for data in method_usage.values())
return (total_appropriate / total_operations) * 100
def _is_method_usage_appropriate(self, path: str, method: str, operation: Dict) -> bool:
"""Check if HTTP method usage is appropriate for the endpoint."""
# Simple heuristics for method appropriateness
has_request_body = 'requestBody' in operation
path_has_id = '{' in path and '}' in path
if method == 'GET':
return not has_request_body # GET should not have body
elif method == 'POST':
return not path_has_id # POST typically for collections
elif method == 'PUT':
return path_has_id and has_request_body # PUT for specific resources
elif method == 'PATCH':
return path_has_id # PATCH for specific resources
elif method == 'DELETE':
return path_has_id # DELETE for specific resources
return True # Default to appropriate for other methods
def _check_status_code_consistency(self) -> float:
"""Check HTTP status code usage consistency."""
paths = self.spec.get('paths', {})
method_status_patterns = {}
total_operations = 0
for path_obj in paths.values():
if not isinstance(path_obj, dict):
continue
for method, operation in path_obj.items():
if method.upper() not in self.http_methods:
continue
total_operations += 1
responses = operation.get('responses', {})
status_codes = set(responses.keys())
if method.upper() not in method_status_patterns:
method_status_patterns[method.upper()] = []
method_status_patterns[method.upper()].append(status_codes)
if total_operations == 0:
return 0
# Check consistency within each method type
consistency_scores = []
for method, status_patterns in method_status_patterns.items():
if not status_patterns:
continue
# Find common status codes for this method
all_codes = set()
for pattern in status_patterns:
all_codes.update(pattern)
# Calculate how many operations use the most common codes
code_usage = {}
for code in all_codes:
code_usage[code] = sum(1 for pattern in status_patterns if code in pattern)
# Score based on consistency of common status codes
if status_patterns:
avg_consistency = sum(
len([code for code in pattern if code_usage.get(code, 0) > len(status_patterns) * 0.5])
for pattern in status_patterns
) / len(status_patterns)
method_consistency = min(avg_consistency / 3.0 * 100, 100) # Expect ~3 common codes
consistency_scores.append(method_consistency)
return sum(consistency_scores) / len(consistency_scores) if consistency_scores else 100
def _score_documentation(self) -> None:
"""Score API documentation quality (20% weight)."""
category = ScoreCategory.DOCUMENTATION
score = CategoryScore(
category=category,
score=0.0,
max_score=100.0,
weight=self.category_weights[category]
)
documentation_checks = [
self._check_api_level_documentation(),
self._check_endpoint_documentation(),
self._check_schema_documentation(),
self._check_parameter_documentation(),
self._check_response_documentation(),
self._check_example_coverage()
]
valid_scores = [s for s in documentation_checks if s is not None]
if valid_scores:
score.score = sum(valid_scores) / len(valid_scores)
# Add recommendations based on score
if score.score < 60:
score.recommendations.extend([
"Add comprehensive descriptions to all API components",
"Include examples for complex operations and schemas",
"Document all parameters and response fields"
])
elif score.score < 80:
score.recommendations.extend([
"Improve documentation completeness for some endpoints",
"Add more examples to enhance developer experience"
])
self.scorecard.category_scores[category] = score
def _check_api_level_documentation(self) -> float:
"""Check API-level documentation completeness."""
info = self.spec.get('info', {})
score = 0
# Required fields
if info.get('title'):
score += 20
if info.get('version'):
score += 20
if info.get('description') and len(info['description']) > 20:
score += 30
# Optional but recommended fields
if info.get('contact'):
score += 15
if info.get('license'):
score += 15
return score
def _check_endpoint_documentation(self) -> float:
"""Check endpoint-level documentation completeness."""
paths = self.spec.get('paths', {})
total_operations = 0
documented_operations = 0
for path_obj in paths.values():
if not isinstance(path_obj, dict):
continue
for method, operation in path_obj.items():
if method.upper() not in self.http_methods:
continue
total_operations += 1
doc_score = 0
if operation.get('summary'):
doc_score += 1
if operation.get('description') and len(operation['description']) > 20:
doc_score += 1
if operation.get('operationId'):
doc_score += 1
# Consider it documented if it has at least 2/3 elements
if doc_score >= 2:
documented_operations += 1
return (documented_operations / total_operations * 100) if total_operations > 0 else 100
def _check_schema_documentation(self) -> float:
"""Check schema documentation completeness."""
schemas = self.spec.get('components', {}).get('schemas', {})
if not schemas:
return 80 # No schemas to document
total_schemas = len(schemas)
documented_schemas = 0
for schema_name, schema in schemas.items():
if not isinstance(schema, dict):
continue
doc_elements = 0
# Schema-level description
if schema.get('description'):
doc_elements += 1
# Property descriptions
properties = schema.get('properties', {})
if properties:
described_props = sum(1 for prop in properties.values()
if isinstance(prop, dict) and prop.get('description'))
if described_props > len(properties) * 0.5: # At least 50% documented
doc_elements += 1
# Examples
if schema.get('example') or any(
isinstance(prop, dict) and prop.get('example')
for prop in properties.values()
):
doc_elements += 1
if doc_elements >= 2:
documented_schemas += 1
return (documented_schemas / total_schemas * 100) if total_schemas > 0 else 100
def _check_parameter_documentation(self) -> float:
"""Check parameter documentation completeness."""
paths = self.spec.get('paths', {})
total_params = 0
documented_params = 0
for path_obj in paths.values():
if not isinstance(path_obj, dict):
continue
for method, operation in path_obj.items():
if method.upper() not in self.http_methods:
continue
parameters = operation.get('parameters', [])
for param in parameters:
if not isinstance(param, dict):
continue
total_params += 1
doc_score = 0
if param.get('description'):
doc_score += 1
if param.get('example') or (param.get('schema', {}).get('example')):
doc_score += 1
if doc_score >= 1: # At least description
documented_params += 1
return (documented_params / total_params * 100) if total_params > 0 else 100
def _check_response_documentation(self) -> float:
"""Check response documentation completeness."""
paths = self.spec.get('paths', {})
total_responses = 0
documented_responses = 0
for path_obj in paths.values():
if not isinstance(path_obj, dict):
continue
for method, operation in path_obj.items():
if method.upper() not in self.http_methods:
continue
responses = operation.get('responses', {})
for status_code, response in responses.items():
if not isinstance(response, dict):
continue
total_responses += 1
if response.get('description'):
documented_responses += 1
return (documented_responses / total_responses * 100) if total_responses > 0 else 100
def _check_example_coverage(self) -> float:
"""Check example coverage across the API."""
paths = self.spec.get('paths', {})
schemas = self.spec.get('components', {}).get('schemas', {})
# Check examples in operations
total_operations = 0
operations_with_examples = 0
for path_obj in paths.values():
if not isinstance(path_obj, dict):
continue
for method, operation in path_obj.items():
if method.upper() not in self.http_methods:
continue
total_operations += 1
has_example = False
# Check request body examples
request_body = operation.get('requestBody', {})
if self._has_examples(request_body.get('content', {})):
has_example = True
# Check response examples
responses = operation.get('responses', {})
for response in responses.values():
if isinstance(response, dict) and self._has_examples(response.get('content', {})):
has_example = True
break
if has_example:
operations_with_examples += 1
# Check examples in schemas
total_schemas = len(schemas)
schemas_with_examples = 0
for schema in schemas.values():
if isinstance(schema, dict) and self._schema_has_examples(schema):
schemas_with_examples += 1
# Combine scores
operation_score = (operations_with_examples / total_operations * 100) if total_operations > 0 else 100
schema_score = (schemas_with_examples / total_schemas * 100) if total_schemas > 0 else 100
return (operation_score + schema_score) / 2
def _has_examples(self, content: Dict[str, Any]) -> bool:
"""Check if content has examples."""
for media_type, media_obj in content.items():
if isinstance(media_obj, dict):
if media_obj.get('example') or media_obj.get('examples'):
return True
return False
def _schema_has_examples(self, schema: Dict[str, Any]) -> bool:
"""Check if schema has examples."""
if schema.get('example'):
return True
properties = schema.get('properties', {})
for prop in properties.values():
if isinstance(prop, dict) and prop.get('example'):
return True
return False
def _score_security(self) -> None:
"""Score API security implementation (20% weight)."""
category = ScoreCategory.SECURITY
score = CategoryScore(
category=category,
score=0.0,
max_score=100.0,
weight=self.category_weights[category]
)
security_checks = [
self._check_security_schemes(),
self._check_security_requirements(),
self._check_https_usage(),
self._check_authentication_patterns(),
self._check_sensitive_data_handling()
]
valid_scores = [s for s in security_checks if s is not None]
if valid_scores:
score.score = sum(valid_scores) / len(valid_scores)
# Add recommendations
if score.score < 50:
score.recommendations.extend([
"Implement comprehensive security schemes (OAuth2, API keys, etc.)",
"Ensure all endpoints have appropriate security requirements",
"Add input validation and rate limiting patterns"
])
elif score.score < 80:
score.recommendations.extend([
"Review security coverage for all endpoints",
"Consider additional security measures for sensitive operations"
])
self.scorecard.category_scores[category] = score
def _check_security_schemes(self) -> float:
"""Check security scheme definitions."""
security_schemes = self.spec.get('components', {}).get('securitySchemes', {})
if not security_schemes:
return 20 # Very low score for no security
score = 40 # Base score for having security schemes
scheme_types = set()
for scheme in security_schemes.values():
if isinstance(scheme, dict):
scheme_type = scheme.get('type')
scheme_types.add(scheme_type)
# Bonus for modern security schemes
if 'oauth2' in scheme_types:
score += 30
if 'apiKey' in scheme_types:
score += 15
if 'http' in scheme_types:
score += 15
return min(score, 100)
def _check_security_requirements(self) -> float:
"""Check security requirement coverage."""
paths = self.spec.get('paths', {})
global_security = self.spec.get('security', [])
total_operations = 0
secured_operations = 0
for path_obj in paths.values():
if not isinstance(path_obj, dict):
continue
for method, operation in path_obj.items():
if method.upper() not in self.http_methods:
continue
total_operations += 1
# Check if operation has security requirements
operation_security = operation.get('security')
if operation_security is not None:
secured_operations += 1
elif global_security:
secured_operations += 1
return (secured_operations / total_operations * 100) if total_operations > 0 else 0
def _check_https_usage(self) -> float:
"""Check HTTPS enforcement."""
servers = self.spec.get('servers', [])
if not servers:
return 60 # No servers defined - assume HTTPS
https_servers = 0
for server in servers:
if isinstance(server, dict):
url = server.get('url', '')
if url.startswith('https://') or not url.startswith('http://'):
https_servers += 1
return (https_servers / len(servers) * 100) if servers else 100
def _check_authentication_patterns(self) -> float:
"""Check authentication pattern quality."""
security_schemes = self.spec.get('components', {}).get('securitySchemes', {})
if not security_schemes:
return 0
pattern_scores = []
for scheme in security_schemes.values():
if not isinstance(scheme, dict):
continue
scheme_type = scheme.get('type', '').lower()
if scheme_type == 'oauth2':
# OAuth2 is highly recommended
flows = scheme.get('flows', {})
if flows:
pattern_scores.append(95)
else:
pattern_scores.append(80)
elif scheme_type == 'http':
scheme_scheme = scheme.get('scheme', '').lower()
if scheme_scheme == 'bearer':
pattern_scores.append(85)
elif scheme_scheme == 'basic':
pattern_scores.append(60) # Less secure
else:
pattern_scores.append(70)
elif scheme_type == 'apikey':
location = scheme.get('in', '').lower()
if location == 'header':
pattern_scores.append(75)
else:
pattern_scores.append(60) # Query/cookie less secure
else:
pattern_scores.append(50) # Unknown scheme
return sum(pattern_scores) / len(pattern_scores) if pattern_scores else 0
def _check_sensitive_data_handling(self) -> float:
"""Check sensitive data handling patterns."""
# This is a simplified check - in reality would need more sophisticated analysis
schemas = self.spec.get('components', {}).get('schemas', {})
score = 80 # Default good score
# Look for potential sensitive fields without proper handling
sensitive_field_names = {'password', 'secret', 'token', 'key', 'ssn', 'credit_card'}
for schema in schemas.values():
if not isinstance(schema, dict):
continue
properties = schema.get('properties', {})
for prop_name, prop_def in properties.items():
if not isinstance(prop_def, dict):
continue
# Check for sensitive field names
if any(sensitive in prop_name.lower() for sensitive in sensitive_field_names):
# Check if it's marked as sensitive (writeOnly, format: password, etc.)
if not (prop_def.get('writeOnly') or
prop_def.get('format') == 'password' or
'password' in prop_def.get('description', '').lower()):
score -= 10 # Penalty for exposed sensitive field
return max(score, 0)
def _score_usability(self) -> None:
"""Score API usability and developer experience (15% weight)."""
category = ScoreCategory.USABILITY
score = CategoryScore(
category=category,
score=0.0,
max_score=100.0,
weight=self.category_weights[category]
)
usability_checks = [
self._check_discoverability(),
self._check_error_handling(),
self._check_filtering_and_searching(),
self._check_resource_relationships(),
self._check_developer_experience()
]
valid_scores = [s for s in usability_checks if s is not None]
if valid_scores:
score.score = sum(valid_scores) / len(valid_scores)
# Add recommendations
if score.score < 60:
score.recommendations.extend([
"Improve error messages with actionable guidance",
"Add filtering and search capabilities to list endpoints",
"Enhance resource discoverability with better linking"
])
elif score.score < 80:
score.recommendations.extend([
"Consider adding HATEOAS links for better discoverability",
"Enhance developer experience with better examples"
])
self.scorecard.category_scores[category] = score
def _check_discoverability(self) -> float:
"""Check API discoverability features."""
paths = self.spec.get('paths', {})
# Look for root/discovery endpoints
has_root = '/' in paths or any(path == '/api' or path.startswith('/api/') for path in paths)
# Look for HATEOAS patterns in responses
hateoas_score = 0
total_responses = 0
for path_obj in paths.values():
if not isinstance(path_obj, dict):
continue
for method, operation in path_obj.items():
if method.upper() not in self.http_methods:
continue
responses = operation.get('responses', {})
for response in responses.values():
if not isinstance(response, dict):
continue
total_responses += 1
# Look for link-like properties in response schemas
content = response.get('content', {})
for media_obj in content.values():
schema = media_obj.get('schema', {})
if self._has_link_properties(schema):
hateoas_score += 1
break
discovery_score = 50 if has_root else 30
if total_responses > 0:
hateoas_ratio = hateoas_score / total_responses
discovery_score += hateoas_ratio * 50
return min(discovery_score, 100)
def _has_link_properties(self, schema: Dict[str, Any]) -> bool:
"""Check if schema has link-like properties."""
if not isinstance(schema, dict):
return False
properties = schema.get('properties', {})
link_indicators = {'links', '_links', 'href', 'url', 'self', 'next', 'prev'}
return any(prop_name.lower() in link_indicators for prop_name in properties.keys())
def _check_error_handling(self) -> float:
"""Check error handling quality."""
paths = self.spec.get('paths', {})
total_operations = 0
operations_with_errors = 0
detailed_error_responses = 0
for path_obj in paths.values():
if not isinstance(path_obj, dict):
continue
for method, operation in path_obj.items():
if method.upper() not in self.http_methods:
continue
total_operations += 1
responses = operation.get('responses', {})
# Check for error responses
has_error_responses = any(
status_code.startswith('4') or status_code.startswith('5')
for status_code in responses.keys()
)
if has_error_responses:
operations_with_errors += 1
# Check for detailed error schemas
for status_code, response in responses.items():
if (status_code.startswith('4') or status_code.startswith('5')) and isinstance(response, dict):
content = response.get('content', {})
for media_obj in content.values():
schema = media_obj.get('schema', {})
if self._has_detailed_error_schema(schema):
detailed_error_responses += 1
break
break
if total_operations == 0:
return 0
error_coverage = (operations_with_errors / total_operations) * 60
error_detail = (detailed_error_responses / operations_with_errors * 40) if operations_with_errors > 0 else 0
return error_coverage + error_detail
def _has_detailed_error_schema(self, schema: Dict[str, Any]) -> bool:
"""Check if error schema has detailed information."""
if not isinstance(schema, dict):
return False
properties = schema.get('properties', {})
error_fields = {'error', 'message', 'details', 'code', 'timestamp'}
matching_fields = sum(1 for field in error_fields if field in properties)
return matching_fields >= 2 # At least 2 standard error fields
def _check_filtering_and_searching(self) -> float:
"""Check filtering and search capabilities."""
paths = self.spec.get('paths', {})
collection_endpoints = 0
endpoints_with_filtering = 0
for path, path_obj in paths.items():
if not isinstance(path_obj, dict):
continue
# Identify collection endpoints (no path parameters)
if '{' not in path:
get_operation = path_obj.get('get')
if get_operation:
collection_endpoints += 1
# Check for filtering/search parameters
parameters = get_operation.get('parameters', [])
filter_params = {'filter', 'search', 'q', 'query', 'limit', 'page', 'offset'}
has_filtering = any(
isinstance(param, dict) and param.get('name', '').lower() in filter_params
for param in parameters
)
if has_filtering:
endpoints_with_filtering += 1
return (endpoints_with_filtering / collection_endpoints * 100) if collection_endpoints > 0 else 100
def _check_resource_relationships(self) -> float:
"""Check resource relationship handling."""
paths = self.spec.get('paths', {})
schemas = self.spec.get('components', {}).get('schemas', {})
# Look for nested resource patterns
nested_resources = 0
total_resource_paths = 0
for path in paths.keys():
# Skip root paths
if path.count('/') >= 3: # e.g., /api/users/123/orders
total_resource_paths += 1
if '{' in path:
nested_resources += 1
# Look for relationship fields in schemas
schemas_with_relations = 0
for schema in schemas.values():
if not isinstance(schema, dict):
continue
properties = schema.get('properties', {})
relation_indicators = {'id', '_id', 'ref', 'link', 'relationship'}
has_relations = any(
any(indicator in prop_name.lower() for indicator in relation_indicators)
for prop_name in properties.keys()
)
if has_relations:
schemas_with_relations += 1
nested_score = (nested_resources / total_resource_paths * 50) if total_resource_paths > 0 else 25
schema_score = (schemas_with_relations / len(schemas) * 50) if schemas else 25
return nested_score + schema_score
def _check_developer_experience(self) -> float:
"""Check overall developer experience factors."""
# This is a composite score based on various DX factors
factors = []
# Factor 1: Consistent response structure
factors.append(self._check_response_consistency())
# Factor 2: Clear operation IDs
paths = self.spec.get('paths', {})
total_operations = 0
operations_with_ids = 0
for path_obj in paths.values():
if not isinstance(path_obj, dict):
continue
for method, operation in path_obj.items():
if method.upper() not in self.http_methods:
continue
total_operations += 1
if isinstance(operation, dict) and operation.get('operationId'):
operations_with_ids += 1
operation_id_score = (operations_with_ids / total_operations * 100) if total_operations > 0 else 100
factors.append(operation_id_score)
# Factor 3: Reasonable path complexity
avg_path_complexity = 0
if paths:
complexities = []
for path in paths.keys():
segments = [seg for seg in path.split('/') if seg]
complexities.append(len(segments))
avg_complexity = sum(complexities) / len(complexities)
# Optimal complexity is 3-4 segments
if 3 <= avg_complexity <= 4:
avg_path_complexity = 100
elif 2 <= avg_complexity <= 5:
avg_path_complexity = 80
else:
avg_path_complexity = 60
factors.append(avg_path_complexity)
return sum(factors) / len(factors) if factors else 0
def _score_performance(self) -> None:
"""Score API performance patterns (15% weight)."""
category = ScoreCategory.PERFORMANCE
score = CategoryScore(
category=category,
score=0.0,
max_score=100.0,
weight=self.category_weights[category]
)
performance_checks = [
self._check_caching_headers(),
self._check_pagination_patterns(),
self._check_compression_support(),
self._check_efficiency_patterns(),
self._check_batch_operations()
]
valid_scores = [s for s in performance_checks if s is not None]
if valid_scores:
score.score = sum(valid_scores) / len(valid_scores)
# Add recommendations
if score.score < 60:
score.recommendations.extend([
"Implement pagination for list endpoints",
"Add caching headers for cacheable responses",
"Consider batch operations for bulk updates"
])
elif score.score < 80:
score.recommendations.extend([
"Review caching strategies for better performance",
"Consider field selection parameters for large responses"
])
self.scorecard.category_scores[category] = score
def _check_caching_headers(self) -> float:
"""Check caching header implementation."""
paths = self.spec.get('paths', {})
get_operations = 0
cacheable_operations = 0
for path_obj in paths.values():
if not isinstance(path_obj, dict):
continue
get_operation = path_obj.get('get')
if get_operation and isinstance(get_operation, dict):
get_operations += 1
# Check for caching-related headers in responses
responses = get_operation.get('responses', {})
for response in responses.values():
if not isinstance(response, dict):
continue
headers = response.get('headers', {})
cache_headers = {'cache-control', 'etag', 'last-modified', 'expires'}
if any(header.lower() in cache_headers for header in headers.keys()):
cacheable_operations += 1
break
return (cacheable_operations / get_operations * 100) if get_operations > 0 else 50
def _check_pagination_patterns(self) -> float:
"""Check pagination implementation."""
paths = self.spec.get('paths', {})
collection_endpoints = 0
paginated_endpoints = 0
for path, path_obj in paths.items():
if not isinstance(path_obj, dict):
continue
# Identify collection endpoints
if '{' not in path: # No path parameters = collection
get_operation = path_obj.get('get')
if get_operation and isinstance(get_operation, dict):
collection_endpoints += 1
# Check for pagination parameters
parameters = get_operation.get('parameters', [])
pagination_params = {'limit', 'offset', 'page', 'pagesize', 'per_page', 'cursor'}
has_pagination = any(
isinstance(param, dict) and param.get('name', '').lower() in pagination_params
for param in parameters
)
if has_pagination:
paginated_endpoints += 1
return (paginated_endpoints / collection_endpoints * 100) if collection_endpoints > 0 else 100
def _check_compression_support(self) -> float:
"""Check compression support indicators."""
# This is speculative - OpenAPI doesn't directly specify compression
# Look for indicators that compression is considered
servers = self.spec.get('servers', [])
# Check if any server descriptions mention compression
compression_mentions = 0
for server in servers:
if isinstance(server, dict):
description = server.get('description', '').lower()
if any(term in description for term in ['gzip', 'compress', 'deflate']):
compression_mentions += 1
# Base score - assume compression is handled at server level
base_score = 70
if compression_mentions > 0:
return min(base_score + (compression_mentions * 10), 100)
return base_score
def _check_efficiency_patterns(self) -> float:
"""Check efficiency patterns like field selection."""
paths = self.spec.get('paths', {})
total_get_operations = 0
operations_with_selection = 0
for path_obj in paths.values():
if not isinstance(path_obj, dict):
continue
get_operation = path_obj.get('get')
if get_operation and isinstance(get_operation, dict):
total_get_operations += 1
# Check for field selection parameters
parameters = get_operation.get('parameters', [])
selection_params = {'fields', 'select', 'include', 'exclude'}
has_selection = any(
isinstance(param, dict) and param.get('name', '').lower() in selection_params
for param in parameters
)
if has_selection:
operations_with_selection += 1
return (operations_with_selection / total_get_operations * 100) if total_get_operations > 0 else 60
def _check_batch_operations(self) -> float:
"""Check for batch operation support."""
paths = self.spec.get('paths', {})
# Look for batch endpoints
batch_indicators = ['batch', 'bulk', 'multi']
batch_endpoints = 0
for path in paths.keys():
if any(indicator in path.lower() for indicator in batch_indicators):
batch_endpoints += 1
# Look for array-based request bodies (indicating batch operations)
array_operations = 0
total_post_put_operations = 0
for path_obj in paths.values():
if not isinstance(path_obj, dict):
continue
for method in ['post', 'put', 'patch']:
operation = path_obj.get(method)
if operation and isinstance(operation, dict):
total_post_put_operations += 1
request_body = operation.get('requestBody', {})
content = request_body.get('content', {})
for media_obj in content.values():
schema = media_obj.get('schema', {})
if schema.get('type') == 'array':
array_operations += 1
break
# Score based on presence of batch patterns
batch_score = min(batch_endpoints * 20, 60) # Up to 60 points for explicit batch endpoints
if total_post_put_operations > 0:
array_score = (array_operations / total_post_put_operations) * 40
batch_score += array_score
return min(batch_score, 100)
def generate_json_report(self) -> str:
"""Generate JSON format scorecard."""
report_data = {
"overall": {
"score": round(self.scorecard.overall_score, 2),
"grade": self.scorecard.overall_grade,
"totalEndpoints": self.scorecard.total_endpoints
},
"api_info": self.scorecard.api_info,
"categories": {},
"topRecommendations": self.scorecard.get_top_recommendations()
}
for category, score in self.scorecard.category_scores.items():
report_data["categories"][category.value] = {
"score": round(score.score, 2),
"grade": score.letter_grade,
"weight": score.weight,
"weightedScore": round(score.weighted_score, 2),
"issues": score.issues,
"recommendations": score.recommendations
}
return json.dumps(report_data, indent=2)
def generate_text_report(self) -> str:
"""Generate human-readable scorecard report."""
lines = [
"═══════════════════════════════════════════════════════════════",
" API DESIGN SCORECARD",
"═══════════════════════════════════════════════════════════════",
f"API: {self.scorecard.api_info.get('title', 'Unknown')}",
f"Version: {self.scorecard.api_info.get('version', 'Unknown')}",
f"Total Endpoints: {self.scorecard.total_endpoints}",
"",
f"🏆 OVERALL GRADE: {self.scorecard.overall_grade} ({self.scorecard.overall_score:.1f}/100.0)",
"",
"═══════════════════════════════════════════════════════════════",
"DETAILED BREAKDOWN:",
"═══════════════════════════════════════════════════════════════"
]
# Sort categories by weight (most important first)
sorted_categories = sorted(
self.scorecard.category_scores.items(),
key=lambda x: x[1].weight,
reverse=True
)
for category, score in sorted_categories:
category_name = category.value.title().replace('_', ' ')
lines.extend([
"",
f"📊 {category_name.upper()} - Grade: {score.letter_grade} ({score.score:.1f}/100)",
f" Weight: {score.weight}% | Contribution: {score.weighted_score:.1f} points",
" " + "─" * 50
])
if score.recommendations:
lines.append(" 💡 Recommendations:")
for rec in score.recommendations[:3]: # Top 3 recommendations
lines.append(f" • {rec}")
else:
lines.append(" ✅ No specific recommendations - performing well!")
# Overall assessment
lines.extend([
"",
"═══════════════════════════════════════════════════════════════",
"OVERALL ASSESSMENT:",
"═══════════════════════════════════════════════════════════════"
])
if self.scorecard.overall_grade == "A":
lines.extend([
"🏆 EXCELLENT! Your API demonstrates outstanding design quality.",
" Continue following these best practices and consider sharing",
" your approach as a reference for other teams."
])
elif self.scorecard.overall_grade == "B":
lines.extend([
"✅ GOOD! Your API follows most best practices with room for",
" minor improvements. Focus on the recommendations above",
" to achieve excellence."
])
elif self.scorecard.overall_grade == "C":
lines.extend([
"⚠️ FAIR! Your API has a solid foundation but several areas",
" need improvement. Prioritize the high-weight categories",
" for maximum impact."
])
elif self.scorecard.overall_grade == "D":
lines.extend([
"❌ NEEDS IMPROVEMENT! Your API has significant issues that",
" may impact developer experience and maintainability.",
" Focus on consistency and documentation first."
])
else: # Grade F
lines.extend([
"🚨 CRITICAL ISSUES! Your API requires major redesign to meet",
" basic quality standards. Consider comprehensive review",
" of design principles and best practices."
])
# Top recommendations
top_recs = self.scorecard.get_top_recommendations(3)
if top_recs:
lines.extend([
"",
"🎯 TOP PRIORITY RECOMMENDATIONS:",
""
])
for i, rec in enumerate(top_recs, 1):
lines.append(f" {i}. {rec}")
lines.extend([
"",
"═══════════════════════════════════════════════════════════════",
f"Generated by API Scorecard Tool | Score: {self.scorecard.overall_grade} ({self.scorecard.overall_score:.1f}%)",
"═══════════════════════════════════════════════════════════════"
])
return "\n".join(lines)
def main():
"""Main CLI entry point."""
parser = argparse.ArgumentParser(
description="Generate comprehensive API design quality scorecard",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python api_scorecard.py openapi.json
python api_scorecard.py --format json openapi.json > scorecard.json
python api_scorecard.py --output scorecard.txt openapi.json
"""
)
parser.add_argument(
'spec_file',
help='OpenAPI/Swagger specification file (JSON format)'
)
parser.add_argument(
'--format',
choices=['text', 'json'],
default='text',
help='Output format (default: text)'
)
parser.add_argument(
'--output',
help='Output file (default: stdout)'
)
parser.add_argument(
'--min-grade',
choices=['A', 'B', 'C', 'D', 'F'],
help='Exit with code 1 if grade is below minimum'
)
args = parser.parse_args()
# Load specification file
try:
with open(args.spec_file, 'r') as f:
spec = json.load(f)
except FileNotFoundError:
print(f"Error: Specification file '{args.spec_file}' not found.", file=sys.stderr)
return 1
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in '{args.spec_file}': {e}", file=sys.stderr)
return 1
# Initialize scoring engine and generate scorecard
engine = APIScoringEngine()
try:
scorecard = engine.score_api(spec)
except Exception as e:
print(f"Error during scoring: {e}", file=sys.stderr)
return 1
# Generate report
if args.format == 'json':
output = engine.generate_json_report()
else:
output = engine.generate_text_report()
# Write output
if args.output:
try:
with open(args.output, 'w') as f:
f.write(output)
print(f"Scorecard written to {args.output}")
except IOError as e:
print(f"Error writing to '{args.output}': {e}", file=sys.stderr)
return 1
else:
print(output)
# Check minimum grade requirement
if args.min_grade:
grade_order = ['F', 'D', 'C', 'B', 'A']
current_grade_index = grade_order.index(scorecard.overall_grade)
min_grade_index = grade_order.index(args.min_grade)
if current_grade_index < min_grade_index:
print(f"Grade {scorecard.overall_grade} is below minimum required grade {args.min_grade}", file=sys.stderr)
return 1
return 0
if __name__ == '__main__':
sys.exit(main())
FILE:scripts/breaking_change_detector.py
#!/usr/bin/env python3
"""
Breaking Change Detector - Compares API specification versions to identify breaking changes.
This script analyzes two versions of an API specification and detects potentially
breaking changes including:
- Removed endpoints
- Modified response structures
- Removed or renamed fields
- Field type changes
- New required fields
- HTTP status code changes
- Parameter changes
Generates detailed reports with migration guides for each breaking change.
"""
import argparse
import json
import sys
from typing import Any, Dict, List, Set, Optional, Tuple, Union
from dataclasses import dataclass, field
from enum import Enum
class ChangeType(Enum):
"""Types of API changes."""
BREAKING = "breaking"
POTENTIALLY_BREAKING = "potentially_breaking"
NON_BREAKING = "non_breaking"
ENHANCEMENT = "enhancement"
class ChangeSeverity(Enum):
"""Severity levels for changes."""
CRITICAL = "critical" # Will definitely break clients
HIGH = "high" # Likely to break some clients
MEDIUM = "medium" # May break clients depending on usage
LOW = "low" # Minor impact, unlikely to break clients
INFO = "info" # Informational, no breaking impact
@dataclass
class Change:
"""Represents a detected change between API versions."""
change_type: ChangeType
severity: ChangeSeverity
category: str
path: str
message: str
old_value: Any = None
new_value: Any = None
migration_guide: str = ""
impact_description: str = ""
def to_dict(self) -> Dict[str, Any]:
"""Convert change to dictionary for JSON serialization."""
return {
"changeType": self.change_type.value,
"severity": self.severity.value,
"category": self.category,
"path": self.path,
"message": self.message,
"oldValue": self.old_value,
"newValue": self.new_value,
"migrationGuide": self.migration_guide,
"impactDescription": self.impact_description
}
@dataclass
class ComparisonReport:
"""Complete comparison report between two API versions."""
changes: List[Change] = field(default_factory=list)
summary: Dict[str, int] = field(default_factory=dict)
def add_change(self, change: Change) -> None:
"""Add a change to the report."""
self.changes.append(change)
def calculate_summary(self) -> None:
"""Calculate summary statistics."""
self.summary = {
"total_changes": len(self.changes),
"breaking_changes": len([c for c in self.changes if c.change_type == ChangeType.BREAKING]),
"potentially_breaking_changes": len([c for c in self.changes if c.change_type == ChangeType.POTENTIALLY_BREAKING]),
"non_breaking_changes": len([c for c in self.changes if c.change_type == ChangeType.NON_BREAKING]),
"enhancements": len([c for c in self.changes if c.change_type == ChangeType.ENHANCEMENT]),
"critical_severity": len([c for c in self.changes if c.severity == ChangeSeverity.CRITICAL]),
"high_severity": len([c for c in self.changes if c.severity == ChangeSeverity.HIGH]),
"medium_severity": len([c for c in self.changes if c.severity == ChangeSeverity.MEDIUM]),
"low_severity": len([c for c in self.changes if c.severity == ChangeSeverity.LOW]),
"info_severity": len([c for c in self.changes if c.severity == ChangeSeverity.INFO])
}
def has_breaking_changes(self) -> bool:
"""Check if report contains any breaking changes."""
return any(c.change_type in [ChangeType.BREAKING, ChangeType.POTENTIALLY_BREAKING]
for c in self.changes)
class BreakingChangeDetector:
"""Main breaking change detection engine."""
def __init__(self):
self.report = ComparisonReport()
self.old_spec: Optional[Dict] = None
self.new_spec: Optional[Dict] = None
def compare_specs(self, old_spec: Dict[str, Any], new_spec: Dict[str, Any]) -> ComparisonReport:
"""Compare two API specifications and detect changes."""
self.old_spec = old_spec
self.new_spec = new_spec
self.report = ComparisonReport()
# Compare different sections of the API specification
self._compare_info_section()
self._compare_servers_section()
self._compare_paths_section()
self._compare_components_section()
self._compare_security_section()
# Calculate summary statistics
self.report.calculate_summary()
return self.report
def _compare_info_section(self) -> None:
"""Compare API info sections."""
old_info = self.old_spec.get('info', {})
new_info = self.new_spec.get('info', {})
# Version comparison
old_version = old_info.get('version', '')
new_version = new_info.get('version', '')
if old_version != new_version:
self.report.add_change(Change(
change_type=ChangeType.NON_BREAKING,
severity=ChangeSeverity.INFO,
category="versioning",
path="/info/version",
message=f"API version changed from '{old_version}' to '{new_version}'",
old_value=old_version,
new_value=new_version,
impact_description="Version change indicates API evolution"
))
# Title comparison
old_title = old_info.get('title', '')
new_title = new_info.get('title', '')
if old_title != new_title:
self.report.add_change(Change(
change_type=ChangeType.NON_BREAKING,
severity=ChangeSeverity.INFO,
category="metadata",
path="/info/title",
message=f"API title changed from '{old_title}' to '{new_title}'",
old_value=old_title,
new_value=new_title,
impact_description="Title change is cosmetic and doesn't affect functionality"
))
def _compare_servers_section(self) -> None:
"""Compare server configurations."""
old_servers = self.old_spec.get('servers', [])
new_servers = self.new_spec.get('servers', [])
old_urls = {server.get('url', '') for server in old_servers if isinstance(server, dict)}
new_urls = {server.get('url', '') for server in new_servers if isinstance(server, dict)}
# Removed servers
removed_urls = old_urls - new_urls
for url in removed_urls:
self.report.add_change(Change(
change_type=ChangeType.BREAKING,
severity=ChangeSeverity.HIGH,
category="servers",
path="/servers",
message=f"Server URL removed: {url}",
old_value=url,
new_value=None,
migration_guide=f"Update client configurations to use alternative server URLs: {list(new_urls)}",
impact_description="Clients configured to use removed server URL will fail to connect"
))
# Added servers
added_urls = new_urls - old_urls
for url in added_urls:
self.report.add_change(Change(
change_type=ChangeType.ENHANCEMENT,
severity=ChangeSeverity.INFO,
category="servers",
path="/servers",
message=f"New server URL added: {url}",
old_value=None,
new_value=url,
impact_description="New server option provides additional deployment flexibility"
))
def _compare_paths_section(self) -> None:
"""Compare API paths and operations."""
old_paths = self.old_spec.get('paths', {})
new_paths = self.new_spec.get('paths', {})
# Find removed, added, and modified paths
old_path_set = set(old_paths.keys())
new_path_set = set(new_paths.keys())
removed_paths = old_path_set - new_path_set
added_paths = new_path_set - old_path_set
common_paths = old_path_set & new_path_set
# Handle removed paths
for path in removed_paths:
old_operations = self._extract_operations(old_paths[path])
for method in old_operations:
self.report.add_change(Change(
change_type=ChangeType.BREAKING,
severity=ChangeSeverity.CRITICAL,
category="endpoints",
path=f"/paths{path}",
message=f"Endpoint removed: {method.upper()} {path}",
old_value=f"{method.upper()} {path}",
new_value=None,
migration_guide=self._generate_endpoint_removal_migration(path, method, new_paths),
impact_description="Clients using this endpoint will receive 404 errors"
))
# Handle added paths
for path in added_paths:
new_operations = self._extract_operations(new_paths[path])
for method in new_operations:
self.report.add_change(Change(
change_type=ChangeType.ENHANCEMENT,
severity=ChangeSeverity.INFO,
category="endpoints",
path=f"/paths{path}",
message=f"New endpoint added: {method.upper()} {path}",
old_value=None,
new_value=f"{method.upper()} {path}",
impact_description="New functionality available to clients"
))
# Handle modified paths
for path in common_paths:
self._compare_path_operations(path, old_paths[path], new_paths[path])
def _extract_operations(self, path_object: Dict[str, Any]) -> List[str]:
"""Extract HTTP operations from a path object."""
http_methods = {'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'}
return [method for method in path_object.keys() if method.lower() in http_methods]
def _compare_path_operations(self, path: str, old_path_obj: Dict, new_path_obj: Dict) -> None:
"""Compare operations within a specific path."""
old_operations = set(self._extract_operations(old_path_obj))
new_operations = set(self._extract_operations(new_path_obj))
# Removed operations
removed_ops = old_operations - new_operations
for method in removed_ops:
self.report.add_change(Change(
change_type=ChangeType.BREAKING,
severity=ChangeSeverity.CRITICAL,
category="endpoints",
path=f"/paths{path}/{method}",
message=f"HTTP method removed: {method.upper()} {path}",
old_value=f"{method.upper()} {path}",
new_value=None,
migration_guide=self._generate_method_removal_migration(path, method, new_operations),
impact_description="Clients using this method will receive 405 Method Not Allowed errors"
))
# Added operations
added_ops = new_operations - old_operations
for method in added_ops:
self.report.add_change(Change(
change_type=ChangeType.ENHANCEMENT,
severity=ChangeSeverity.INFO,
category="endpoints",
path=f"/paths{path}/{method}",
message=f"New HTTP method added: {method.upper()} {path}",
old_value=None,
new_value=f"{method.upper()} {path}",
impact_description="New method provides additional functionality for this resource"
))
# Modified operations
common_ops = old_operations & new_operations
for method in common_ops:
self._compare_operation_details(path, method, old_path_obj[method], new_path_obj[method])
def _compare_operation_details(self, path: str, method: str, old_op: Dict, new_op: Dict) -> None:
"""Compare details of individual operations."""
operation_path = f"/paths{path}/{method}"
# Compare parameters
self._compare_parameters(operation_path, old_op.get('parameters', []), new_op.get('parameters', []))
# Compare request body
self._compare_request_body(operation_path, old_op.get('requestBody'), new_op.get('requestBody'))
# Compare responses
self._compare_responses(operation_path, old_op.get('responses', {}), new_op.get('responses', {}))
# Compare security requirements
self._compare_security_requirements(operation_path, old_op.get('security'), new_op.get('security'))
def _compare_parameters(self, base_path: str, old_params: List[Dict], new_params: List[Dict]) -> None:
"""Compare operation parameters."""
# Create lookup dictionaries
old_param_map = {(p.get('name'), p.get('in')): p for p in old_params}
new_param_map = {(p.get('name'), p.get('in')): p for p in new_params}
old_param_keys = set(old_param_map.keys())
new_param_keys = set(new_param_map.keys())
# Removed parameters
removed_params = old_param_keys - new_param_keys
for param_key in removed_params:
name, location = param_key
self.report.add_change(Change(
change_type=ChangeType.BREAKING,
severity=ChangeSeverity.HIGH,
category="parameters",
path=f"{base_path}/parameters",
message=f"Parameter removed: {name} (in: {location})",
old_value=old_param_map[param_key],
new_value=None,
migration_guide=f"Remove '{name}' parameter from {location} when calling this endpoint",
impact_description="Clients sending this parameter may receive validation errors"
))
# Added parameters
added_params = new_param_keys - old_param_keys
for param_key in added_params:
name, location = param_key
new_param = new_param_map[param_key]
is_required = new_param.get('required', False)
if is_required:
self.report.add_change(Change(
change_type=ChangeType.BREAKING,
severity=ChangeSeverity.CRITICAL,
category="parameters",
path=f"{base_path}/parameters",
message=f"New required parameter added: {name} (in: {location})",
old_value=None,
new_value=new_param,
migration_guide=f"Add required '{name}' parameter to {location} when calling this endpoint",
impact_description="Clients not providing this parameter will receive 400 Bad Request errors"
))
else:
self.report.add_change(Change(
change_type=ChangeType.NON_BREAKING,
severity=ChangeSeverity.INFO,
category="parameters",
path=f"{base_path}/parameters",
message=f"New optional parameter added: {name} (in: {location})",
old_value=None,
new_value=new_param,
impact_description="Optional parameter provides additional functionality"
))
# Modified parameters
common_params = old_param_keys & new_param_keys
for param_key in common_params:
name, location = param_key
old_param = old_param_map[param_key]
new_param = new_param_map[param_key]
self._compare_parameter_details(base_path, name, location, old_param, new_param)
def _compare_parameter_details(self, base_path: str, name: str, location: str,
old_param: Dict, new_param: Dict) -> None:
"""Compare individual parameter details."""
param_path = f"{base_path}/parameters/{name}"
# Required status change
old_required = old_param.get('required', False)
new_required = new_param.get('required', False)
if old_required != new_required:
if new_required:
self.report.add_change(Change(
change_type=ChangeType.BREAKING,
severity=ChangeSeverity.HIGH,
category="parameters",
path=param_path,
message=f"Parameter '{name}' is now required (was optional)",
old_value=old_required,
new_value=new_required,
migration_guide=f"Ensure '{name}' parameter is always provided when calling this endpoint",
impact_description="Clients not providing this parameter will receive validation errors"
))
else:
self.report.add_change(Change(
change_type=ChangeType.NON_BREAKING,
severity=ChangeSeverity.INFO,
category="parameters",
path=param_path,
message=f"Parameter '{name}' is now optional (was required)",
old_value=old_required,
new_value=new_required,
impact_description="Parameter is now optional, providing more flexibility to clients"
))
# Schema/type changes
old_schema = old_param.get('schema', {})
new_schema = new_param.get('schema', {})
if old_schema != new_schema:
self._compare_schemas(param_path, old_schema, new_schema, f"parameter '{name}'")
def _compare_request_body(self, base_path: str, old_body: Optional[Dict], new_body: Optional[Dict]) -> None:
"""Compare request body specifications."""
body_path = f"{base_path}/requestBody"
# Request body added
if old_body is None and new_body is not None:
is_required = new_body.get('required', False)
if is_required:
self.report.add_change(Change(
change_type=ChangeType.BREAKING,
severity=ChangeSeverity.HIGH,
category="request_body",
path=body_path,
message="Required request body added",
old_value=None,
new_value=new_body,
migration_guide="Include request body with appropriate content type when calling this endpoint",
impact_description="Clients not providing request body will receive validation errors"
))
else:
self.report.add_change(Change(
change_type=ChangeType.NON_BREAKING,
severity=ChangeSeverity.INFO,
category="request_body",
path=body_path,
message="Optional request body added",
old_value=None,
new_value=new_body,
impact_description="Optional request body provides additional functionality"
))
# Request body removed
elif old_body is not None and new_body is None:
self.report.add_change(Change(
change_type=ChangeType.BREAKING,
severity=ChangeSeverity.HIGH,
category="request_body",
path=body_path,
message="Request body removed",
old_value=old_body,
new_value=None,
migration_guide="Remove request body when calling this endpoint",
impact_description="Clients sending request body may receive validation errors"
))
# Request body modified
elif old_body is not None and new_body is not None:
self._compare_request_body_details(body_path, old_body, new_body)
def _compare_request_body_details(self, base_path: str, old_body: Dict, new_body: Dict) -> None:
"""Compare request body details."""
# Required status change
old_required = old_body.get('required', False)
new_required = new_body.get('required', False)
if old_required != new_required:
if new_required:
self.report.add_change(Change(
change_type=ChangeType.BREAKING,
severity=ChangeSeverity.HIGH,
category="request_body",
path=base_path,
message="Request body is now required (was optional)",
old_value=old_required,
new_value=new_required,
migration_guide="Always include request body when calling this endpoint",
impact_description="Clients not providing request body will receive validation errors"
))
else:
self.report.add_change(Change(
change_type=ChangeType.NON_BREAKING,
severity=ChangeSeverity.INFO,
category="request_body",
path=base_path,
message="Request body is now optional (was required)",
old_value=old_required,
new_value=new_required,
impact_description="Request body is now optional, providing more flexibility"
))
# Content type changes
old_content = old_body.get('content', {})
new_content = new_body.get('content', {})
self._compare_content_types(base_path, old_content, new_content, "request body")
def _compare_responses(self, base_path: str, old_responses: Dict, new_responses: Dict) -> None:
"""Compare response specifications."""
responses_path = f"{base_path}/responses"
old_status_codes = set(old_responses.keys())
new_status_codes = set(new_responses.keys())
# Removed status codes
removed_codes = old_status_codes - new_status_codes
for code in removed_codes:
self.report.add_change(Change(
change_type=ChangeType.BREAKING,
severity=ChangeSeverity.HIGH,
category="responses",
path=f"{responses_path}/{code}",
message=f"Response status code {code} removed",
old_value=old_responses[code],
new_value=None,
migration_guide=f"Handle alternative status codes: {list(new_status_codes)}",
impact_description=f"Clients expecting status code {code} need to handle different responses"
))
# Added status codes
added_codes = new_status_codes - old_status_codes
for code in added_codes:
self.report.add_change(Change(
change_type=ChangeType.NON_BREAKING,
severity=ChangeSeverity.INFO,
category="responses",
path=f"{responses_path}/{code}",
message=f"New response status code {code} added",
old_value=None,
new_value=new_responses[code],
impact_description="New status code provides more specific response information"
))
# Modified responses
common_codes = old_status_codes & new_status_codes
for code in common_codes:
self._compare_response_details(responses_path, code, old_responses[code], new_responses[code])
def _compare_response_details(self, base_path: str, status_code: str,
old_response: Dict, new_response: Dict) -> None:
"""Compare individual response details."""
response_path = f"{base_path}/{status_code}"
# Compare content types and schemas
old_content = old_response.get('content', {})
new_content = new_response.get('content', {})
self._compare_content_types(response_path, old_content, new_content, f"response {status_code}")
def _compare_content_types(self, base_path: str, old_content: Dict, new_content: Dict, context: str) -> None:
"""Compare content types and their schemas."""
old_types = set(old_content.keys())
new_types = set(new_content.keys())
# Removed content types
removed_types = old_types - new_types
for content_type in removed_types:
self.report.add_change(Change(
change_type=ChangeType.BREAKING,
severity=ChangeSeverity.HIGH,
category="content_types",
path=f"{base_path}/content",
message=f"Content type '{content_type}' removed from {context}",
old_value=content_type,
new_value=None,
migration_guide=f"Use alternative content types: {list(new_types)}",
impact_description=f"Clients expecting '{content_type}' need to handle different formats"
))
# Added content types
added_types = new_types - old_types
for content_type in added_types:
self.report.add_change(Change(
change_type=ChangeType.ENHANCEMENT,
severity=ChangeSeverity.INFO,
category="content_types",
path=f"{base_path}/content",
message=f"New content type '{content_type}' added to {context}",
old_value=None,
new_value=content_type,
impact_description=f"Additional format option available for {context}"
))
# Modified schemas for common content types
common_types = old_types & new_types
for content_type in common_types:
old_media = old_content[content_type]
new_media = new_content[content_type]
old_schema = old_media.get('schema', {})
new_schema = new_media.get('schema', {})
if old_schema != new_schema:
schema_path = f"{base_path}/content/{content_type}/schema"
self._compare_schemas(schema_path, old_schema, new_schema, f"{context} ({content_type})")
def _compare_schemas(self, base_path: str, old_schema: Dict, new_schema: Dict, context: str) -> None:
"""Compare schema definitions."""
# Type changes
old_type = old_schema.get('type')
new_type = new_schema.get('type')
if old_type != new_type and old_type is not None and new_type is not None:
self.report.add_change(Change(
change_type=ChangeType.BREAKING,
severity=ChangeSeverity.CRITICAL,
category="schema",
path=base_path,
message=f"Schema type changed from '{old_type}' to '{new_type}' for {context}",
old_value=old_type,
new_value=new_type,
migration_guide=f"Update client code to handle {new_type} instead of {old_type}",
impact_description="Type change will break client parsing and validation"
))
# Property changes for object types
if old_schema.get('type') == 'object' and new_schema.get('type') == 'object':
self._compare_object_properties(base_path, old_schema, new_schema, context)
# Array item changes
if old_schema.get('type') == 'array' and new_schema.get('type') == 'array':
old_items = old_schema.get('items', {})
new_items = new_schema.get('items', {})
if old_items != new_items:
self._compare_schemas(f"{base_path}/items", old_items, new_items, f"{context} items")
def _compare_object_properties(self, base_path: str, old_schema: Dict, new_schema: Dict, context: str) -> None:
"""Compare object schema properties."""
old_props = old_schema.get('properties', {})
new_props = new_schema.get('properties', {})
old_required = set(old_schema.get('required', []))
new_required = set(new_schema.get('required', []))
old_prop_names = set(old_props.keys())
new_prop_names = set(new_props.keys())
# Removed properties
removed_props = old_prop_names - new_prop_names
for prop_name in removed_props:
severity = ChangeSeverity.CRITICAL if prop_name in old_required else ChangeSeverity.HIGH
self.report.add_change(Change(
change_type=ChangeType.BREAKING,
severity=severity,
category="schema",
path=f"{base_path}/properties",
message=f"Property '{prop_name}' removed from {context}",
old_value=old_props[prop_name],
new_value=None,
migration_guide=f"Remove references to '{prop_name}' property in client code",
impact_description="Clients expecting this property will receive incomplete data"
))
# Added properties
added_props = new_prop_names - old_prop_names
for prop_name in added_props:
if prop_name in new_required:
# This is handled separately in required field changes
pass
else:
self.report.add_change(Change(
change_type=ChangeType.NON_BREAKING,
severity=ChangeSeverity.INFO,
category="schema",
path=f"{base_path}/properties",
message=f"New optional property '{prop_name}' added to {context}",
old_value=None,
new_value=new_props[prop_name],
impact_description="New property provides additional data without breaking existing clients"
))
# Required field changes
added_required = new_required - old_required
removed_required = old_required - new_required
for prop_name in added_required:
self.report.add_change(Change(
change_type=ChangeType.BREAKING,
severity=ChangeSeverity.CRITICAL,
category="schema",
path=f"{base_path}/properties",
message=f"Property '{prop_name}' is now required in {context}",
old_value=False,
new_value=True,
migration_guide=f"Ensure '{prop_name}' is always provided when sending {context}",
impact_description="Clients not providing this property will receive validation errors"
))
for prop_name in removed_required:
self.report.add_change(Change(
change_type=ChangeType.NON_BREAKING,
severity=ChangeSeverity.INFO,
category="schema",
path=f"{base_path}/properties",
message=f"Property '{prop_name}' is no longer required in {context}",
old_value=True,
new_value=False,
impact_description="Property is now optional, providing more flexibility"
))
# Modified properties
common_props = old_prop_names & new_prop_names
for prop_name in common_props:
old_prop = old_props[prop_name]
new_prop = new_props[prop_name]
if old_prop != new_prop:
self._compare_schemas(f"{base_path}/properties/{prop_name}",
old_prop, new_prop, f"{context}.{prop_name}")
def _compare_security_requirements(self, base_path: str, old_security: Optional[List],
new_security: Optional[List]) -> None:
"""Compare security requirements."""
# Simplified security comparison - could be expanded
if old_security != new_security:
severity = ChangeSeverity.HIGH if new_security else ChangeSeverity.CRITICAL
change_type = ChangeType.BREAKING
if old_security is None and new_security is not None:
message = "Security requirements added"
migration_guide = "Ensure proper authentication/authorization when calling this endpoint"
impact = "Endpoint now requires authentication"
elif old_security is not None and new_security is None:
message = "Security requirements removed"
migration_guide = "Authentication is no longer required for this endpoint"
impact = "Endpoint is now publicly accessible"
severity = ChangeSeverity.MEDIUM # Less severe, more permissive
else:
message = "Security requirements modified"
migration_guide = "Update authentication/authorization method for this endpoint"
impact = "Different authentication method required"
self.report.add_change(Change(
change_type=change_type,
severity=severity,
category="security",
path=f"{base_path}/security",
message=message,
old_value=old_security,
new_value=new_security,
migration_guide=migration_guide,
impact_description=impact
))
def _compare_components_section(self) -> None:
"""Compare components sections."""
old_components = self.old_spec.get('components', {})
new_components = self.new_spec.get('components', {})
# Compare schemas
old_schemas = old_components.get('schemas', {})
new_schemas = new_components.get('schemas', {})
old_schema_names = set(old_schemas.keys())
new_schema_names = set(new_schemas.keys())
# Removed schemas
removed_schemas = old_schema_names - new_schema_names
for schema_name in removed_schemas:
self.report.add_change(Change(
change_type=ChangeType.BREAKING,
severity=ChangeSeverity.HIGH,
category="components",
path=f"/components/schemas/{schema_name}",
message=f"Schema '{schema_name}' removed from components",
old_value=old_schemas[schema_name],
new_value=None,
migration_guide=f"Remove references to schema '{schema_name}' or use alternative schemas",
impact_description="References to this schema will fail validation"
))
# Added schemas
added_schemas = new_schema_names - old_schema_names
for schema_name in added_schemas:
self.report.add_change(Change(
change_type=ChangeType.ENHANCEMENT,
severity=ChangeSeverity.INFO,
category="components",
path=f"/components/schemas/{schema_name}",
message=f"New schema '{schema_name}' added to components",
old_value=None,
new_value=new_schemas[schema_name],
impact_description="New reusable schema available"
))
# Modified schemas
common_schemas = old_schema_names & new_schema_names
for schema_name in common_schemas:
old_schema = old_schemas[schema_name]
new_schema = new_schemas[schema_name]
if old_schema != new_schema:
self._compare_schemas(f"/components/schemas/{schema_name}",
old_schema, new_schema, f"schema '{schema_name}'")
def _compare_security_section(self) -> None:
"""Compare security definitions."""
old_security_schemes = self.old_spec.get('components', {}).get('securitySchemes', {})
new_security_schemes = self.new_spec.get('components', {}).get('securitySchemes', {})
if old_security_schemes != new_security_schemes:
# Simplified comparison - could be more detailed
self.report.add_change(Change(
change_type=ChangeType.POTENTIALLY_BREAKING,
severity=ChangeSeverity.MEDIUM,
category="security",
path="/components/securitySchemes",
message="Security scheme definitions changed",
old_value=old_security_schemes,
new_value=new_security_schemes,
migration_guide="Review authentication implementation for compatibility with new security schemes",
impact_description="Authentication mechanisms may have changed"
))
def _generate_endpoint_removal_migration(self, removed_path: str, method: str,
remaining_paths: Dict[str, Any]) -> str:
"""Generate migration guide for removed endpoints."""
# Look for similar endpoints
similar_paths = []
path_segments = removed_path.strip('/').split('/')
for existing_path in remaining_paths.keys():
existing_segments = existing_path.strip('/').split('/')
if len(existing_segments) == len(path_segments):
# Check similarity
similarity = sum(1 for i, seg in enumerate(path_segments)
if i < len(existing_segments) and seg == existing_segments[i])
if similarity >= len(path_segments) * 0.5: # At least 50% similar
similar_paths.append(existing_path)
if similar_paths:
return f"Consider using alternative endpoints: {', '.join(similar_paths[:3])}"
else:
return "No direct replacement available. Review API documentation for alternative approaches."
def _generate_method_removal_migration(self, path: str, removed_method: str,
remaining_methods: Set[str]) -> str:
"""Generate migration guide for removed HTTP methods."""
method_alternatives = {
'get': ['head'],
'post': ['put', 'patch'],
'put': ['post', 'patch'],
'patch': ['put', 'post'],
'delete': []
}
alternatives = []
for alt_method in method_alternatives.get(removed_method.lower(), []):
if alt_method in remaining_methods:
alternatives.append(alt_method.upper())
if alternatives:
return f"Use alternative methods: {', '.join(alternatives)}"
else:
return f"No alternative HTTP methods available for {path}"
def generate_json_report(self) -> str:
"""Generate JSON format report."""
report_data = {
"summary": self.report.summary,
"hasBreakingChanges": self.report.has_breaking_changes(),
"changes": [change.to_dict() for change in self.report.changes]
}
return json.dumps(report_data, indent=2)
def generate_text_report(self) -> str:
"""Generate human-readable text report."""
lines = [
"═══════════════════════════════════════════════════════════════",
" BREAKING CHANGE ANALYSIS REPORT",
"═══════════════════════════════════════════════════════════════",
"",
"SUMMARY:",
f" Total Changes: {self.report.summary.get('total_changes', 0)}",
f" 🔴 Breaking Changes: {self.report.summary.get('breaking_changes', 0)}",
f" 🟡 Potentially Breaking: {self.report.summary.get('potentially_breaking_changes', 0)}",
f" 🟢 Non-Breaking Changes: {self.report.summary.get('non_breaking_changes', 0)}",
f" ✨ Enhancements: {self.report.summary.get('enhancements', 0)}",
"",
"SEVERITY BREAKDOWN:",
f" 🚨 Critical: {self.report.summary.get('critical_severity', 0)}",
f" ⚠️ High: {self.report.summary.get('high_severity', 0)}",
f" ⚪ Medium: {self.report.summary.get('medium_severity', 0)}",
f" 🔵 Low: {self.report.summary.get('low_severity', 0)}",
f" ℹ️ Info: {self.report.summary.get('info_severity', 0)}",
""
]
if not self.report.changes:
lines.extend([
"🎉 No changes detected between the API versions!",
""
])
else:
# Group changes by type and severity
breaking_changes = [c for c in self.report.changes if c.change_type == ChangeType.BREAKING]
potentially_breaking = [c for c in self.report.changes if c.change_type == ChangeType.POTENTIALLY_BREAKING]
non_breaking = [c for c in self.report.changes if c.change_type == ChangeType.NON_BREAKING]
enhancements = [c for c in self.report.changes if c.change_type == ChangeType.ENHANCEMENT]
# Breaking changes section
if breaking_changes:
lines.extend([
"🔴 BREAKING CHANGES:",
"═" * 60
])
for change in sorted(breaking_changes, key=lambda x: x.severity.value):
self._add_change_to_report(lines, change)
lines.append("")
# Potentially breaking changes section
if potentially_breaking:
lines.extend([
"🟡 POTENTIALLY BREAKING CHANGES:",
"═" * 60
])
for change in sorted(potentially_breaking, key=lambda x: x.severity.value):
self._add_change_to_report(lines, change)
lines.append("")
# Non-breaking changes section
if non_breaking:
lines.extend([
"🟢 NON-BREAKING CHANGES:",
"═" * 60
])
for change in non_breaking:
self._add_change_to_report(lines, change)
lines.append("")
# Enhancements section
if enhancements:
lines.extend([
"✨ ENHANCEMENTS:",
"═" * 60
])
for change in enhancements:
self._add_change_to_report(lines, change)
lines.append("")
# Add overall assessment
lines.extend([
"═══════════════════════════════════════════════════════════════",
"OVERALL ASSESSMENT:",
"═══════════════════════════════════════════════════════════════"
])
if self.report.has_breaking_changes():
breaking_count = self.report.summary.get('breaking_changes', 0)
potentially_breaking_count = self.report.summary.get('potentially_breaking_changes', 0)
if breaking_count > 0:
lines.extend([
f"⛔ MAJOR VERSION BUMP REQUIRED",
f" This API version contains {breaking_count} breaking changes that will",
f" definitely break existing clients. A major version bump is required.",
""
])
elif potentially_breaking_count > 0:
lines.extend([
f"⚠️ MINOR VERSION BUMP RECOMMENDED",
f" This API version contains {potentially_breaking_count} potentially breaking",
f" changes. Consider a minor version bump and communicate changes to clients.",
""
])
else:
lines.extend([
"✅ PATCH VERSION BUMP ACCEPTABLE",
" No breaking changes detected. This version is backward compatible",
" with existing clients.",
""
])
return "\n".join(lines)
def _add_change_to_report(self, lines: List[str], change: Change) -> None:
"""Add a change to the text report."""
severity_icons = {
ChangeSeverity.CRITICAL: "🚨",
ChangeSeverity.HIGH: "⚠️ ",
ChangeSeverity.MEDIUM: "⚪",
ChangeSeverity.LOW: "🔵",
ChangeSeverity.INFO: "ℹ️ "
}
icon = severity_icons.get(change.severity, "❓")
lines.extend([
f"{icon} {change.severity.value.upper()}: {change.message}",
f" Path: {change.path}",
f" Category: {change.category}"
])
if change.impact_description:
lines.append(f" Impact: {change.impact_description}")
if change.migration_guide:
lines.append(f" 💡 Migration: {change.migration_guide}")
lines.append("")
def main():
"""Main CLI entry point."""
parser = argparse.ArgumentParser(
description="Compare API specification versions to detect breaking changes",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python breaking_change_detector.py v1.json v2.json
python breaking_change_detector.py --format json v1.json v2.json > changes.json
python breaking_change_detector.py --output report.txt v1.json v2.json
"""
)
parser.add_argument(
'old_spec',
help='Old API specification file (JSON format)'
)
parser.add_argument(
'new_spec',
help='New API specification file (JSON format)'
)
parser.add_argument(
'--format',
choices=['text', 'json'],
default='text',
help='Output format (default: text)'
)
parser.add_argument(
'--output',
help='Output file (default: stdout)'
)
parser.add_argument(
'--exit-on-breaking',
action='store_true',
help='Exit with code 1 if breaking changes are detected'
)
args = parser.parse_args()
# Load specification files
try:
with open(args.old_spec, 'r') as f:
old_spec = json.load(f)
except FileNotFoundError:
print(f"Error: Old specification file '{args.old_spec}' not found.", file=sys.stderr)
return 1
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in '{args.old_spec}': {e}", file=sys.stderr)
return 1
try:
with open(args.new_spec, 'r') as f:
new_spec = json.load(f)
except FileNotFoundError:
print(f"Error: New specification file '{args.new_spec}' not found.", file=sys.stderr)
return 1
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in '{args.new_spec}': {e}", file=sys.stderr)
return 1
# Initialize detector and compare specifications
detector = BreakingChangeDetector()
try:
report = detector.compare_specs(old_spec, new_spec)
except Exception as e:
print(f"Error during comparison: {e}", file=sys.stderr)
return 1
# Generate report
if args.format == 'json':
output = detector.generate_json_report()
else:
output = detector.generate_text_report()
# Write output
if args.output:
try:
with open(args.output, 'w') as f:
f.write(output)
print(f"Breaking change report written to {args.output}")
except IOError as e:
print(f"Error writing to '{args.output}': {e}", file=sys.stderr)
return 1
else:
print(output)
# Exit with appropriate code
if args.exit_on_breaking and report.has_breaking_changes():
return 1
return 0
if __name__ == '__main__':
sys.exit(main())