'v1'
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "awesome-skills-plugin",
|
||||||
|
"version": "12.8.0",
|
||||||
|
"description": "Curated Fullstack & DevOps Developer Pack",
|
||||||
|
"entry": "skills"
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
---
|
||||||
|
name: acceptance-orchestrator
|
||||||
|
description: Use when a coding task should be driven end-to-end from issue intake through implementation, review, deployment, and acceptance verification with minimal human re-intervention.
|
||||||
|
risk: safe
|
||||||
|
source: community
|
||||||
|
date_added: "2026-03-12"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Acceptance Orchestrator
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Orchestrate coding work as a state machine that ends only when acceptance criteria are verified with evidence or the task is explicitly escalated.
|
||||||
|
|
||||||
|
Core rule: **do not optimize for "code changed"; optimize for "DoD proven".**
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
- The task already has an issue or clear acceptance criteria and should run end-to-end with minimal human re-intervention.
|
||||||
|
- You need structured handoff across implementation, review, deployment, and final verification.
|
||||||
|
- You want explicit stop conditions and escalation instead of silent partial completion.
|
||||||
|
|
||||||
|
## Required Sub-Skills
|
||||||
|
|
||||||
|
- `create-issue-gate`
|
||||||
|
- `closed-loop-delivery`
|
||||||
|
- `verification-before-completion`
|
||||||
|
|
||||||
|
Optional supporting skills:
|
||||||
|
- `deploy-dev`
|
||||||
|
- `pr-watch`
|
||||||
|
- `pr-review-autopilot`
|
||||||
|
- `git-ship`
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
Require these inputs:
|
||||||
|
- issue id or issue body
|
||||||
|
- issue status
|
||||||
|
- acceptance criteria (DoD)
|
||||||
|
- target environment (`dev` default)
|
||||||
|
|
||||||
|
Fixed defaults:
|
||||||
|
- max iteration rounds = `2`
|
||||||
|
- PR review polling = `3m -> 6m -> 10m`
|
||||||
|
|
||||||
|
## State Machine
|
||||||
|
|
||||||
|
- `intake`
|
||||||
|
- `issue-gated`
|
||||||
|
- `executing`
|
||||||
|
- `review-loop`
|
||||||
|
- `deploy-verify`
|
||||||
|
- `accepted`
|
||||||
|
- `escalated`
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Intake**
|
||||||
|
- Read issue and extract task goal + DoD.
|
||||||
|
|
||||||
|
2. **Issue gate**
|
||||||
|
- Use `create-issue-gate` logic.
|
||||||
|
- If issue is not `ready` or execution gate is not `allowed`, stop immediately.
|
||||||
|
- Do not implement anything while issue remains `draft`.
|
||||||
|
|
||||||
|
3. **Execute**
|
||||||
|
- Hand off to `closed-loop-delivery` for implementation and local verification.
|
||||||
|
|
||||||
|
4. **Review loop**
|
||||||
|
- If PR feedback is relevant, batch polling windows as:
|
||||||
|
- wait `3m`
|
||||||
|
- then `6m`
|
||||||
|
- then `10m`
|
||||||
|
- After the `10m` round, stop waiting and process all visible comments together.
|
||||||
|
|
||||||
|
5. **Deploy and runtime verification**
|
||||||
|
- If DoD depends on runtime behavior, deploy only to `dev` by default.
|
||||||
|
- Verify with real logs/API/Lambda behavior, not assumptions.
|
||||||
|
|
||||||
|
6. **Completion gate**
|
||||||
|
- Before any claim of completion, require `verification-before-completion`.
|
||||||
|
- No success claim without fresh evidence.
|
||||||
|
|
||||||
|
## Stop Conditions
|
||||||
|
|
||||||
|
Move to `accepted` only when every acceptance criterion has matching evidence.
|
||||||
|
|
||||||
|
Move to `escalated` when any of these happen:
|
||||||
|
- DoD still fails after `2` full rounds
|
||||||
|
- missing secrets/permissions/external dependency blocks progress
|
||||||
|
- task needs production action or destructive operation approval
|
||||||
|
- review instructions conflict and cannot both be satisfied
|
||||||
|
|
||||||
|
## Human Gates
|
||||||
|
|
||||||
|
Always stop for human confirmation on:
|
||||||
|
- prod/stage deploys beyond agreed scope
|
||||||
|
- destructive git/data operations
|
||||||
|
- billing or security posture changes
|
||||||
|
- missing user-provided acceptance criteria
|
||||||
|
|
||||||
|
## Output Contract
|
||||||
|
|
||||||
|
When reporting status, always include:
|
||||||
|
- `Status`: intake / executing / accepted / escalated
|
||||||
|
- `Acceptance Criteria`: pass/fail checklist
|
||||||
|
- `Evidence`: commands, logs, API results, or runtime proof
|
||||||
|
- `Open Risks`: anything still uncertain
|
||||||
|
- `Need Human Input`: smallest next decision, if blocked
|
||||||
|
|
||||||
|
Do not report "done" unless status is `accepted`.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
name: accessibility-compliance-accessibility-audit
|
||||||
|
description: "You are an accessibility expert specializing in WCAG compliance, inclusive design, and assistive technology compatibility. Conduct audits, identify barriers, and provide remediation guidance."
|
||||||
|
risk: safe
|
||||||
|
source: community
|
||||||
|
date_added: "2026-02-27"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Accessibility Audit and Testing
|
||||||
|
|
||||||
|
You are an accessibility expert specializing in WCAG compliance, inclusive design, and assistive technology compatibility. Conduct comprehensive audits, identify barriers, provide remediation guidance, and ensure digital products are accessible to all users.
|
||||||
|
|
||||||
|
## Use this skill when
|
||||||
|
|
||||||
|
- Auditing web or mobile experiences for WCAG compliance
|
||||||
|
- Identifying accessibility barriers and remediation priorities
|
||||||
|
- Establishing ongoing accessibility testing practices
|
||||||
|
- Preparing compliance evidence for stakeholders
|
||||||
|
|
||||||
|
## Do not use this skill when
|
||||||
|
|
||||||
|
- You only need a general UI design review without accessibility scope
|
||||||
|
- The request is unrelated to user experience or compliance
|
||||||
|
- You cannot access the UI, design artifacts, or content
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The user needs to audit and improve accessibility to ensure compliance with WCAG standards and provide an inclusive experience for users with disabilities. Focus on automated testing, manual verification, remediation strategies, and establishing ongoing accessibility practices.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
$ARGUMENTS
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
- Confirm scope (platforms, WCAG level, target pages, key user journeys).
|
||||||
|
- Run automated scans to collect baseline violations and coverage gaps.
|
||||||
|
- Perform manual checks (keyboard, screen reader, focus order, contrast).
|
||||||
|
- Map findings to WCAG criteria, severity, and user impact.
|
||||||
|
- Provide remediation steps and re-test after fixes.
|
||||||
|
- If detailed procedures are required, open `resources/implementation-playbook.md`.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- `resources/implementation-playbook.md` for detailed audit steps, tooling, and remediation examples.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
+502
@@ -0,0 +1,502 @@
|
|||||||
|
# Accessibility Audit and Testing Implementation Playbook
|
||||||
|
|
||||||
|
This file contains detailed patterns, checklists, and code samples referenced by the skill.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
### 1. Automated Testing with axe-core
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// accessibility-test.js
|
||||||
|
const { AxePuppeteer } = require("@axe-core/puppeteer");
|
||||||
|
const puppeteer = require("puppeteer");
|
||||||
|
|
||||||
|
class AccessibilityAuditor {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.wcagLevel = options.wcagLevel || "AA";
|
||||||
|
this.viewport = options.viewport || { width: 1920, height: 1080 };
|
||||||
|
}
|
||||||
|
|
||||||
|
async runFullAudit(url) {
|
||||||
|
const browser = await puppeteer.launch();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport(this.viewport);
|
||||||
|
await page.goto(url, { waitUntil: "networkidle2" });
|
||||||
|
|
||||||
|
const results = await new AxePuppeteer(page)
|
||||||
|
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
|
||||||
|
.exclude(".no-a11y-check")
|
||||||
|
.analyze();
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
violations: results.violations.map((v) => ({
|
||||||
|
id: v.id,
|
||||||
|
impact: v.impact,
|
||||||
|
description: v.description,
|
||||||
|
help: v.help,
|
||||||
|
helpUrl: v.helpUrl,
|
||||||
|
nodes: v.nodes.map((n) => ({
|
||||||
|
html: n.html,
|
||||||
|
target: n.target,
|
||||||
|
failureSummary: n.failureSummary,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
score: this.calculateScore(results),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateScore(results) {
|
||||||
|
const weights = { critical: 10, serious: 5, moderate: 2, minor: 1 };
|
||||||
|
let totalWeight = 0;
|
||||||
|
results.violations.forEach((v) => {
|
||||||
|
totalWeight += weights[v.impact] || 0;
|
||||||
|
});
|
||||||
|
return Math.max(0, 100 - totalWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component testing with jest-axe
|
||||||
|
import { render } from "@testing-library/react";
|
||||||
|
import { axe, toHaveNoViolations } from "jest-axe";
|
||||||
|
|
||||||
|
expect.extend(toHaveNoViolations);
|
||||||
|
|
||||||
|
describe("Accessibility Tests", () => {
|
||||||
|
it("should have no violations", async () => {
|
||||||
|
const { container } = render(<MyComponent />);
|
||||||
|
const results = await axe(container);
|
||||||
|
expect(results).toHaveNoViolations();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Color Contrast Validation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// color-contrast.js
|
||||||
|
class ColorContrastAnalyzer {
|
||||||
|
constructor() {
|
||||||
|
this.wcagLevels = {
|
||||||
|
'AA': { normal: 4.5, large: 3 },
|
||||||
|
'AAA': { normal: 7, large: 4.5 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async analyzePageContrast(page) {
|
||||||
|
const elements = await page.evaluate(() => {
|
||||||
|
return Array.from(document.querySelectorAll('*'))
|
||||||
|
.filter(el => el.innerText && el.innerText.trim())
|
||||||
|
.map(el => {
|
||||||
|
const styles = window.getComputedStyle(el);
|
||||||
|
return {
|
||||||
|
text: el.innerText.trim().substring(0, 50),
|
||||||
|
color: styles.color,
|
||||||
|
backgroundColor: styles.backgroundColor,
|
||||||
|
fontSize: parseFloat(styles.fontSize),
|
||||||
|
fontWeight: styles.fontWeight
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return elements
|
||||||
|
.map(el => {
|
||||||
|
const contrast = this.calculateContrast(el.color, el.backgroundColor);
|
||||||
|
const isLarge = this.isLargeText(el.fontSize, el.fontWeight);
|
||||||
|
const required = isLarge ? this.wcagLevels.AA.large : this.wcagLevels.AA.normal;
|
||||||
|
|
||||||
|
if (contrast < required) {
|
||||||
|
return {
|
||||||
|
text: el.text,
|
||||||
|
currentContrast: contrast.toFixed(2),
|
||||||
|
requiredContrast: required,
|
||||||
|
foreground: el.color,
|
||||||
|
background: el.backgroundColor
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateContrast(fg, bg) {
|
||||||
|
const l1 = this.relativeLuminance(this.parseColor(fg));
|
||||||
|
const l2 = this.relativeLuminance(this.parseColor(bg));
|
||||||
|
const lighter = Math.max(l1, l2);
|
||||||
|
const darker = Math.min(l1, l2);
|
||||||
|
return (lighter + 0.05) / (darker + 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
relativeLuminance(rgb) {
|
||||||
|
const [r, g, b] = rgb.map(val => {
|
||||||
|
val = val / 255;
|
||||||
|
return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
|
||||||
|
});
|
||||||
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// High contrast CSS
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
:root {
|
||||||
|
--text-primary: #000;
|
||||||
|
--bg-primary: #fff;
|
||||||
|
--border-color: #000;
|
||||||
|
}
|
||||||
|
a { text-decoration: underline !important; }
|
||||||
|
button, input { border: 2px solid var(--border-color) !important; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Keyboard Navigation Testing
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// keyboard-navigation.js
|
||||||
|
class KeyboardNavigationTester {
|
||||||
|
async testKeyboardNavigation(page) {
|
||||||
|
const results = {
|
||||||
|
focusableElements: [],
|
||||||
|
missingFocusIndicators: [],
|
||||||
|
keyboardTraps: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all focusable elements
|
||||||
|
const focusable = await page.evaluate(() => {
|
||||||
|
const selector =
|
||||||
|
'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
||||||
|
return Array.from(document.querySelectorAll(selector)).map((el) => ({
|
||||||
|
tagName: el.tagName.toLowerCase(),
|
||||||
|
text: el.innerText || el.value || el.placeholder || "",
|
||||||
|
tabIndex: el.tabIndex,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
results.focusableElements = focusable;
|
||||||
|
|
||||||
|
// Test tab order and focus indicators
|
||||||
|
for (let i = 0; i < focusable.length; i++) {
|
||||||
|
await page.keyboard.press("Tab");
|
||||||
|
|
||||||
|
const focused = await page.evaluate(() => {
|
||||||
|
const el = document.activeElement;
|
||||||
|
return {
|
||||||
|
tagName: el.tagName.toLowerCase(),
|
||||||
|
hasFocusIndicator: window.getComputedStyle(el).outline !== "none",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!focused.hasFocusIndicator) {
|
||||||
|
results.missingFocusIndicators.push(focused);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhance keyboard accessibility
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
const modal = document.querySelector(".modal.open");
|
||||||
|
if (modal) closeModal(modal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make div clickable accessible
|
||||||
|
document.querySelectorAll("[onclick]").forEach((el) => {
|
||||||
|
if (!["a", "button", "input"].includes(el.tagName.toLowerCase())) {
|
||||||
|
el.setAttribute("tabindex", "0");
|
||||||
|
el.setAttribute("role", "button");
|
||||||
|
el.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
el.click();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Screen Reader Testing
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// screen-reader-test.js
|
||||||
|
class ScreenReaderTester {
|
||||||
|
async testScreenReaderCompatibility(page) {
|
||||||
|
return {
|
||||||
|
landmarks: await this.testLandmarks(page),
|
||||||
|
headings: await this.testHeadingStructure(page),
|
||||||
|
images: await this.testImageAccessibility(page),
|
||||||
|
forms: await this.testFormAccessibility(page),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async testHeadingStructure(page) {
|
||||||
|
const headings = await page.evaluate(() => {
|
||||||
|
return Array.from(
|
||||||
|
document.querySelectorAll("h1, h2, h3, h4, h5, h6"),
|
||||||
|
).map((h) => ({
|
||||||
|
level: parseInt(h.tagName[1]),
|
||||||
|
text: h.textContent.trim(),
|
||||||
|
isEmpty: !h.textContent.trim(),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const issues = [];
|
||||||
|
let previousLevel = 0;
|
||||||
|
|
||||||
|
headings.forEach((heading, index) => {
|
||||||
|
if (heading.level > previousLevel + 1 && previousLevel !== 0) {
|
||||||
|
issues.push({
|
||||||
|
type: "skipped-level",
|
||||||
|
message: `Heading level ${heading.level} skips from level ${previousLevel}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (heading.isEmpty) {
|
||||||
|
issues.push({ type: "empty-heading", index });
|
||||||
|
}
|
||||||
|
previousLevel = heading.level;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!headings.some((h) => h.level === 1)) {
|
||||||
|
issues.push({ type: "missing-h1", message: "Page missing h1 element" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { headings, issues };
|
||||||
|
}
|
||||||
|
|
||||||
|
async testFormAccessibility(page) {
|
||||||
|
const forms = await page.evaluate(() => {
|
||||||
|
return Array.from(document.querySelectorAll("form")).map((form) => {
|
||||||
|
const inputs = form.querySelectorAll("input, textarea, select");
|
||||||
|
return {
|
||||||
|
fields: Array.from(inputs).map((input) => ({
|
||||||
|
type: input.type || input.tagName.toLowerCase(),
|
||||||
|
id: input.id,
|
||||||
|
hasLabel: input.id
|
||||||
|
? !!document.querySelector(`label[for="${input.id}"]`)
|
||||||
|
: !!input.closest("label"),
|
||||||
|
hasAriaLabel: !!input.getAttribute("aria-label"),
|
||||||
|
required: input.required,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const issues = [];
|
||||||
|
forms.forEach((form, i) => {
|
||||||
|
form.fields.forEach((field, j) => {
|
||||||
|
if (!field.hasLabel && !field.hasAriaLabel) {
|
||||||
|
issues.push({ type: "missing-label", form: i, field: j });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { forms, issues };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ARIA patterns
|
||||||
|
const ariaPatterns = {
|
||||||
|
modal: `
|
||||||
|
<div role="dialog" aria-labelledby="modal-title" aria-modal="true">
|
||||||
|
<h2 id="modal-title">Modal Title</h2>
|
||||||
|
<button aria-label="Close">×</button>
|
||||||
|
</div>`,
|
||||||
|
|
||||||
|
tabs: `
|
||||||
|
<div role="tablist" aria-label="Navigation">
|
||||||
|
<button role="tab" aria-selected="true" aria-controls="panel-1">Tab 1</button>
|
||||||
|
</div>
|
||||||
|
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">Content</div>`,
|
||||||
|
|
||||||
|
form: `
|
||||||
|
<label for="name">Name <span aria-label="required">*</span></label>
|
||||||
|
<input id="name" required aria-required="true" aria-describedby="name-error">
|
||||||
|
<span id="name-error" role="alert" aria-live="polite"></span>`,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Manual Testing Checklist
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Manual Accessibility Testing
|
||||||
|
|
||||||
|
### Keyboard Navigation
|
||||||
|
|
||||||
|
- [ ] All interactive elements accessible via Tab
|
||||||
|
- [ ] Buttons activate with Enter/Space
|
||||||
|
- [ ] Esc key closes modals
|
||||||
|
- [ ] Focus indicator always visible
|
||||||
|
- [ ] No keyboard traps
|
||||||
|
- [ ] Logical tab order
|
||||||
|
|
||||||
|
### Screen Reader
|
||||||
|
|
||||||
|
- [ ] Page title descriptive
|
||||||
|
- [ ] Headings create logical outline
|
||||||
|
- [ ] Images have alt text
|
||||||
|
- [ ] Form fields have labels
|
||||||
|
- [ ] Error messages announced
|
||||||
|
- [ ] Dynamic updates announced
|
||||||
|
|
||||||
|
### Visual
|
||||||
|
|
||||||
|
- [ ] Text resizes to 200% without loss
|
||||||
|
- [ ] Color not sole means of info
|
||||||
|
- [ ] Focus indicators have sufficient contrast
|
||||||
|
- [ ] Content reflows at 320px
|
||||||
|
- [ ] Animations can be paused
|
||||||
|
|
||||||
|
### Cognitive
|
||||||
|
|
||||||
|
- [ ] Instructions clear and simple
|
||||||
|
- [ ] Error messages helpful
|
||||||
|
- [ ] No time limits on forms
|
||||||
|
- [ ] Navigation consistent
|
||||||
|
- [ ] Important actions reversible
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Remediation Examples
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Fix missing alt text
|
||||||
|
document.querySelectorAll("img:not([alt])").forEach((img) => {
|
||||||
|
const isDecorative =
|
||||||
|
img.role === "presentation" || img.closest('[role="presentation"]');
|
||||||
|
img.setAttribute("alt", isDecorative ? "" : img.title || "Image");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fix missing labels
|
||||||
|
document
|
||||||
|
.querySelectorAll("input:not([aria-label]):not([id])")
|
||||||
|
.forEach((input) => {
|
||||||
|
if (input.placeholder) {
|
||||||
|
input.setAttribute("aria-label", input.placeholder);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// React accessible components
|
||||||
|
const AccessibleButton = ({ children, onClick, ariaLabel, ...props }) => (
|
||||||
|
<button onClick={onClick} aria-label={ariaLabel} {...props}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LiveRegion = ({ message, politeness = "polite" }) => (
|
||||||
|
<div
|
||||||
|
role="status"
|
||||||
|
aria-live={politeness}
|
||||||
|
aria-atomic="true"
|
||||||
|
className="sr-only"
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. CI/CD Integration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/accessibility.yml
|
||||||
|
name: Accessibility Tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
a11y-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
|
|
||||||
|
- name: Install and build
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Start server
|
||||||
|
run: |
|
||||||
|
npm start &
|
||||||
|
npx wait-on http://localhost:3000
|
||||||
|
|
||||||
|
- name: Run axe tests
|
||||||
|
run: npm run test:a11y
|
||||||
|
|
||||||
|
- name: Run pa11y
|
||||||
|
run: npx pa11y http://localhost:3000 --standard WCAG2AA --threshold 0
|
||||||
|
|
||||||
|
- name: Upload report
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: a11y-report
|
||||||
|
path: a11y-report.html
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Reporting
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// report-generator.js
|
||||||
|
class AccessibilityReportGenerator {
|
||||||
|
generateHTMLReport(auditResults) {
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Accessibility Audit</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||||
|
.summary { background: #f0f0f0; padding: 20px; border-radius: 8px; }
|
||||||
|
.score { font-size: 48px; font-weight: bold; }
|
||||||
|
.violation { margin: 20px 0; padding: 15px; border: 1px solid #ddd; }
|
||||||
|
.critical { border-color: #f00; background: #fee; }
|
||||||
|
.serious { border-color: #fa0; background: #ffe; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Accessibility Audit Report</h1>
|
||||||
|
<p>Generated: ${new Date().toLocaleString()}</p>
|
||||||
|
|
||||||
|
<div class="summary">
|
||||||
|
<h2>Summary</h2>
|
||||||
|
<div class="score">${auditResults.score}/100</div>
|
||||||
|
<p>Total Violations: ${auditResults.violations.length}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Violations</h2>
|
||||||
|
${auditResults.violations
|
||||||
|
.map(
|
||||||
|
(v) => `
|
||||||
|
<div class="violation ${v.impact}">
|
||||||
|
<h3>${v.help}</h3>
|
||||||
|
<p><strong>Impact:</strong> ${v.impact}</p>
|
||||||
|
<p>${v.description}</p>
|
||||||
|
<a href="${v.helpUrl}">Learn more</a>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
1. **Accessibility Score**: Overall compliance with WCAG levels
|
||||||
|
2. **Violation Report**: Detailed issues with severity and fixes
|
||||||
|
3. **Test Results**: Automated and manual test outcomes
|
||||||
|
4. **Remediation Guide**: Step-by-step fixes for each issue
|
||||||
|
5. **Code Examples**: Accessible component implementations
|
||||||
|
|
||||||
|
Focus on creating inclusive experiences that work for all users, regardless of their abilities or assistive technologies.
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
---
|
||||||
|
name: accesslint-audit
|
||||||
|
description: "Find and fix WCAG 2.2 accessibility issues. Two modes — report (sweep a codebase or page, produce a prioritized written report, no edits) and fix (audit→edit→verify loop on a target). Prefers direct-CDP live-DOM auditing; falls back to a browser-MCP composition or HTML-string audits."
|
||||||
|
risk: safe
|
||||||
|
source: "https://github.com/AccessLint/skills"
|
||||||
|
date_added: "2026-06-02"
|
||||||
|
---
|
||||||
|
|
||||||
|
You audit accessibility and optionally fix what's broken.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
- Use this skill when the task matches this description: Find and fix WCAG 2.2 accessibility issues. Two modes — report (sweep a codebase or page, produce a prioritized written report, no edits) and fix (audit→edit→verify loop on a target). Prefers direct-CDP live-DOM auditing; falls back to a browser-MCP composition or HTML-string audits.
|
||||||
|
|
||||||
|
## Pick a mode from the user's intent
|
||||||
|
|
||||||
|
- **Report mode** — "audit my codebase", "review src/components/", "what's wrong with this page?", "give me an a11y report". You audit + write a report. **You do not edit files.**
|
||||||
|
- **Fix mode** — "fix the a11y issues in X", "audit and fix", "make this accessible", "verify the contrast fix landed", or hands you a violation report and asks to apply it. You audit → edit → verify.
|
||||||
|
|
||||||
|
If unsure, ask. Don't default-to-fix when the user only asked for an audit.
|
||||||
|
|
||||||
|
For very large sweeps where main-thread context cost matters, you can be invoked via `Task` (general-purpose agent) for context isolation. The recipe is the same either way.
|
||||||
|
|
||||||
|
## Picking a flow
|
||||||
|
|
||||||
|
Three flows, in order of preference.
|
||||||
|
|
||||||
|
1. **`audit_live`** — try first for any URL. Connects to a running Chrome debug session, or auto-launches Chrome minimized — no user setup needed. Single call; IIFE bytes don't enter your context.
|
||||||
|
2. **`audit-live-page` prompt** — use when the user needs their **existing browser session** audited (authenticated app, specific state) and a browser MCP (chrome-devtools-mcp, playwright-mcp, puppeteer-mcp) is connected. Invoke via `Skill` with `mode: "fix"` or `mode: "plan"`.
|
||||||
|
3. **`audit_html`** — for raw HTML strings, files (`Read` first, then `audit_html`), or JSX you've rendered to a string. Pair with `audit_diff({ html })` for fix-mode verification.
|
||||||
|
|
||||||
|
For non-URL targets, skip straight to flow 3. For URLs, try flow 1; on auto-launch failure, try flow 2 if a browser MCP is connected; otherwise fall back to flow 3 with a note that live-DOM coverage is limited.
|
||||||
|
|
||||||
|
## Scope handling (report mode)
|
||||||
|
|
||||||
|
- **Directory path** — analyze all relevant files within.
|
||||||
|
- **Multiple files** — analyze the listed files plus imports they reach.
|
||||||
|
- **A URL** — audit it. If it's a dev-server URL, that's flow 1 or 2.
|
||||||
|
- **No arguments** — ask the user to narrow scope. Whole-codebase sweeps are rarely the right thing.
|
||||||
|
|
||||||
|
State the scope explicitly at the start of your report.
|
||||||
|
|
||||||
|
## Approach (report mode)
|
||||||
|
|
||||||
|
1. **Map the surface.** Glob/Grep to enumerate components, templates, styles. Sample representative files; don't open everything blindly.
|
||||||
|
2. **Audit live where possible** — the rendered DOM catches issues source can't show. Use the flow picker above.
|
||||||
|
3. **Look for patterns.** If one component fails a rule, similar components likely do too. Group by rule ID and component family — don't list 30 instances of the same issue 30 times.
|
||||||
|
4. **Prioritize by user impact.** Critical/serious first. Many low-impact violations of one rule are often a single root-cause fix.
|
||||||
|
5. **Use `format: "compact"` for sweep-time calls.** Reserve verbose output for rules you'll expand in the report.
|
||||||
|
6. **Trust `Source:` lines.** Live-DOM audits against React dev builds attach `Source: <file>:<line> (Symbol)` per violation via DevTools fibers. Use it as the file pointer instead of grepping selectors. Fall back to stable hooks → visible text → tree position when absent.
|
||||||
|
7. **Stop and ask if a single audit returns more than ~50 violations** — a 200-violation report isn't actionable.
|
||||||
|
|
||||||
|
The engine catches what's mechanically detectable. Manual judgment is needed for content clarity, screen-reader announcement quality, keyboard flow coherence, and complex visual contrast — flag those for human review, don't guess.
|
||||||
|
|
||||||
|
### Report format
|
||||||
|
|
||||||
|
```
|
||||||
|
# Accessibility audit — <scope>
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
- N critical, M serious, K moderate, J minor (after deduplication)
|
||||||
|
- Most impactful patterns: <one-line each, max 3>
|
||||||
|
|
||||||
|
## Critical (blocks access)
|
||||||
|
For each pattern:
|
||||||
|
- **Pattern**: <one-line description>
|
||||||
|
- **WCAG**: <ID> — <name>
|
||||||
|
- **Affected files**: <file:line> (×N if repeated)
|
||||||
|
- **Fix**: <directive from engine output, or specific code change>
|
||||||
|
- **Why critical**: <user impact>
|
||||||
|
|
||||||
|
## Serious
|
||||||
|
[same shape]
|
||||||
|
|
||||||
|
## Moderate / Minor
|
||||||
|
[Bullet list, deduplicated by rule. Skip per-instance detail unless the fix differs.]
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
- Architectural / pattern-level changes that would prevent recurrence.
|
||||||
|
- Tooling or component abstractions worth introducing.
|
||||||
|
- What to verify manually (screen reader, keyboard, low-vision testing).
|
||||||
|
|
||||||
|
## Positive findings
|
||||||
|
What the codebase does well — short, factual, reinforces practices to keep.
|
||||||
|
```
|
||||||
|
|
||||||
|
Include rule IDs in every entry. Quote the `Fix:` directive verbatim for `mechanical` rules. For `visual` / `contextual`, leave a `TODO` with the rule ID; don't invent content.
|
||||||
|
|
||||||
|
## Recipe (fix mode)
|
||||||
|
|
||||||
|
1. **Baseline.** Audit with `name: "before"` and `format: "compact"`.
|
||||||
|
2. **Plan + apply.** For each violation:
|
||||||
|
- `Source:` line present → open that file at that line. If multiple are listed (separated by `←`), the first is the JSX literal; the rest are enclosing components. Use `Symbol` to disambiguate.
|
||||||
|
- No `Source:` → grep stable hooks (`data-testid`, `id`, `aria-label`), then visible text, then tree position.
|
||||||
|
- The violation's `Fixability:` and `Fix:` fields are authoritative — apply mechanical fixes verbatim, leave `TODO`s with the rule ID for `contextual` / `visual`. Never invent content.
|
||||||
|
- Group same-file edits into one operation.
|
||||||
|
- Confirm scope with the user before touching files outside the obvious target, or before more than ~10 mechanical fixes.
|
||||||
|
3. **Verify.** Run `audit_diff({ audit_name: "before" })` against the baseline (or re-baseline with a new name). Confirm `-fixed` covers your targets and `+new` is empty.
|
||||||
|
|
||||||
|
`Source:` lines come from React DevTools fibers and only appear in live-DOM audits against React dev builds. Static audits won't have them — fall back to selectors.
|
||||||
|
|
||||||
|
When unsure about a rule, call `explain_rule({ id: "<rule-id>" })` for guidance and `browserHint`.
|
||||||
|
|
||||||
|
## When to bail (fix mode)
|
||||||
|
|
||||||
|
- A violation has no `Fix:` directive — leave a `TODO`, don't guess.
|
||||||
|
- Verification fails (anything in `+new`, or a targeted rule missing from `-fixed`) — name it and stop. Do not iterate silently.
|
||||||
|
|
||||||
|
## Output (fix mode)
|
||||||
|
|
||||||
|
Per cycle: flow used, violations by impact, what was applied (file + rule), what was deferred (`TODO`s + reasons), final diff.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
name: accesslint-diff
|
||||||
|
description: "Diff a live page's accessibility violations against a baseline — by default compares uncommitted changes (stash-based), or pass --branch [<name>] to diff against a branch. Reports only new violations introduced, violations fixed, and pre-existing count. Use `scan` for a full audit with no diffing."
|
||||||
|
risk: safe
|
||||||
|
source: "https://github.com/AccessLint/skills"
|
||||||
|
date_added: "2026-06-02"
|
||||||
|
---
|
||||||
|
|
||||||
|
Default branch: !`git symbolic-ref refs/remotes/origin/HEAD --short 2>/dev/null | sed 's|.*/||' || echo main`
|
||||||
|
|
||||||
|
Report only what changed. Locate; don't fix. If no URL in `$ARGUMENTS`, ask for one.
|
||||||
|
|
||||||
|
Parse `$ARGUMENTS`: strip `--branch <name>` if present → branch mode. If `--branch` has no value, use the default branch above. Remainder is the URL.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
- Use this skill when the task matches this description: Diff a live page's accessibility violations against a baseline — by default compares uncommitted changes (stash-based), or pass --branch [<name>] to diff against a branch. Reports only new violations introduced, violations fixed, and pre-existing count. Use `scan` for a full audit with no diffing.
|
||||||
|
|
||||||
|
## 1. Audit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PORT=$(npx -y @accesslint/chrome@latest ensure | node -e 'process.stdin.on("data",d=>process.stdout.write(""+JSON.parse(d).port))')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Stash mode** (default — uncommitted changes). Tell the user first: _"Running in diff mode — stashing your changes to capture a baseline, then restoring. Your working tree will be fully restored."_ If `git stash push` fails, warn and exit.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git stash push -u -m "accesslint-diff-baseline"
|
||||||
|
npx -y @accesslint/cli@latest "<url>" --port "$PORT" --snapshot accesslint-diff --snapshot-dir /tmp --update-snapshot
|
||||||
|
git stash pop && sleep 2
|
||||||
|
npx -y @accesslint/cli@latest "<url>" --port "$PORT" --snapshot accesslint-diff --snapshot-dir /tmp --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Branch mode** (`--branch <name>`). Tell the user first: _"Diffing against `<name>` — checking out that branch to capture a baseline, then restoring. Your working tree will be fully restored."_
|
||||||
|
|
||||||
|
Branch switching triggers a rebuild but not a browser reload — the CLI opens a fresh tab each time so it always reads the current build. Use `--wait-for "<selector>"` to gate the audit until the rebuild is ready; without it, warn the user that a slow build may yield a stale baseline.
|
||||||
|
|
||||||
|
Keep the branch value in the quoted `branch` variable below; never paste or evaluate a branch name as shell syntax.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --quiet && git diff --cached --quiet || git stash push -u -m "accesslint-diff-branch"
|
||||||
|
branch="<branch>"
|
||||||
|
git check-ref-format --branch "$branch" >/dev/null
|
||||||
|
case "$branch" in -*) echo "Refusing option-like branch name: $branch" >&2; exit 1 ;; esac
|
||||||
|
git rev-parse --verify --quiet "$branch^{commit}" >/dev/null
|
||||||
|
git switch "$branch"
|
||||||
|
npx -y @accesslint/cli@latest "<url>" --port "$PORT" --snapshot accesslint-diff --snapshot-dir /tmp --update-snapshot [--wait-for "<selector>"]
|
||||||
|
git switch - && git stash pop 2>/dev/null
|
||||||
|
npx -y @accesslint/cli@latest "<url>" --port "$PORT" --snapshot accesslint-diff --snapshot-dir /tmp --format json [--wait-for "<selector>"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass `--selector`, `--include-aaa` to **both** runs.
|
||||||
|
|
||||||
|
## 2. Report
|
||||||
|
|
||||||
|
```
|
||||||
|
Accessibility diff — http://localhost:3000/ vs main (94 rules, live DOM)
|
||||||
|
2 new · 1 fixed · 4 pre-existing hidden
|
||||||
|
|
||||||
|
New — Critical
|
||||||
|
- color-contrast — 2.1:1 (needs 4.5:1), #bbb on #fff
|
||||||
|
where: main > p.subtitle fix: darken to #767676
|
||||||
|
Fixed
|
||||||
|
- img-alt — <img src="old.jpg"> (no longer present)
|
||||||
|
```
|
||||||
|
|
||||||
|
Each new violation: **where** (selector verbatim + `file:line (symbol)` if `source` present — never fabricate), **evidence**, **fix** (mechanical change or `NEEDS HUMAN`).
|
||||||
|
|
||||||
|
Don't edit. For fixes: apply mechanical ones then re-run `accesslint:diff` to verify; for bulk work hand off to `accesslint:audit`.
|
||||||
|
|
||||||
|
## 3. Tear down
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx -y @accesslint/chrome@latest stop --all # skip if ensure reported "managed":false
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- `ensure` always determines the port — never hardcode 9222.
|
||||||
|
- CLI exit 2 = bad URL or page never loaded; check the dev server.
|
||||||
|
- Stash mode: `sleep 2` covers most HMR cases; if baseline looks identical to current, add `--wait-for "<selector>"`.
|
||||||
|
- Branch mode: no HMR — CLI opens a fresh tab each run. `--wait-for` is the rebuild gate.
|
||||||
|
- Heavy DOM changes between runs cause selector drift — re-run with `accesslint:scan` for the full picture.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
name: accesslint-scan
|
||||||
|
description: "Audit a live page for accessibility issues, locate each WCAG violation precisely, and return a selector-grounded fix worklist without editing."
|
||||||
|
risk: safe
|
||||||
|
source: "https://github.com/AccessLint/skills"
|
||||||
|
date_added: "2026-06-02"
|
||||||
|
---
|
||||||
|
|
||||||
|
Audit a live page and report what's broken and where. Locate; don't fix. If no URL in `$ARGUMENTS`, ask for one.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
- Use this skill when the task matches this description: Audit a live page for accessibility issues, locate each WCAG violation precisely, and return a selector-grounded fix worklist without editing.
|
||||||
|
|
||||||
|
## 1. Audit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PORT=$(npx -y @accesslint/chrome@latest ensure | node -e 'process.stdin.on("data",d=>process.stdout.write(""+JSON.parse(d).port))')
|
||||||
|
npx -y @accesslint/cli@latest "<url>" --port "$PORT" --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
Flags as needed: `--selector`, `--wait-for "<selector>"`, `--include-aaa`, `--disable <rules>`.
|
||||||
|
|
||||||
|
## 2. Report
|
||||||
|
|
||||||
|
Counts by impact, then one entry per violation:
|
||||||
|
|
||||||
|
- **where** — selector verbatim + `file:line (symbol)` if `source` is present — never fabricate. If no violation has `source`, note "source mapping unavailable — located by selector only".
|
||||||
|
- **evidence** — contrast ratio, missing attribute, empty name
|
||||||
|
- **fix** — mechanical change or `NEEDS HUMAN`
|
||||||
|
|
||||||
|
Don't edit. For fixes: apply mechanical ones then re-run to verify; for bulk work hand off to `accesslint:audit`.
|
||||||
|
|
||||||
|
## 3. Tear down
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx -y @accesslint/chrome@latest stop --all # skip if ensure reported "managed":false
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- `ensure` always determines the port — never hardcode 9222.
|
||||||
|
- CLI exit 2 = bad URL or page never loaded; check the dev server.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,460 @@
|
|||||||
|
---
|
||||||
|
name: advanced-evaluation
|
||||||
|
description: This skill should be used when the user asks to "implement LLM-as-judge", "compare model outputs", "create evaluation rubrics", "mitigate evaluation bias", or mentions direct scoring, pairwise comparison, position bias, evaluation pipelines, or automated quality assessment.
|
||||||
|
risk: safe
|
||||||
|
source: community
|
||||||
|
date_added: 2026-03-18
|
||||||
|
---
|
||||||
|
|
||||||
|
# Advanced Evaluation
|
||||||
|
|
||||||
|
This skill covers production-grade techniques for evaluating LLM outputs using LLMs as judges. It synthesizes research from academic papers, industry practices, and practical implementation experience into actionable patterns for building reliable evaluation systems.
|
||||||
|
|
||||||
|
**Key insight**: LLM-as-a-Judge is not a single technique but a family of approaches, each suited to different evaluation contexts. Choosing the right approach and mitigating known biases is the core competency this skill develops.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
Activate this skill when:
|
||||||
|
|
||||||
|
- Building automated evaluation pipelines for LLM outputs
|
||||||
|
- Comparing multiple model responses to select the best one
|
||||||
|
- Establishing consistent quality standards across evaluation teams
|
||||||
|
- Debugging evaluation systems that show inconsistent results
|
||||||
|
- Designing A/B tests for prompt or model changes
|
||||||
|
- Creating rubrics for human or automated evaluation
|
||||||
|
- Analyzing correlation between automated and human judgments
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### The Evaluation Taxonomy
|
||||||
|
|
||||||
|
Evaluation approaches fall into two primary categories with distinct reliability profiles:
|
||||||
|
|
||||||
|
**Direct Scoring**: A single LLM rates one response on a defined scale.
|
||||||
|
- Best for: Objective criteria (factual accuracy, instruction following, toxicity)
|
||||||
|
- Reliability: Moderate to high for well-defined criteria
|
||||||
|
- Failure mode: Score calibration drift, inconsistent scale interpretation
|
||||||
|
|
||||||
|
**Pairwise Comparison**: An LLM compares two responses and selects the better one.
|
||||||
|
- Best for: Subjective preferences (tone, style, persuasiveness)
|
||||||
|
- Reliability: Higher than direct scoring for preferences
|
||||||
|
- Failure mode: Position bias, length bias
|
||||||
|
|
||||||
|
Research from the MT-Bench paper (Zheng et al., 2023) establishes that pairwise comparison achieves higher agreement with human judges than direct scoring for preference-based evaluation, while direct scoring remains appropriate for objective criteria with clear ground truth.
|
||||||
|
|
||||||
|
### The Bias Landscape
|
||||||
|
|
||||||
|
LLM judges exhibit systematic biases that must be actively mitigated:
|
||||||
|
|
||||||
|
**Position Bias**: First-position responses receive preferential treatment in pairwise comparison. Mitigation: Evaluate twice with swapped positions, use majority vote or consistency check.
|
||||||
|
|
||||||
|
**Length Bias**: Longer responses are rated higher regardless of quality. Mitigation: Explicit prompting to ignore length, length-normalized scoring.
|
||||||
|
|
||||||
|
**Self-Enhancement Bias**: Models rate their own outputs higher. Mitigation: Use different models for generation and evaluation, or acknowledge limitation.
|
||||||
|
|
||||||
|
**Verbosity Bias**: Detailed explanations receive higher scores even when unnecessary. Mitigation: Criteria-specific rubrics that penalize irrelevant detail.
|
||||||
|
|
||||||
|
**Authority Bias**: Confident, authoritative tone rated higher regardless of accuracy. Mitigation: Require evidence citation, fact-checking layer.
|
||||||
|
|
||||||
|
### Metric Selection Framework
|
||||||
|
|
||||||
|
Choose metrics based on the evaluation task structure:
|
||||||
|
|
||||||
|
| Task Type | Primary Metrics | Secondary Metrics |
|
||||||
|
|-----------|-----------------|-------------------|
|
||||||
|
| Binary classification (pass/fail) | Recall, Precision, F1 | Cohen's κ |
|
||||||
|
| Ordinal scale (1-5 rating) | Spearman's ρ, Kendall's τ | Cohen's κ (weighted) |
|
||||||
|
| Pairwise preference | Agreement rate, Position consistency | Confidence calibration |
|
||||||
|
| Multi-label | Macro-F1, Micro-F1 | Per-label precision/recall |
|
||||||
|
|
||||||
|
The critical insight: High absolute agreement matters less than systematic disagreement patterns. A judge that consistently disagrees with humans on specific criteria is more problematic than one with random noise.
|
||||||
|
|
||||||
|
## Evaluation Approaches
|
||||||
|
|
||||||
|
### Direct Scoring Implementation
|
||||||
|
|
||||||
|
Direct scoring requires three components: clear criteria, a calibrated scale, and structured output format.
|
||||||
|
|
||||||
|
**Criteria Definition Pattern**:
|
||||||
|
```
|
||||||
|
Criterion: [Name]
|
||||||
|
Description: [What this criterion measures]
|
||||||
|
Weight: [Relative importance, 0-1]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Scale Calibration**:
|
||||||
|
- 1-3 scales: Binary with neutral option, lowest cognitive load
|
||||||
|
- 1-5 scales: Standard Likert, good balance of granularity and reliability
|
||||||
|
- 1-10 scales: High granularity but harder to calibrate, use only with detailed rubrics
|
||||||
|
|
||||||
|
**Prompt Structure for Direct Scoring**:
|
||||||
|
```
|
||||||
|
You are an expert evaluator assessing response quality.
|
||||||
|
|
||||||
|
## Task
|
||||||
|
Evaluate the following response against each criterion.
|
||||||
|
|
||||||
|
## Original Prompt
|
||||||
|
{prompt}
|
||||||
|
|
||||||
|
## Response to Evaluate
|
||||||
|
{response}
|
||||||
|
|
||||||
|
## Criteria
|
||||||
|
{for each criterion: name, description, weight}
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
For each criterion:
|
||||||
|
1. Find specific evidence in the response
|
||||||
|
2. Score according to the rubric (1-{max} scale)
|
||||||
|
3. Justify your score with evidence
|
||||||
|
4. Suggest one specific improvement
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
Respond with structured JSON containing scores, justifications, and summary.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Chain-of-Thought Requirement**: All scoring prompts must require justification before the score. Research shows this improves reliability by 15-25% compared to score-first approaches.
|
||||||
|
|
||||||
|
### Pairwise Comparison Implementation
|
||||||
|
|
||||||
|
Pairwise comparison is inherently more reliable for preference-based evaluation but requires bias mitigation.
|
||||||
|
|
||||||
|
**Position Bias Mitigation Protocol**:
|
||||||
|
1. First pass: Response A in first position, Response B in second
|
||||||
|
2. Second pass: Response B in first position, Response A in second
|
||||||
|
3. Consistency check: If passes disagree, return TIE with reduced confidence
|
||||||
|
4. Final verdict: Consistent winner with averaged confidence
|
||||||
|
|
||||||
|
**Prompt Structure for Pairwise Comparison**:
|
||||||
|
```
|
||||||
|
You are an expert evaluator comparing two AI responses.
|
||||||
|
|
||||||
|
## Critical Instructions
|
||||||
|
- Do NOT prefer responses because they are longer
|
||||||
|
- Do NOT prefer responses based on position (first vs second)
|
||||||
|
- Focus ONLY on quality according to the specified criteria
|
||||||
|
- Ties are acceptable when responses are genuinely equivalent
|
||||||
|
|
||||||
|
## Original Prompt
|
||||||
|
{prompt}
|
||||||
|
|
||||||
|
## Response A
|
||||||
|
{response_a}
|
||||||
|
|
||||||
|
## Response B
|
||||||
|
{response_b}
|
||||||
|
|
||||||
|
## Comparison Criteria
|
||||||
|
{criteria list}
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
1. Analyze each response independently first
|
||||||
|
2. Compare them on each criterion
|
||||||
|
3. Determine overall winner with confidence level
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
JSON with per-criterion comparison, overall winner, confidence (0-1), and reasoning.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Confidence Calibration**: Confidence scores should reflect position consistency:
|
||||||
|
- Both passes agree: confidence = average of individual confidences
|
||||||
|
- Passes disagree: confidence = 0.5, verdict = TIE
|
||||||
|
|
||||||
|
### Rubric Generation
|
||||||
|
|
||||||
|
Well-defined rubrics reduce evaluation variance by 40-60% compared to open-ended scoring.
|
||||||
|
|
||||||
|
**Rubric Components**:
|
||||||
|
1. **Level descriptions**: Clear boundaries for each score level
|
||||||
|
2. **Characteristics**: Observable features that define each level
|
||||||
|
3. **Examples**: Representative text for each level (optional but valuable)
|
||||||
|
4. **Edge cases**: Guidance for ambiguous situations
|
||||||
|
5. **Scoring guidelines**: General principles for consistent application
|
||||||
|
|
||||||
|
**Strictness Calibration**:
|
||||||
|
- **Lenient**: Lower bar for passing scores, appropriate for encouraging iteration
|
||||||
|
- **Balanced**: Fair, typical expectations for production use
|
||||||
|
- **Strict**: High standards, appropriate for safety-critical or high-stakes evaluation
|
||||||
|
|
||||||
|
**Domain Adaptation**: Rubrics should use domain-specific terminology. A "code readability" rubric mentions variables, functions, and comments. A "medical accuracy" rubric references clinical terminology and evidence standards.
|
||||||
|
|
||||||
|
## Practical Guidance
|
||||||
|
|
||||||
|
### Evaluation Pipeline Design
|
||||||
|
|
||||||
|
Production evaluation systems require multiple layers:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ Evaluation Pipeline │
|
||||||
|
├─────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Input: Response + Prompt + Context │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────┐ │
|
||||||
|
│ │ Criteria Loader │ ◄── Rubrics, weights │
|
||||||
|
│ └──────────┬──────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────┐ │
|
||||||
|
│ │ Primary Scorer │ ◄── Direct or Pairwise │
|
||||||
|
│ └──────────┬──────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────┐ │
|
||||||
|
│ │ Bias Mitigation │ ◄── Position swap, etc. │
|
||||||
|
│ └──────────┬──────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────┐ │
|
||||||
|
│ │ Confidence Scoring │ ◄── Calibration │
|
||||||
|
│ └──────────┬──────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ Output: Scores + Justifications + Confidence │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Anti-Patterns
|
||||||
|
|
||||||
|
**Anti-pattern: Scoring without justification**
|
||||||
|
- Problem: Scores lack grounding, difficult to debug or improve
|
||||||
|
- Solution: Always require evidence-based justification before score
|
||||||
|
|
||||||
|
**Anti-pattern: Single-pass pairwise comparison**
|
||||||
|
- Problem: Position bias corrupts results
|
||||||
|
- Solution: Always swap positions and check consistency
|
||||||
|
|
||||||
|
**Anti-pattern: Overloaded criteria**
|
||||||
|
- Problem: Criteria measuring multiple things are unreliable
|
||||||
|
- Solution: One criterion = one measurable aspect
|
||||||
|
|
||||||
|
**Anti-pattern: Missing edge case guidance**
|
||||||
|
- Problem: Evaluators handle ambiguous cases inconsistently
|
||||||
|
- Solution: Include edge cases in rubrics with explicit guidance
|
||||||
|
|
||||||
|
**Anti-pattern: Ignoring confidence calibration**
|
||||||
|
- Problem: High-confidence wrong judgments are worse than low-confidence
|
||||||
|
- Solution: Calibrate confidence to position consistency and evidence strength
|
||||||
|
|
||||||
|
### Decision Framework: Direct vs. Pairwise
|
||||||
|
|
||||||
|
Use this decision tree:
|
||||||
|
|
||||||
|
```
|
||||||
|
Is there an objective ground truth?
|
||||||
|
├── Yes → Direct Scoring
|
||||||
|
│ └── Examples: factual accuracy, instruction following, format compliance
|
||||||
|
│
|
||||||
|
└── No → Is it a preference or quality judgment?
|
||||||
|
├── Yes → Pairwise Comparison
|
||||||
|
│ └── Examples: tone, style, persuasiveness, creativity
|
||||||
|
│
|
||||||
|
└── No → Consider reference-based evaluation
|
||||||
|
└── Examples: summarization (compare to source), translation (compare to reference)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scaling Evaluation
|
||||||
|
|
||||||
|
For high-volume evaluation:
|
||||||
|
|
||||||
|
1. **Panel of LLMs (PoLL)**: Use multiple models as judges, aggregate votes
|
||||||
|
- Reduces individual model bias
|
||||||
|
- More expensive but more reliable for high-stakes decisions
|
||||||
|
|
||||||
|
2. **Hierarchical evaluation**: Fast cheap model for screening, expensive model for edge cases
|
||||||
|
- Cost-effective for large volumes
|
||||||
|
- Requires calibration of screening threshold
|
||||||
|
|
||||||
|
3. **Human-in-the-loop**: Automated evaluation for clear cases, human review for low-confidence
|
||||||
|
- Best reliability for critical applications
|
||||||
|
- Design feedback loop to improve automated evaluation
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1: Direct Scoring for Accuracy
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```
|
||||||
|
Prompt: "What causes seasons on Earth?"
|
||||||
|
Response: "Seasons are caused by Earth's tilted axis. As Earth orbits the Sun,
|
||||||
|
different hemispheres receive more direct sunlight at different times of year."
|
||||||
|
Criterion: Factual Accuracy (weight: 1.0)
|
||||||
|
Scale: 1-5
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"criterion": "Factual Accuracy",
|
||||||
|
"score": 5,
|
||||||
|
"evidence": [
|
||||||
|
"Correctly identifies axial tilt as primary cause",
|
||||||
|
"Correctly explains differential sunlight by hemisphere",
|
||||||
|
"No factual errors present"
|
||||||
|
],
|
||||||
|
"justification": "Response accurately explains the cause of seasons with correct
|
||||||
|
scientific reasoning. Both the axial tilt and its effect on sunlight distribution
|
||||||
|
are correctly described.",
|
||||||
|
"improvement": "Could add the specific tilt angle (23.5°) for completeness."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Pairwise Comparison with Position Swap
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```
|
||||||
|
Prompt: "Explain machine learning to a beginner"
|
||||||
|
Response A: [Technical explanation with jargon]
|
||||||
|
Response B: [Simple analogy-based explanation]
|
||||||
|
Criteria: ["clarity", "accessibility"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**First Pass (A first)**:
|
||||||
|
```json
|
||||||
|
{ "winner": "B", "confidence": 0.8 }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Second Pass (B first)**:
|
||||||
|
```json
|
||||||
|
{ "winner": "A", "confidence": 0.6 }
|
||||||
|
```
|
||||||
|
(Note: Winner is A because B was in first position)
|
||||||
|
|
||||||
|
**Mapped Second Pass**:
|
||||||
|
```json
|
||||||
|
{ "winner": "B", "confidence": 0.6 }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Final Result**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"winner": "B",
|
||||||
|
"confidence": 0.7,
|
||||||
|
"positionConsistency": {
|
||||||
|
"consistent": true,
|
||||||
|
"firstPassWinner": "B",
|
||||||
|
"secondPassWinner": "B"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Rubric Generation
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```
|
||||||
|
criterionName: "Code Readability"
|
||||||
|
criterionDescription: "How easy the code is to understand and maintain"
|
||||||
|
domain: "software engineering"
|
||||||
|
scale: "1-5"
|
||||||
|
strictness: "balanced"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output** (abbreviated):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"levels": [
|
||||||
|
{
|
||||||
|
"score": 1,
|
||||||
|
"label": "Poor",
|
||||||
|
"description": "Code is difficult to understand without significant effort",
|
||||||
|
"characteristics": [
|
||||||
|
"No meaningful variable or function names",
|
||||||
|
"No comments or documentation",
|
||||||
|
"Deeply nested or convoluted logic"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"score": 3,
|
||||||
|
"label": "Adequate",
|
||||||
|
"description": "Code is understandable with some effort",
|
||||||
|
"characteristics": [
|
||||||
|
"Most variables have meaningful names",
|
||||||
|
"Basic comments present for complex sections",
|
||||||
|
"Logic is followable but could be cleaner"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"score": 5,
|
||||||
|
"label": "Excellent",
|
||||||
|
"description": "Code is immediately clear and maintainable",
|
||||||
|
"characteristics": [
|
||||||
|
"All names are descriptive and consistent",
|
||||||
|
"Comprehensive documentation",
|
||||||
|
"Clean, modular structure"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edgeCases": [
|
||||||
|
{
|
||||||
|
"situation": "Code is well-structured but uses domain-specific abbreviations",
|
||||||
|
"guidance": "Score based on readability for domain experts, not general audience"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
1. **Always require justification before scores** - Chain-of-thought prompting improves reliability by 15-25%
|
||||||
|
|
||||||
|
2. **Always swap positions in pairwise comparison** - Single-pass comparison is corrupted by position bias
|
||||||
|
|
||||||
|
3. **Match scale granularity to rubric specificity** - Don't use 1-10 without detailed level descriptions
|
||||||
|
|
||||||
|
4. **Separate objective and subjective criteria** - Use direct scoring for objective, pairwise for subjective
|
||||||
|
|
||||||
|
5. **Include confidence scores** - Calibrate to position consistency and evidence strength
|
||||||
|
|
||||||
|
6. **Define edge cases explicitly** - Ambiguous situations cause the most evaluation variance
|
||||||
|
|
||||||
|
7. **Use domain-specific rubrics** - Generic rubrics produce generic (less useful) evaluations
|
||||||
|
|
||||||
|
8. **Validate against human judgments** - Automated evaluation is only valuable if it correlates with human assessment
|
||||||
|
|
||||||
|
9. **Monitor for systematic bias** - Track disagreement patterns by criterion, response type, model
|
||||||
|
|
||||||
|
10. **Design for iteration** - Evaluation systems improve with feedback loops
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
This skill integrates with:
|
||||||
|
|
||||||
|
- **context-fundamentals** - Evaluation prompts require effective context structure
|
||||||
|
- **tool-design** - Evaluation tools need proper schemas and error handling
|
||||||
|
- **context-optimization** - Evaluation prompts can be optimized for token efficiency
|
||||||
|
- **evaluation** (foundational) - This skill extends the foundational evaluation concepts
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
Internal reference:
|
||||||
|
- LLM-as-Judge Implementation Patterns
|
||||||
|
- Bias Mitigation Techniques
|
||||||
|
- Metric Selection Guide
|
||||||
|
|
||||||
|
External research:
|
||||||
|
- [Eugene Yan: Evaluating the Effectiveness of LLM-Evaluators](https://eugeneyan.com/writing/llm-evaluators/)
|
||||||
|
- [Judging LLM-as-a-Judge (Zheng et al., 2023)](https://arxiv.org/abs/2306.05685)
|
||||||
|
- [G-Eval: NLG Evaluation using GPT-4 (Liu et al., 2023)](https://arxiv.org/abs/2303.16634)
|
||||||
|
- [Large Language Models are not Fair Evaluators (Wang et al., 2023)](https://arxiv.org/abs/2305.17926)
|
||||||
|
|
||||||
|
Related skills in this collection:
|
||||||
|
- evaluation - Foundational evaluation concepts
|
||||||
|
- context-fundamentals - Context structure for evaluation prompts
|
||||||
|
- tool-design - Building evaluation tools
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Skill Metadata
|
||||||
|
|
||||||
|
**Created**: 2024-12-24
|
||||||
|
**Last Updated**: 2024-12-24
|
||||||
|
**Author**: Muratcan Koylan
|
||||||
|
**Version**: 1.0.0
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
name: agent-manager-skill
|
||||||
|
description: "Manage multiple local CLI agents via tmux sessions (start/stop/monitor/assign) with cron-friendly scheduling."
|
||||||
|
risk: unknown
|
||||||
|
source: community
|
||||||
|
date_added: "2026-02-27"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent Manager Skill
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
Use this skill when you need to:
|
||||||
|
|
||||||
|
- run multiple local CLI agents in parallel (separate tmux sessions)
|
||||||
|
- start/stop agents and tail their logs
|
||||||
|
- assign tasks to agents and monitor output
|
||||||
|
- schedule recurring agent work (cron)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Install `agent-manager-skill` in your workspace:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/fractalmind-ai/agent-manager-skill.git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 agent-manager/scripts/main.py doctor
|
||||||
|
python3 agent-manager/scripts/main.py list
|
||||||
|
python3 agent-manager/scripts/main.py start EMP_0001
|
||||||
|
python3 agent-manager/scripts/main.py monitor EMP_0001 --follow
|
||||||
|
python3 agent-manager/scripts/main.py assign EMP_0002 <<'EOF'
|
||||||
|
Follow teams/fractalmind-ai-maintenance.md Workflow
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Requires `tmux` and `python3`.
|
||||||
|
- Agents are configured under an `agents/` directory (see the repo for examples).
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
name: agent-memory-mcp
|
||||||
|
description: "A hybrid memory system that provides persistent, searchable knowledge management for AI agents (Architecture, Patterns, Decisions)."
|
||||||
|
risk: unknown
|
||||||
|
source: community
|
||||||
|
date_added: "2026-02-27"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent Memory Skill
|
||||||
|
|
||||||
|
This skill provides a persistent, searchable memory bank that automatically syncs with project documentation. It runs as an MCP server to allow reading/writing/searching of long-term memories.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js (v18+)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. **Clone the Repository**:
|
||||||
|
Clone the `agentMemory` project into your agent's workspace or a parallel directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/webzler/agentMemory.git .agent/skills/agent-memory
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install Dependencies**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd .agent/skills/agent-memory
|
||||||
|
npm install
|
||||||
|
npm run compile
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start the MCP Server**:
|
||||||
|
Use the helper script to activate the memory bank for your current project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start-server <project_id> <absolute_path_to_target_workspace>
|
||||||
|
```
|
||||||
|
|
||||||
|
_Example for current directory:_
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start-server my-project $(pwd)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Capabilities (MCP Tools)
|
||||||
|
|
||||||
|
### `memory_search`
|
||||||
|
|
||||||
|
Search for memories by query, type, or tags.
|
||||||
|
|
||||||
|
- **Args**: `query` (string), `type?` (string), `tags?` (string[])
|
||||||
|
- **Usage**: "Find all authentication patterns" -> `memory_search({ query: "authentication", type: "pattern" })`
|
||||||
|
|
||||||
|
### `memory_write`
|
||||||
|
|
||||||
|
Record new knowledge or decisions.
|
||||||
|
|
||||||
|
- **Args**: `key` (string), `type` (string), `content` (string), `tags?` (string[])
|
||||||
|
- **Usage**: "Save this architecture decision" -> `memory_write({ key: "auth-v1", type: "decision", content: "..." })`
|
||||||
|
|
||||||
|
### `memory_read`
|
||||||
|
|
||||||
|
Retrieve specific memory content by key.
|
||||||
|
|
||||||
|
- **Args**: `key` (string)
|
||||||
|
- **Usage**: "Get the auth design" -> `memory_read({ key: "auth-v1" })`
|
||||||
|
|
||||||
|
### `memory_stats`
|
||||||
|
|
||||||
|
View analytics on memory usage.
|
||||||
|
|
||||||
|
- **Usage**: "Show memory statistics" -> `memory_stats({})`
|
||||||
|
|
||||||
|
## Dashboard
|
||||||
|
|
||||||
|
This skill includes a standalone dashboard to visualize memory usage.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start-dashboard <absolute_path_to_target_workspace>
|
||||||
|
```
|
||||||
|
|
||||||
|
Access at: `http://localhost:3333`
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
This skill is applicable to execute the workflow or actions described in the overview.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,357 @@
|
|||||||
|
---
|
||||||
|
name: agent-orchestration-improve-agent
|
||||||
|
description: "Systematic improvement of existing agents through performance analysis, prompt engineering, and continuous iteration."
|
||||||
|
risk: unknown
|
||||||
|
source: community
|
||||||
|
date_added: "2026-02-27"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent Performance Optimization Workflow
|
||||||
|
|
||||||
|
Systematic improvement of existing agents through performance analysis, prompt engineering, and continuous iteration.
|
||||||
|
|
||||||
|
[Extended thinking: Agent optimization requires a data-driven approach combining performance metrics, user feedback analysis, and advanced prompt engineering techniques. Success depends on systematic evaluation, targeted improvements, and rigorous testing with rollback capabilities for production safety.]
|
||||||
|
|
||||||
|
## Use this skill when
|
||||||
|
|
||||||
|
- Improving an existing agent's performance or reliability
|
||||||
|
- Analyzing failure modes, prompt quality, or tool usage
|
||||||
|
- Running structured A/B tests or evaluation suites
|
||||||
|
- Designing iterative optimization workflows for agents
|
||||||
|
|
||||||
|
## Do not use this skill when
|
||||||
|
|
||||||
|
- You are building a brand-new agent from scratch
|
||||||
|
- There are no metrics, feedback, or test cases available
|
||||||
|
- The task is unrelated to agent performance or prompt quality
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. Establish baseline metrics and collect representative examples.
|
||||||
|
2. Identify failure modes and prioritize high-impact fixes.
|
||||||
|
3. Apply prompt and workflow improvements with measurable goals.
|
||||||
|
4. Validate with tests and roll out changes in controlled stages.
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
|
||||||
|
- Avoid deploying prompt changes without regression testing.
|
||||||
|
- Roll back quickly if quality or safety metrics regress.
|
||||||
|
|
||||||
|
## Phase 1: Performance Analysis and Baseline Metrics
|
||||||
|
|
||||||
|
Comprehensive analysis of agent performance using context-manager for historical data collection.
|
||||||
|
|
||||||
|
### 1.1 Gather Performance Data
|
||||||
|
|
||||||
|
```
|
||||||
|
Use: context-manager
|
||||||
|
Command: analyze-agent-performance $ARGUMENTS --days 30
|
||||||
|
```
|
||||||
|
|
||||||
|
Collect metrics including:
|
||||||
|
|
||||||
|
- Task completion rate (successful vs failed tasks)
|
||||||
|
- Response accuracy and factual correctness
|
||||||
|
- Tool usage efficiency (correct tools, call frequency)
|
||||||
|
- Average response time and token consumption
|
||||||
|
- User satisfaction indicators (corrections, retries)
|
||||||
|
- Hallucination incidents and error patterns
|
||||||
|
|
||||||
|
### 1.2 User Feedback Pattern Analysis
|
||||||
|
|
||||||
|
Identify recurring patterns in user interactions:
|
||||||
|
|
||||||
|
- **Correction patterns**: Where users consistently modify outputs
|
||||||
|
- **Clarification requests**: Common areas of ambiguity
|
||||||
|
- **Task abandonment**: Points where users give up
|
||||||
|
- **Follow-up questions**: Indicators of incomplete responses
|
||||||
|
- **Positive feedback**: Successful patterns to preserve
|
||||||
|
|
||||||
|
### 1.3 Failure Mode Classification
|
||||||
|
|
||||||
|
Categorize failures by root cause:
|
||||||
|
|
||||||
|
- **Instruction misunderstanding**: Role or task confusion
|
||||||
|
- **Output format errors**: Structure or formatting issues
|
||||||
|
- **Context loss**: Long conversation degradation
|
||||||
|
- **Tool misuse**: Incorrect or inefficient tool selection
|
||||||
|
- **Constraint violations**: Safety or business rule breaches
|
||||||
|
- **Edge case handling**: Unusual input scenarios
|
||||||
|
|
||||||
|
### 1.4 Baseline Performance Report
|
||||||
|
|
||||||
|
Generate quantitative baseline metrics:
|
||||||
|
|
||||||
|
```
|
||||||
|
Performance Baseline:
|
||||||
|
- Task Success Rate: [X%]
|
||||||
|
- Average Corrections per Task: [Y]
|
||||||
|
- Tool Call Efficiency: [Z%]
|
||||||
|
- User Satisfaction Score: [1-10]
|
||||||
|
- Average Response Latency: [Xms]
|
||||||
|
- Token Efficiency Ratio: [X:Y]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 2: Prompt Engineering Improvements
|
||||||
|
|
||||||
|
Apply advanced prompt optimization techniques using prompt-engineer agent.
|
||||||
|
|
||||||
|
### 2.1 Chain-of-Thought Enhancement
|
||||||
|
|
||||||
|
Implement structured reasoning patterns:
|
||||||
|
|
||||||
|
```
|
||||||
|
Use: prompt-engineer
|
||||||
|
Technique: chain-of-thought-optimization
|
||||||
|
```
|
||||||
|
|
||||||
|
- Add explicit reasoning steps: "Let's approach this step-by-step..."
|
||||||
|
- Include self-verification checkpoints: "Before proceeding, verify that..."
|
||||||
|
- Implement recursive decomposition for complex tasks
|
||||||
|
- Add reasoning trace visibility for debugging
|
||||||
|
|
||||||
|
### 2.2 Few-Shot Example Optimization
|
||||||
|
|
||||||
|
Curate high-quality examples from successful interactions:
|
||||||
|
|
||||||
|
- **Select diverse examples** covering common use cases
|
||||||
|
- **Include edge cases** that previously failed
|
||||||
|
- **Show both positive and negative examples** with explanations
|
||||||
|
- **Order examples** from simple to complex
|
||||||
|
- **Annotate examples** with key decision points
|
||||||
|
|
||||||
|
Example structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
Good Example:
|
||||||
|
Input: [User request]
|
||||||
|
Reasoning: [Step-by-step thought process]
|
||||||
|
Output: [Successful response]
|
||||||
|
Why this works: [Key success factors]
|
||||||
|
|
||||||
|
Bad Example:
|
||||||
|
Input: [Similar request]
|
||||||
|
Output: [Failed response]
|
||||||
|
Why this fails: [Specific issues]
|
||||||
|
Correct approach: [Fixed version]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Role Definition Refinement
|
||||||
|
|
||||||
|
Strengthen agent identity and capabilities:
|
||||||
|
|
||||||
|
- **Core purpose**: Clear, single-sentence mission
|
||||||
|
- **Expertise domains**: Specific knowledge areas
|
||||||
|
- **Behavioral traits**: Personality and interaction style
|
||||||
|
- **Tool proficiency**: Available tools and when to use them
|
||||||
|
- **Constraints**: What the agent should NOT do
|
||||||
|
- **Success criteria**: How to measure task completion
|
||||||
|
|
||||||
|
### 2.4 Constitutional AI Integration
|
||||||
|
|
||||||
|
Implement self-correction mechanisms:
|
||||||
|
|
||||||
|
```
|
||||||
|
Constitutional Principles:
|
||||||
|
1. Verify factual accuracy before responding
|
||||||
|
2. Self-check for potential biases or harmful content
|
||||||
|
3. Validate output format matches requirements
|
||||||
|
4. Ensure response completeness
|
||||||
|
5. Maintain consistency with previous responses
|
||||||
|
```
|
||||||
|
|
||||||
|
Add critique-and-revise loops:
|
||||||
|
|
||||||
|
- Initial response generation
|
||||||
|
- Self-critique against principles
|
||||||
|
- Automatic revision if issues detected
|
||||||
|
- Final validation before output
|
||||||
|
|
||||||
|
### 2.5 Output Format Tuning
|
||||||
|
|
||||||
|
Optimize response structure:
|
||||||
|
|
||||||
|
- **Structured templates** for common tasks
|
||||||
|
- **Dynamic formatting** based on complexity
|
||||||
|
- **Progressive disclosure** for detailed information
|
||||||
|
- **Markdown optimization** for readability
|
||||||
|
- **Code block formatting** with syntax highlighting
|
||||||
|
- **Table and list generation** for data presentation
|
||||||
|
|
||||||
|
## Phase 3: Testing and Validation
|
||||||
|
|
||||||
|
Comprehensive testing framework with A/B comparison.
|
||||||
|
|
||||||
|
### 3.1 Test Suite Development
|
||||||
|
|
||||||
|
Create representative test scenarios:
|
||||||
|
|
||||||
|
```
|
||||||
|
Test Categories:
|
||||||
|
1. Golden path scenarios (common successful cases)
|
||||||
|
2. Previously failed tasks (regression testing)
|
||||||
|
3. Edge cases and corner scenarios
|
||||||
|
4. Stress tests (complex, multi-step tasks)
|
||||||
|
5. Adversarial inputs (potential breaking points)
|
||||||
|
6. Cross-domain tasks (combining capabilities)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 A/B Testing Framework
|
||||||
|
|
||||||
|
Compare original vs improved agent:
|
||||||
|
|
||||||
|
```
|
||||||
|
Use: parallel-test-runner
|
||||||
|
Config:
|
||||||
|
- Agent A: Original version
|
||||||
|
- Agent B: Improved version
|
||||||
|
- Test set: 100 representative tasks
|
||||||
|
- Metrics: Success rate, speed, token usage
|
||||||
|
- Evaluation: Blind human review + automated scoring
|
||||||
|
```
|
||||||
|
|
||||||
|
Statistical significance testing:
|
||||||
|
|
||||||
|
- Minimum sample size: 100 tasks per variant
|
||||||
|
- Confidence level: 95% (p < 0.05)
|
||||||
|
- Effect size calculation (Cohen's d)
|
||||||
|
- Power analysis for future tests
|
||||||
|
|
||||||
|
### 3.3 Evaluation Metrics
|
||||||
|
|
||||||
|
Comprehensive scoring framework:
|
||||||
|
|
||||||
|
**Task-Level Metrics:**
|
||||||
|
|
||||||
|
- Completion rate (binary success/failure)
|
||||||
|
- Correctness score (0-100% accuracy)
|
||||||
|
- Efficiency score (steps taken vs optimal)
|
||||||
|
- Tool usage appropriateness
|
||||||
|
- Response relevance and completeness
|
||||||
|
|
||||||
|
**Quality Metrics:**
|
||||||
|
|
||||||
|
- Hallucination rate (factual errors per response)
|
||||||
|
- Consistency score (alignment with previous responses)
|
||||||
|
- Format compliance (matches specified structure)
|
||||||
|
- Safety score (constraint adherence)
|
||||||
|
- User satisfaction prediction
|
||||||
|
|
||||||
|
**Performance Metrics:**
|
||||||
|
|
||||||
|
- Response latency (time to first token)
|
||||||
|
- Total generation time
|
||||||
|
- Token consumption (input + output)
|
||||||
|
- Cost per task (API usage fees)
|
||||||
|
- Memory/context efficiency
|
||||||
|
|
||||||
|
### 3.4 Human Evaluation Protocol
|
||||||
|
|
||||||
|
Structured human review process:
|
||||||
|
|
||||||
|
- Blind evaluation (evaluators don't know version)
|
||||||
|
- Standardized rubric with clear criteria
|
||||||
|
- Multiple evaluators per sample (inter-rater reliability)
|
||||||
|
- Qualitative feedback collection
|
||||||
|
- Preference ranking (A vs B comparison)
|
||||||
|
|
||||||
|
## Phase 4: Version Control and Deployment
|
||||||
|
|
||||||
|
Safe rollout with monitoring and rollback capabilities.
|
||||||
|
|
||||||
|
### 4.1 Version Management
|
||||||
|
|
||||||
|
Systematic versioning strategy:
|
||||||
|
|
||||||
|
```
|
||||||
|
Version Format: agent-name-v[MAJOR].[MINOR].[PATCH]
|
||||||
|
Example: customer-support-v2.3.1
|
||||||
|
|
||||||
|
MAJOR: Significant capability changes
|
||||||
|
MINOR: Prompt improvements, new examples
|
||||||
|
PATCH: Bug fixes, minor adjustments
|
||||||
|
```
|
||||||
|
|
||||||
|
Maintain version history:
|
||||||
|
|
||||||
|
- Git-based prompt storage
|
||||||
|
- Changelog with improvement details
|
||||||
|
- Performance metrics per version
|
||||||
|
- Rollback procedures documented
|
||||||
|
|
||||||
|
### 4.2 Staged Rollout
|
||||||
|
|
||||||
|
Progressive deployment strategy:
|
||||||
|
|
||||||
|
1. **Alpha testing**: Internal team validation (5% traffic)
|
||||||
|
2. **Beta testing**: Selected users (20% traffic)
|
||||||
|
3. **Canary release**: Gradual increase (20% → 50% → 100%)
|
||||||
|
4. **Full deployment**: After success criteria met
|
||||||
|
5. **Monitoring period**: 7-day observation window
|
||||||
|
|
||||||
|
### 4.3 Rollback Procedures
|
||||||
|
|
||||||
|
Quick recovery mechanism:
|
||||||
|
|
||||||
|
```
|
||||||
|
Rollback Triggers:
|
||||||
|
- Success rate drops >10% from baseline
|
||||||
|
- Critical errors increase >5%
|
||||||
|
- User complaints spike
|
||||||
|
- Cost per task increases >20%
|
||||||
|
- Safety violations detected
|
||||||
|
|
||||||
|
Rollback Process:
|
||||||
|
1. Detect issue via monitoring
|
||||||
|
2. Alert team immediately
|
||||||
|
3. Switch to previous stable version
|
||||||
|
4. Analyze root cause
|
||||||
|
5. Fix and re-test before retry
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 Continuous Monitoring
|
||||||
|
|
||||||
|
Real-time performance tracking:
|
||||||
|
|
||||||
|
- Dashboard with key metrics
|
||||||
|
- Anomaly detection alerts
|
||||||
|
- User feedback collection
|
||||||
|
- Automated regression testing
|
||||||
|
- Weekly performance reports
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
Agent improvement is successful when:
|
||||||
|
|
||||||
|
- Task success rate improves by ≥15%
|
||||||
|
- User corrections decrease by ≥25%
|
||||||
|
- No increase in safety violations
|
||||||
|
- Response time remains within 10% of baseline
|
||||||
|
- Cost per task doesn't increase >5%
|
||||||
|
- Positive user feedback increases
|
||||||
|
|
||||||
|
## Post-Deployment Review
|
||||||
|
|
||||||
|
After 30 days of production use:
|
||||||
|
|
||||||
|
1. Analyze accumulated performance data
|
||||||
|
2. Compare against baseline and targets
|
||||||
|
3. Identify new improvement opportunities
|
||||||
|
4. Document lessons learned
|
||||||
|
5. Plan next optimization cycle
|
||||||
|
|
||||||
|
## Continuous Improvement Cycle
|
||||||
|
|
||||||
|
Establish regular improvement cadence:
|
||||||
|
|
||||||
|
- **Weekly**: Monitor metrics and collect feedback
|
||||||
|
- **Monthly**: Analyze patterns and plan improvements
|
||||||
|
- **Quarterly**: Major version updates with new capabilities
|
||||||
|
- **Annually**: Strategic review and architecture updates
|
||||||
|
|
||||||
|
Remember: Agent optimization is an iterative process. Each cycle builds upon previous learnings, gradually improving performance while maintaining stability and safety.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
+247
@@ -0,0 +1,247 @@
|
|||||||
|
---
|
||||||
|
name: agent-orchestration-multi-agent-optimize
|
||||||
|
description: "Optimize multi-agent systems with coordinated profiling, workload distribution, and cost-aware orchestration. Use when improving agent performance, throughput, or reliability."
|
||||||
|
risk: unknown
|
||||||
|
source: community
|
||||||
|
date_added: "2026-02-27"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Multi-Agent Optimization Toolkit
|
||||||
|
|
||||||
|
## Use this skill when
|
||||||
|
|
||||||
|
- Improving multi-agent coordination, throughput, or latency
|
||||||
|
- Profiling agent workflows to identify bottlenecks
|
||||||
|
- Designing orchestration strategies for complex workflows
|
||||||
|
- Optimizing cost, context usage, or tool efficiency
|
||||||
|
|
||||||
|
## Do not use this skill when
|
||||||
|
|
||||||
|
- You only need to tune a single agent prompt
|
||||||
|
- There are no measurable metrics or evaluation data
|
||||||
|
- The task is unrelated to multi-agent orchestration
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. Establish baseline metrics and target performance goals.
|
||||||
|
2. Profile agent workloads and identify coordination bottlenecks.
|
||||||
|
3. Apply orchestration changes and cost controls incrementally.
|
||||||
|
4. Validate improvements with repeatable tests and rollbacks.
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
|
||||||
|
- Avoid deploying orchestration changes without regression testing.
|
||||||
|
- Roll out changes gradually to prevent system-wide regressions.
|
||||||
|
|
||||||
|
## Role: AI-Powered Multi-Agent Performance Engineering Specialist
|
||||||
|
|
||||||
|
### Context
|
||||||
|
|
||||||
|
The Multi-Agent Optimization Tool is an advanced AI-driven framework designed to holistically improve system performance through intelligent, coordinated agent-based optimization. Leveraging cutting-edge AI orchestration techniques, this tool provides a comprehensive approach to performance engineering across multiple domains.
|
||||||
|
|
||||||
|
### Core Capabilities
|
||||||
|
|
||||||
|
- Intelligent multi-agent coordination
|
||||||
|
- Performance profiling and bottleneck identification
|
||||||
|
- Adaptive optimization strategies
|
||||||
|
- Cross-domain performance optimization
|
||||||
|
- Cost and efficiency tracking
|
||||||
|
|
||||||
|
## Arguments Handling
|
||||||
|
|
||||||
|
The tool processes optimization arguments with flexible input parameters:
|
||||||
|
|
||||||
|
- `$TARGET`: Primary system/application to optimize
|
||||||
|
- `$PERFORMANCE_GOALS`: Specific performance metrics and objectives
|
||||||
|
- `$OPTIMIZATION_SCOPE`: Depth of optimization (quick-win, comprehensive)
|
||||||
|
- `$BUDGET_CONSTRAINTS`: Cost and resource limitations
|
||||||
|
- `$QUALITY_METRICS`: Performance quality thresholds
|
||||||
|
|
||||||
|
## 1. Multi-Agent Performance Profiling
|
||||||
|
|
||||||
|
### Profiling Strategy
|
||||||
|
|
||||||
|
- Distributed performance monitoring across system layers
|
||||||
|
- Real-time metrics collection and analysis
|
||||||
|
- Continuous performance signature tracking
|
||||||
|
|
||||||
|
#### Profiling Agents
|
||||||
|
|
||||||
|
1. **Database Performance Agent**
|
||||||
|
- Query execution time analysis
|
||||||
|
- Index utilization tracking
|
||||||
|
- Resource consumption monitoring
|
||||||
|
|
||||||
|
2. **Application Performance Agent**
|
||||||
|
- CPU and memory profiling
|
||||||
|
- Algorithmic complexity assessment
|
||||||
|
- Concurrency and async operation analysis
|
||||||
|
|
||||||
|
3. **Frontend Performance Agent**
|
||||||
|
- Rendering performance metrics
|
||||||
|
- Network request optimization
|
||||||
|
- Core Web Vitals monitoring
|
||||||
|
|
||||||
|
### Profiling Code Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
def multi_agent_profiler(target_system):
|
||||||
|
agents = [
|
||||||
|
DatabasePerformanceAgent(target_system),
|
||||||
|
ApplicationPerformanceAgent(target_system),
|
||||||
|
FrontendPerformanceAgent(target_system)
|
||||||
|
]
|
||||||
|
|
||||||
|
performance_profile = {}
|
||||||
|
for agent in agents:
|
||||||
|
performance_profile[agent.__class__.__name__] = agent.profile()
|
||||||
|
|
||||||
|
return aggregate_performance_metrics(performance_profile)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Context Window Optimization
|
||||||
|
|
||||||
|
### Optimization Techniques
|
||||||
|
|
||||||
|
- Intelligent context compression
|
||||||
|
- Semantic relevance filtering
|
||||||
|
- Dynamic context window resizing
|
||||||
|
- Token budget management
|
||||||
|
|
||||||
|
### Context Compression Algorithm
|
||||||
|
|
||||||
|
```python
|
||||||
|
def compress_context(context, max_tokens=4000):
|
||||||
|
# Semantic compression using embedding-based truncation
|
||||||
|
compressed_context = semantic_truncate(
|
||||||
|
context,
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
importance_threshold=0.7
|
||||||
|
)
|
||||||
|
return compressed_context
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Agent Coordination Efficiency
|
||||||
|
|
||||||
|
### Coordination Principles
|
||||||
|
|
||||||
|
- Parallel execution design
|
||||||
|
- Minimal inter-agent communication overhead
|
||||||
|
- Dynamic workload distribution
|
||||||
|
- Fault-tolerant agent interactions
|
||||||
|
|
||||||
|
### Orchestration Framework
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MultiAgentOrchestrator:
|
||||||
|
def __init__(self, agents):
|
||||||
|
self.agents = agents
|
||||||
|
self.execution_queue = PriorityQueue()
|
||||||
|
self.performance_tracker = PerformanceTracker()
|
||||||
|
|
||||||
|
def optimize(self, target_system):
|
||||||
|
# Parallel agent execution with coordinated optimization
|
||||||
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||||
|
futures = {
|
||||||
|
executor.submit(agent.optimize, target_system): agent
|
||||||
|
for agent in self.agents
|
||||||
|
}
|
||||||
|
|
||||||
|
for future in concurrent.futures.as_completed(futures):
|
||||||
|
agent = futures[future]
|
||||||
|
result = future.result()
|
||||||
|
self.performance_tracker.log(agent, result)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Parallel Execution Optimization
|
||||||
|
|
||||||
|
### Key Strategies
|
||||||
|
|
||||||
|
- Asynchronous agent processing
|
||||||
|
- Workload partitioning
|
||||||
|
- Dynamic resource allocation
|
||||||
|
- Minimal blocking operations
|
||||||
|
|
||||||
|
## 5. Cost Optimization Strategies
|
||||||
|
|
||||||
|
### LLM Cost Management
|
||||||
|
|
||||||
|
- Token usage tracking
|
||||||
|
- Adaptive model selection
|
||||||
|
- Caching and result reuse
|
||||||
|
- Efficient prompt engineering
|
||||||
|
|
||||||
|
### Cost Tracking Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CostOptimizer:
|
||||||
|
def __init__(self):
|
||||||
|
self.token_budget = 100000 # Monthly budget
|
||||||
|
self.token_usage = 0
|
||||||
|
self.model_costs = {
|
||||||
|
'gpt-5': 0.03,
|
||||||
|
'claude-4-sonnet': 0.015,
|
||||||
|
'claude-4-haiku': 0.0025
|
||||||
|
}
|
||||||
|
|
||||||
|
def select_optimal_model(self, complexity):
|
||||||
|
# Dynamic model selection based on task complexity and budget
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Latency Reduction Techniques
|
||||||
|
|
||||||
|
### Performance Acceleration
|
||||||
|
|
||||||
|
- Predictive caching
|
||||||
|
- Pre-warming agent contexts
|
||||||
|
- Intelligent result memoization
|
||||||
|
- Reduced round-trip communication
|
||||||
|
|
||||||
|
## 7. Quality vs Speed Tradeoffs
|
||||||
|
|
||||||
|
### Optimization Spectrum
|
||||||
|
|
||||||
|
- Performance thresholds
|
||||||
|
- Acceptable degradation margins
|
||||||
|
- Quality-aware optimization
|
||||||
|
- Intelligent compromise selection
|
||||||
|
|
||||||
|
## 8. Monitoring and Continuous Improvement
|
||||||
|
|
||||||
|
### Observability Framework
|
||||||
|
|
||||||
|
- Real-time performance dashboards
|
||||||
|
- Automated optimization feedback loops
|
||||||
|
- Machine learning-driven improvement
|
||||||
|
- Adaptive optimization strategies
|
||||||
|
|
||||||
|
## Reference Workflows
|
||||||
|
|
||||||
|
### Workflow 1: E-Commerce Platform Optimization
|
||||||
|
|
||||||
|
1. Initial performance profiling
|
||||||
|
2. Agent-based optimization
|
||||||
|
3. Cost and performance tracking
|
||||||
|
4. Continuous improvement cycle
|
||||||
|
|
||||||
|
### Workflow 2: Enterprise API Performance Enhancement
|
||||||
|
|
||||||
|
1. Comprehensive system analysis
|
||||||
|
2. Multi-layered agent optimization
|
||||||
|
3. Iterative performance refinement
|
||||||
|
4. Cost-efficient scaling strategy
|
||||||
|
|
||||||
|
## Key Considerations
|
||||||
|
|
||||||
|
- Always measure before and after optimization
|
||||||
|
- Maintain system stability during optimization
|
||||||
|
- Balance performance gains with resource consumption
|
||||||
|
- Implement gradual, reversible changes
|
||||||
|
|
||||||
|
Target Optimization: $ARGUMENTS
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
---
|
||||||
|
name: agent-orchestrator
|
||||||
|
description: Meta-skill que orquestra todos os agentes do ecossistema. Scan automatico de skills, match por capacidades, coordenacao de workflows multi-skill e registry management.
|
||||||
|
risk: safe
|
||||||
|
source: community
|
||||||
|
date_added: '2026-03-06'
|
||||||
|
author: renat
|
||||||
|
tags:
|
||||||
|
- orchestration
|
||||||
|
- multi-agent
|
||||||
|
- workflow
|
||||||
|
- automation
|
||||||
|
tools:
|
||||||
|
- claude-code
|
||||||
|
- antigravity
|
||||||
|
- cursor
|
||||||
|
- gemini-cli
|
||||||
|
- codex-cli
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent Orchestrator
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Meta-skill que orquestra todos os agentes do ecossistema. Scan automatico de skills, match por capacidades, coordenacao de workflows multi-skill e registry management.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
- When you need specialized assistance with this domain
|
||||||
|
|
||||||
|
## Do Not Use This Skill When
|
||||||
|
|
||||||
|
- The task is unrelated to agent orchestrator
|
||||||
|
- A simpler, more specific tool can handle the request
|
||||||
|
- The user needs general-purpose assistance without domain expertise
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
Meta-skill que funciona como camada central de decisao e coordenacao para todo
|
||||||
|
o ecossistema de skills. Faz varredura automatica, identifica agentes relevantes
|
||||||
|
e orquestra multiplos skills para tarefas complexas.
|
||||||
|
|
||||||
|
## Principio: Zero Intervencao Manual
|
||||||
|
|
||||||
|
- **SEMPRE faz varredura** antes de processar qualquer solicitacao
|
||||||
|
- Novas skills sao **auto-detectadas e incluidas** ao criar SKILL.md em qualquer subpasta
|
||||||
|
- Skills removidas sao **auto-excluidas** do registry
|
||||||
|
- Nenhum comando manual e necessario para registrar novas skills
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Obrigatorio (Toda Solicitacao)
|
||||||
|
|
||||||
|
Execute estes passos ANTES de processar qualquer request do usuario.
|
||||||
|
Os scripts usam paths relativos automaticamente - funciona de qualquer diretorio.
|
||||||
|
|
||||||
|
## Passo 1: Auto-Discovery (Varredura)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python agent-orchestrator/scripts/scan_registry.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Ultra-rapido (<100ms) via cache de hashes MD5. So re-processa arquivos alterados.
|
||||||
|
Retorna JSON com resumo de todos os skills encontrados.
|
||||||
|
|
||||||
|
## Passo 2: Match De Skills
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python agent-orchestrator/scripts/match_skills.py "<solicitacao do usuario>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Retorna JSON com skills ranqueadas por relevancia. Interpretar o resultado:
|
||||||
|
|
||||||
|
| Resultado | Acao |
|
||||||
|
|:-----------------------|:--------------------------------------------------------|
|
||||||
|
| `matched: 0` | Nenhum skill relevante. Operar normalmente sem skills. |
|
||||||
|
| `matched: 1` | Um skill relevante. Carregar seu SKILL.md e seguir. |
|
||||||
|
| `matched: 2+` | Multiplos skills. Executar Passo 3 (orquestracao). |
|
||||||
|
|
||||||
|
## Passo 3: Orquestracao (Se Matched >= 2)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python agent-orchestrator/scripts/orchestrate.py --skills skill1,skill2 --query "<solicitacao>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Retorna plano de execucao com padrao, ordem dos steps e data flow entre skills.
|
||||||
|
|
||||||
|
## Passo Rapido (Atalho)
|
||||||
|
|
||||||
|
Para queries simples, os passos 1+2 podem ser combinados em sequencia:
|
||||||
|
```bash
|
||||||
|
python agent-orchestrator/scripts/scan_registry.py && python agent-orchestrator/scripts/match_skills.py "<solicitacao>"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Skill Registry
|
||||||
|
|
||||||
|
O registry vive em:
|
||||||
|
```
|
||||||
|
agent-orchestrator/data/registry.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Locais De Busca
|
||||||
|
|
||||||
|
O scanner procura SKILL.md em:
|
||||||
|
1. `.claude/skills/*/` (skills registradas no Claude Code)
|
||||||
|
2. `*/` (skills standalone no top-level)
|
||||||
|
3. `*/*\` (skills em subpastas, ate profundidade 3)
|
||||||
|
|
||||||
|
## Metadata Por Skill
|
||||||
|
|
||||||
|
Cada entrada no registry contem:
|
||||||
|
|
||||||
|
| Campo | Descricao |
|
||||||
|
|:---------------|:---------------------------------------------------|
|
||||||
|
| name | Nome da skill (do frontmatter YAML) |
|
||||||
|
| description | Descricao completa (triggers inclusos) |
|
||||||
|
| location | Caminho absoluto do diretorio |
|
||||||
|
| skill_md | Caminho absoluto do SKILL.md |
|
||||||
|
| registered | Se esta em .claude/skills/ (true/false) |
|
||||||
|
| capabilities | Tags de capacidade (auto-extraidas + explicitas) |
|
||||||
|
| triggers | Keywords de ativacao extraidas da description |
|
||||||
|
| language | Linguagem principal (python/nodejs/bash/none) |
|
||||||
|
| status | active / incomplete / missing |
|
||||||
|
|
||||||
|
## Comandos Do Registry
|
||||||
|
|
||||||
|
```bash
|
||||||
|
|
||||||
|
## Scan Rapido (Usa Cache De Hashes)
|
||||||
|
|
||||||
|
python agent-orchestrator/scripts/scan_registry.py
|
||||||
|
|
||||||
|
## Tabela De Status Detalhada
|
||||||
|
|
||||||
|
python agent-orchestrator/scripts/scan_registry.py --status
|
||||||
|
|
||||||
|
## Re-Scan Completo (Ignora Cache)
|
||||||
|
|
||||||
|
python agent-orchestrator/scripts/scan_registry.py --force
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Algoritmo De Matching
|
||||||
|
|
||||||
|
Para cada solicitacao, o matcher pontua skills usando:
|
||||||
|
|
||||||
|
| Criterio | Pontos | Exemplo |
|
||||||
|
|:-----------------------------|:-------|:--------------------------------------|
|
||||||
|
| Nome do skill na query | +15 | "use web-scraper" -> web-scraper |
|
||||||
|
| Keyword trigger exata | +10 | "scrape" -> web-scraper |
|
||||||
|
| Categoria de capacidade | +5 | data-extraction -> web-scraper |
|
||||||
|
| Sobreposicao de palavras | +1 | Palavras da query na description |
|
||||||
|
| Boost de projeto | +20 | Skill atribuida ao projeto ativo |
|
||||||
|
|
||||||
|
Threshold minimo: 5 pontos. Skills abaixo disso sao ignoradas.
|
||||||
|
|
||||||
|
## Match Com Projeto
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python agent-orchestrator/scripts/match_skills.py --project meu-projeto "query aqui"
|
||||||
|
```
|
||||||
|
|
||||||
|
Skills atribuidas ao projeto recebem +20 de boost automatico.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Padroes De Orquestracao
|
||||||
|
|
||||||
|
Quando multiplos skills sao relevantes, o orchestrator classifica o padrao:
|
||||||
|
|
||||||
|
## 1. Pipeline Sequencial
|
||||||
|
|
||||||
|
Skills formam uma cadeia onde o output de uma alimenta a proxima.
|
||||||
|
|
||||||
|
**Quando:** Mix de skills "produtoras" (data-extraction, government-data) e "consumidoras" (messaging, social-media).
|
||||||
|
|
||||||
|
**Exemplo:** web-scraper coleta precos -> whatsapp-cloud-api envia alerta
|
||||||
|
|
||||||
|
```
|
||||||
|
user_query -> web-scraper -> whatsapp-cloud-api -> result
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Execucao Paralela
|
||||||
|
|
||||||
|
Skills trabalham independentemente em aspectos diferentes da solicitacao.
|
||||||
|
|
||||||
|
**Quando:** Todas as skills tem o mesmo papel (todas produtoras ou todas consumidoras).
|
||||||
|
|
||||||
|
**Exemplo:** instagram publica post + whatsapp envia notificacao (ambos recebem o mesmo conteudo)
|
||||||
|
|
||||||
|
```
|
||||||
|
user_query -> [instagram, whatsapp-cloud-api] -> aggregated_result
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Primario + Suporte
|
||||||
|
|
||||||
|
Uma skill principal lidera; outras fornecem dados de apoio.
|
||||||
|
|
||||||
|
**Quando:** Uma skill tem score muito superior as demais (>= 2x).
|
||||||
|
|
||||||
|
**Exemplo:** whatsapp-cloud-api envia mensagem (primario) + web-scraper fornece dados (suporte)
|
||||||
|
|
||||||
|
```
|
||||||
|
user_query -> whatsapp-cloud-api (primary) + web-scraper (support) -> result
|
||||||
|
```
|
||||||
|
|
||||||
|
## Detalhes Em `References/Orchestration-Patterns.Md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gerenciamento De Projetos
|
||||||
|
|
||||||
|
Atribuir skills a projetos permite boost de relevancia e contexto persistente.
|
||||||
|
|
||||||
|
## Arquivo De Projetos
|
||||||
|
|
||||||
|
```
|
||||||
|
agent-orchestrator/data/projects.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Operacoes
|
||||||
|
|
||||||
|
**Criar projeto:**
|
||||||
|
Adicionar entrada ao projects.json:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "nome-do-projeto",
|
||||||
|
"created_at": "2026-02-25T12:00:00",
|
||||||
|
"skills": ["web-scraper", "whatsapp-cloud-api"],
|
||||||
|
"description": "Descricao do projeto"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Adicionar skill a projeto:** Atualizar o array `skills` do projeto.
|
||||||
|
|
||||||
|
**Remover skill de projeto:** Remover do array `skills`.
|
||||||
|
|
||||||
|
**Consultar skills do projeto:** Ler o projects.json e listar skills atribuidas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adicionando Novas Skills
|
||||||
|
|
||||||
|
Para adicionar uma nova skill ao ecossistema:
|
||||||
|
|
||||||
|
1. Criar uma pasta em qualquer lugar sob `skills root:`
|
||||||
|
2. Criar um `SKILL.md` com frontmatter YAML:
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
name: minha-nova-skill
|
||||||
|
description: "Descricao com keywords de ativacao..."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentacao Da Skill
|
||||||
|
|
||||||
|
```
|
||||||
|
3. **Pronto!** O auto-discovery detecta automaticamente na proxima solicitacao.
|
||||||
|
|
||||||
|
Opcionalmente, para discovery nativo do Claude Code:
|
||||||
|
4. Copiar o SKILL.md para `.claude/skills/<nome>/SKILL.md`
|
||||||
|
|
||||||
|
## Tags De Capacidade Explicitas (Opcional)
|
||||||
|
|
||||||
|
Adicionar ao frontmatter para matching mais preciso:
|
||||||
|
```yaml
|
||||||
|
capabilities: [data-extraction, web-automation]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ver Status De Todos Os Skills
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python agent-orchestrator/scripts/scan_registry.py --status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interpretar Status
|
||||||
|
|
||||||
|
| Status | Significado |
|
||||||
|
|:-----------|:---------------------------------------------------|
|
||||||
|
| active | SKILL.md com name + description presentes |
|
||||||
|
| incomplete | SKILL.md existe mas falta name ou description |
|
||||||
|
| missing | Diretorio existe mas sem SKILL.md |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Skills Atuais Do Ecossistema
|
||||||
|
|
||||||
|
| Skill | Capacidades | Status |
|
||||||
|
|:-------------------|:--------------------------------------|:--------|
|
||||||
|
| web-scraper | data-extraction, web-automation | active |
|
||||||
|
| junta-leiloeiros | government-data, data-extraction | active |
|
||||||
|
| whatsapp-cloud-api | messaging, api-integration | active |
|
||||||
|
| instagram | social-media, api-integration | partial |
|
||||||
|
|
||||||
|
*Esta tabela e atualizada automaticamente via `scan_registry.py --status`.*
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- Provide clear, specific context about your project and requirements
|
||||||
|
- Review all suggestions before applying them to production code
|
||||||
|
- Combine with other complementary skills for comprehensive analysis
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
- Using this skill for tasks outside its domain expertise
|
||||||
|
- Applying recommendations without understanding your specific context
|
||||||
|
- Not providing enough project context for accurate analysis
|
||||||
|
|
||||||
|
## Related Skills
|
||||||
|
|
||||||
|
- `multi-advisor` - Complementary skill for enhanced analysis
|
||||||
|
- `task-intelligence` - Complementary skill for enhanced analysis
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
# Taxonomia de Capacidades (Capability Tags)
|
||||||
|
|
||||||
|
Categorias padrao para classificar skills no ecossistema.
|
||||||
|
Cada skill pode ter multiplas categorias.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Categorias
|
||||||
|
|
||||||
|
### data-extraction
|
||||||
|
**Descricao:** Coleta e extracao de dados de fontes web ou APIs.
|
||||||
|
**Keywords PT:** raspar, extrair, coletar, dados, tabela
|
||||||
|
**Keywords EN:** scrape, extract, crawl, parse, harvest, collect, data, table, csv
|
||||||
|
**Skills atuais:** web-scraper, junta-leiloeiros
|
||||||
|
|
||||||
|
### messaging
|
||||||
|
**Descricao:** Envio e recebimento de mensagens via plataformas de comunicacao.
|
||||||
|
**Keywords PT:** mensagem, enviar, notificacao, atendimento, comunicar, avisar
|
||||||
|
**Keywords EN:** whatsapp, message, send, chat, notify, notification, sms
|
||||||
|
**Skills atuais:** whatsapp-cloud-api
|
||||||
|
|
||||||
|
### social-media
|
||||||
|
**Descricao:** Interacao com plataformas de redes sociais (posts, stories, analytics).
|
||||||
|
**Keywords PT:** publicar, rede social, engajamento, post, stories
|
||||||
|
**Keywords EN:** instagram, facebook, twitter, post, stories, reels, social, feed, follower
|
||||||
|
**Skills atuais:** instagram
|
||||||
|
|
||||||
|
### government-data
|
||||||
|
**Descricao:** Coleta de dados governamentais, registros publicos, orgaos oficiais.
|
||||||
|
**Keywords PT:** junta, leiloeiro, cadastro, governo, comercial, tribunal, certidao, registro
|
||||||
|
**Keywords EN:** government, registry, official, court, public records
|
||||||
|
**Skills atuais:** junta-leiloeiros
|
||||||
|
|
||||||
|
### web-automation
|
||||||
|
**Descricao:** Automacao de navegador, preenchimento de formularios, interacao com paginas.
|
||||||
|
**Keywords PT:** navegador, automatizar, automacao, preencher
|
||||||
|
**Keywords EN:** browser, selenium, playwright, automate, click, fill form
|
||||||
|
**Skills atuais:** web-scraper
|
||||||
|
|
||||||
|
### api-integration
|
||||||
|
**Descricao:** Integracao com APIs externas, webhooks, autenticacao OAuth.
|
||||||
|
**Keywords PT:** integracao, integrar, conectar, api, webhook
|
||||||
|
**Keywords EN:** api, endpoint, webhook, rest, graph, oauth, token
|
||||||
|
**Skills atuais:** whatsapp-cloud-api, instagram
|
||||||
|
|
||||||
|
### analytics
|
||||||
|
**Descricao:** Analise de dados, metricas, dashboards, relatorios.
|
||||||
|
**Keywords PT:** relatorio, metricas, analise, estatistica
|
||||||
|
**Keywords EN:** insight, analytics, metrics, dashboard, report, stats
|
||||||
|
**Skills atuais:** (nenhuma dedicada ainda)
|
||||||
|
|
||||||
|
### content-management
|
||||||
|
**Descricao:** Publicacao, agendamento e gestao de conteudo em plataformas.
|
||||||
|
**Keywords PT:** publicar, agendar, conteudo, midia, template
|
||||||
|
**Keywords EN:** publish, schedule, template, content, media, upload
|
||||||
|
**Skills atuais:** instagram
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Roles (Papeis)
|
||||||
|
|
||||||
|
As categorias se agrupam em papeis para orquestracao:
|
||||||
|
|
||||||
|
| Papel | Categorias | Descricao |
|
||||||
|
|:-----------|:------------------------------------------------|:---------------------------------|
|
||||||
|
| Producer | data-extraction, government-data, analytics | Gera/coleta dados |
|
||||||
|
| Consumer | messaging, social-media, content-management | Atua sobre dados (envia, publica)|
|
||||||
|
| Hybrid | api-integration, web-automation | Pode produzir e consumir dados |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Como Declarar no SKILL.md
|
||||||
|
|
||||||
|
Adicionar campo `capabilities` ao frontmatter YAML:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
name: minha-skill
|
||||||
|
description: "..."
|
||||||
|
capabilities: [data-extraction, web-automation]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
Se omitido, o scanner extrai automaticamente da `description` via keywords.
|
||||||
|
Tags explicitas tem prioridade e nao sao duplicadas com as auto-extraidas.
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
# Padroes de Orquestracao Multi-Skill
|
||||||
|
|
||||||
|
Guia detalhado para coordenar multiplos skills em workflows complexos.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Pipeline Sequencial
|
||||||
|
|
||||||
|
Output de um skill alimenta o input do proximo.
|
||||||
|
|
||||||
|
### Quando Usar
|
||||||
|
- Mix de skills "produtoras" (data-extraction, government-data, analytics) e "consumidoras" (messaging, social-media, content-management)
|
||||||
|
- A tarefa tem etapas distintas: coletar -> processar -> entregar
|
||||||
|
|
||||||
|
### Fluxo
|
||||||
|
```
|
||||||
|
user_query -> Skill A (produtora) -> dados -> Skill B (consumidora) -> resultado
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exemplo Concreto
|
||||||
|
**Solicitacao:** "Coletar precos de leiloeiros de SP e enviar por WhatsApp"
|
||||||
|
```
|
||||||
|
1. junta-leiloeiros: Executar scraper para SP, exportar dados
|
||||||
|
2. whatsapp-cloud-api: Formatar dados como mensagem e enviar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Regras de Contexto
|
||||||
|
- O output de cada step deve ser passado como contexto para o proximo
|
||||||
|
- Formatos comuns de passagem: JSON, tabela Markdown, texto resumido
|
||||||
|
- Se um step falhar, interromper o pipeline e reportar ao usuario
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Execucao Paralela
|
||||||
|
|
||||||
|
Skills trabalham independentemente em aspectos diferentes.
|
||||||
|
|
||||||
|
### Quando Usar
|
||||||
|
- Todas as skills tem o mesmo papel (todas produtoras OU todas consumidoras)
|
||||||
|
- Os aspectos da tarefa sao independentes entre si
|
||||||
|
- Nao ha dependencia de dados entre skills
|
||||||
|
|
||||||
|
### Fluxo
|
||||||
|
```
|
||||||
|
┌─> Skill A ─> output A ─┐
|
||||||
|
user_query ──>├─> Skill B ─> output B ─├──> resultado agregado
|
||||||
|
└─> Skill C ─> output C ─┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exemplo Concreto
|
||||||
|
**Solicitacao:** "Publicar a promocao no Instagram e enviar por WhatsApp"
|
||||||
|
```
|
||||||
|
1. (paralelo) instagram: Criar e publicar post da promocao
|
||||||
|
1. (paralelo) whatsapp-cloud-api: Enviar mensagem da promocao
|
||||||
|
-> Agregar: reportar status de ambas as publicacoes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Regras de Contexto
|
||||||
|
- Cada skill recebe a query original completa
|
||||||
|
- Os outputs sao agregados em uma resposta unificada
|
||||||
|
- Se um skill falhar, os outros continuam normalmente
|
||||||
|
- Reportar sucesso/falha de cada skill individualmente
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Primario + Suporte
|
||||||
|
|
||||||
|
Uma skill principal lidera; outras fornecem dados de apoio.
|
||||||
|
|
||||||
|
### Quando Usar
|
||||||
|
- Uma skill tem score de relevancia muito superior (>= 2x a proxima)
|
||||||
|
- A tarefa principal e clara, mas pode se beneficiar de dados adicionais
|
||||||
|
- Skills de suporte sao opcionais / "nice to have"
|
||||||
|
|
||||||
|
### Fluxo
|
||||||
|
```
|
||||||
|
user_query -> Skill A (primaria) ──────────────> resultado
|
||||||
|
↑
|
||||||
|
Skill B (suporte) ─> dados extras
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exemplo Concreto
|
||||||
|
**Solicitacao:** "Configurar chatbot WhatsApp para responder com dados de leiloeiros"
|
||||||
|
```
|
||||||
|
1. (primaria) whatsapp-cloud-api: Configurar webhook e logica do chatbot
|
||||||
|
2. (suporte) junta-leiloeiros: Fornecer endpoint/dados para o chatbot consultar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Regras de Contexto
|
||||||
|
- A skill primaria conduz o workflow
|
||||||
|
- Skills de suporte sao consultadas sob demanda
|
||||||
|
- Se skill de suporte falhar, a primaria deve continuar (graceful degradation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tratamento de Erros
|
||||||
|
|
||||||
|
### Regras Gerais
|
||||||
|
1. **Falha em skill individual**: Reportar ao usuario qual skill falhou e por que
|
||||||
|
2. **Falha em pipeline**: Interromper e mostrar ate onde chegou
|
||||||
|
3. **Falha parcial em paralelo**: Continuar com as demais, reportar falha(s)
|
||||||
|
4. **Skill incomplete**: Avisar que a skill esta com status incompleto antes de tentar usa-la
|
||||||
|
|
||||||
|
### Fallback
|
||||||
|
- Se uma skill falha, verificar se outra skill tem capacidade similar
|
||||||
|
- Se nao houver alternativa, operar sem a skill e informar o usuario
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Serializacao de Contexto
|
||||||
|
|
||||||
|
Formato padrao para passar dados entre skills:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"source_skill": "web-scraper",
|
||||||
|
"target_skill": "whatsapp-cloud-api",
|
||||||
|
"data_type": "table",
|
||||||
|
"data": [
|
||||||
|
{"nome": "Joao Silva", "uf": "SP", "registro": "12345"},
|
||||||
|
{"nome": "Maria Santos", "uf": "RJ", "registro": "67890"}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"total_items": 2,
|
||||||
|
"collected_at": "2026-02-25T12:00:00",
|
||||||
|
"query": "leiloeiros de SP e RJ"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Skill Matching Algorithm for Agent Orchestrator.
|
||||||
|
|
||||||
|
Scores and ranks skills against a user query to determine
|
||||||
|
which agents are relevant for the current request.
|
||||||
|
|
||||||
|
Scoring:
|
||||||
|
- Skill name appears in query: +15
|
||||||
|
- Exact trigger keyword match: +10 per keyword
|
||||||
|
- Capability category match: +5 per category
|
||||||
|
- Description word overlap: +1 per word
|
||||||
|
- Project assignment boost: +20 if skill is assigned to active project
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python match_skills.py "raspar dados de um site"
|
||||||
|
python match_skills.py "coletar precos e enviar por whatsapp"
|
||||||
|
python match_skills.py --project myproject "query here"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# ── Configuration ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Resolve paths relative to this script's location
|
||||||
|
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
ORCHESTRATOR_DIR = _SCRIPT_DIR.parent
|
||||||
|
SKILLS_ROOT = ORCHESTRATOR_DIR.parent
|
||||||
|
DATA_DIR = ORCHESTRATOR_DIR / "data"
|
||||||
|
REGISTRY_PATH = DATA_DIR / "registry.json"
|
||||||
|
PROJECTS_PATH = DATA_DIR / "projects.json"
|
||||||
|
SCAN_SCRIPT = _SCRIPT_DIR / "scan_registry.py"
|
||||||
|
|
||||||
|
# Capability keywords for query -> category matching (PT + EN)
|
||||||
|
CAPABILITY_KEYWORDS = {
|
||||||
|
"data-extraction": [
|
||||||
|
"scrape", "extract", "crawl", "parse", "harvest", "collect", "data",
|
||||||
|
"raspar", "extrair", "coletar", "dados", "tabela", "table", "csv",
|
||||||
|
"web data", "pull info", "get data",
|
||||||
|
],
|
||||||
|
"messaging": [
|
||||||
|
"whatsapp", "message", "send", "chat", "notify", "notification", "sms",
|
||||||
|
"mensagem", "enviar", "notificar", "notificacao", "atendimento",
|
||||||
|
"comunicar", "avisar",
|
||||||
|
],
|
||||||
|
"social-media": [
|
||||||
|
"instagram", "facebook", "twitter", "post", "stories", "reels",
|
||||||
|
"social", "feed", "follower", "publicar", "rede social", "engajamento",
|
||||||
|
],
|
||||||
|
"government-data": [
|
||||||
|
"junta", "leiloeiro", "cadastro", "governo", "comercial", "tribunal",
|
||||||
|
"diario oficial", "certidao", "registro", "uf", "estado",
|
||||||
|
],
|
||||||
|
"web-automation": [
|
||||||
|
"browser", "selenium", "playwright", "automate", "click", "fill form",
|
||||||
|
"navegador", "automatizar", "automacao", "preencher",
|
||||||
|
],
|
||||||
|
"api-integration": [
|
||||||
|
"api", "endpoint", "webhook", "rest", "graph", "oauth", "token",
|
||||||
|
"integracao", "integrar", "conectar",
|
||||||
|
],
|
||||||
|
"analytics": [
|
||||||
|
"insight", "analytics", "metrics", "dashboard", "report", "stats",
|
||||||
|
"relatorio", "metricas", "analise", "estatistica",
|
||||||
|
],
|
||||||
|
"content-management": [
|
||||||
|
"publish", "schedule", "template", "content", "media", "upload",
|
||||||
|
"publicar", "agendar", "conteudo", "midia",
|
||||||
|
],
|
||||||
|
"legal": [
|
||||||
|
"advogado", "direito", "juridico", "lei", "processo",
|
||||||
|
"acao", "peticao", "recurso", "sentenca", "juiz",
|
||||||
|
"divorcio", "guarda", "alimentos", "pensao", "alimenticia", "inventario", "heranca", "partilha",
|
||||||
|
"acidente de trabalho", "acidente",
|
||||||
|
"familia", "criminal", "penal", "crime", "feminicidio", "maria da penha",
|
||||||
|
"violencia domestica", "medida protetiva", "stalking",
|
||||||
|
"danos morais", "responsabilidade civil", "indenizacao", "dano",
|
||||||
|
"consumidor", "cdc", "plano de saude",
|
||||||
|
"trabalhista", "clt", "rescisao", "fgts", "horas extras",
|
||||||
|
"previdenciario", "aposentadoria", "aposentar", "inss",
|
||||||
|
"imobiliario", "usucapiao", "despejo", "inquilinato",
|
||||||
|
"alienacao fiduciaria", "bem de familia",
|
||||||
|
"tributario", "imposto", "icms", "execucao fiscal",
|
||||||
|
"administrativo", "licitacao", "improbidade", "mandado de seguranca",
|
||||||
|
"empresarial", "societario", "falencia", "recuperacao judicial",
|
||||||
|
"empresa", "ltda", "cnpj", "mei", "eireli", "contrato social",
|
||||||
|
"contrato", "clausula", "contestacao", "apelacao", "agravo",
|
||||||
|
"habeas corpus", "mandado", "liminar", "tutela",
|
||||||
|
"cpc", "stj", "stf", "sumula", "jurisprudencia",
|
||||||
|
"oab", "honorarios", "custas",
|
||||||
|
],
|
||||||
|
"auction": [
|
||||||
|
"leilao", "leilao judicial", "leilao extrajudicial", "hasta publica",
|
||||||
|
"arrematacao", "arrematar", "arrematante", "lance", "desagio",
|
||||||
|
"edital leilao", "penhora", "adjudicacao", "praca",
|
||||||
|
"imissao na posse", "carta arrematacao", "vil preco",
|
||||||
|
"avaliacao imovel", "laudo", "perito", "matricula",
|
||||||
|
"leiloeiro", "comissao leiloeiro",
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
"seguranca", "security", "owasp", "vulnerability", "incident",
|
||||||
|
"pentest", "firewall", "malware", "phishing", "cve",
|
||||||
|
"autenticacao", "criptografia", "encryption",
|
||||||
|
],
|
||||||
|
"image-generation": [
|
||||||
|
"imagem", "image", "gerar imagem", "generate image",
|
||||||
|
"stable diffusion", "comfyui", "midjourney", "dall-e",
|
||||||
|
"foto", "ilustracao", "arte", "design",
|
||||||
|
],
|
||||||
|
"monitoring": [
|
||||||
|
"monitor", "monitorar", "health", "status",
|
||||||
|
"audit", "auditoria", "sentinel", "check",
|
||||||
|
],
|
||||||
|
"context-management": [
|
||||||
|
"contexto", "context", "sessao", "session", "compactacao", "compaction",
|
||||||
|
"comprimir", "compress", "snapshot", "checkpoint", "briefing",
|
||||||
|
"continuidade", "continuity", "preservar", "preserve",
|
||||||
|
"memoria", "memory", "resumo", "summary",
|
||||||
|
"salvar estado", "save state", "context window", "janela de contexto",
|
||||||
|
"perda de dados", "data loss", "backup",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Functions ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def ensure_registry():
|
||||||
|
"""Run scan if registry doesn't exist."""
|
||||||
|
if not REGISTRY_PATH.exists():
|
||||||
|
subprocess.run(
|
||||||
|
[sys.executable, str(SCAN_SCRIPT)],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_registry() -> list[dict]:
|
||||||
|
"""Load skills from registry.json."""
|
||||||
|
ensure_registry()
|
||||||
|
if not REGISTRY_PATH.exists():
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
data = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
|
||||||
|
return data.get("skills", [])
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def load_projects() -> dict:
|
||||||
|
"""Load project assignments."""
|
||||||
|
if not PROJECTS_PATH.exists():
|
||||||
|
return {"projects": []}
|
||||||
|
try:
|
||||||
|
return json.loads(PROJECTS_PATH.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return {"projects": []}
|
||||||
|
|
||||||
|
|
||||||
|
def get_project_skills(project_name: str) -> set:
|
||||||
|
"""Get set of skill names assigned to a project."""
|
||||||
|
projects = load_projects()
|
||||||
|
for p in projects.get("projects", []):
|
||||||
|
if p.get("name", "").lower() == project_name.lower():
|
||||||
|
return set(p.get("skills", []))
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
|
def query_to_capabilities(query: str) -> list[str]:
|
||||||
|
"""Map a query to capability categories using word boundary matching."""
|
||||||
|
q_lower = query.lower()
|
||||||
|
q_words = set(re.findall(r'[a-zA-ZÀ-ÿ]+', q_lower))
|
||||||
|
caps = []
|
||||||
|
for cap, keywords in CAPABILITY_KEYWORDS.items():
|
||||||
|
for kw in keywords:
|
||||||
|
# Multi-word keywords: substring match. Single-word: exact word match.
|
||||||
|
if " " in kw:
|
||||||
|
if kw in q_lower:
|
||||||
|
caps.append(cap)
|
||||||
|
break
|
||||||
|
elif kw in q_words:
|
||||||
|
caps.append(cap)
|
||||||
|
break
|
||||||
|
return caps
|
||||||
|
|
||||||
|
|
||||||
|
def normalize(text: str) -> set[str]:
|
||||||
|
"""Normalize text to a set of lowercase words."""
|
||||||
|
return set(re.findall(r'[a-zA-ZÀ-ÿ]{3,}', text.lower()))
|
||||||
|
|
||||||
|
|
||||||
|
def score_skill(skill: dict, query: str, project_skills: set = None) -> dict:
|
||||||
|
"""
|
||||||
|
Score a skill's relevance to a query.
|
||||||
|
|
||||||
|
Returns dict with score, reasons, and skill info.
|
||||||
|
"""
|
||||||
|
q_lower = query.lower()
|
||||||
|
score = 0
|
||||||
|
reasons = []
|
||||||
|
|
||||||
|
name = skill.get("name", "")
|
||||||
|
description = skill.get("description", "")
|
||||||
|
triggers = skill.get("triggers", [])
|
||||||
|
capabilities = skill.get("capabilities", [])
|
||||||
|
|
||||||
|
# 1. Skill name in query (+15)
|
||||||
|
if name.lower() in q_lower or name.lower().replace("-", " ") in q_lower:
|
||||||
|
score += 15
|
||||||
|
reasons.append(f"name:{name}")
|
||||||
|
|
||||||
|
# 2. Trigger keyword matches (+10 each) - word boundary matching
|
||||||
|
q_words = set(re.findall(r'[a-zA-ZÀ-ÿ]+', q_lower))
|
||||||
|
for trigger in triggers:
|
||||||
|
trigger_lower = trigger.lower()
|
||||||
|
# Multi-word triggers: substring match. Single-word: exact word match.
|
||||||
|
if " " in trigger_lower:
|
||||||
|
if trigger_lower in q_lower:
|
||||||
|
score += 10
|
||||||
|
reasons.append(f"trigger:{trigger}")
|
||||||
|
elif trigger_lower in q_words:
|
||||||
|
score += 10
|
||||||
|
reasons.append(f"trigger:{trigger}")
|
||||||
|
|
||||||
|
# 3. Capability category match (+5 each)
|
||||||
|
query_caps = query_to_capabilities(query)
|
||||||
|
for cap in capabilities:
|
||||||
|
if cap in query_caps:
|
||||||
|
score += 5
|
||||||
|
reasons.append(f"capability:{cap}")
|
||||||
|
|
||||||
|
# 4. Description word overlap (+1 each, max 10)
|
||||||
|
query_words = normalize(query)
|
||||||
|
desc_words = normalize(description)
|
||||||
|
overlap = query_words & desc_words
|
||||||
|
overlap_score = min(len(overlap), 10)
|
||||||
|
if overlap_score > 0:
|
||||||
|
score += overlap_score
|
||||||
|
reasons.append(f"word_overlap:{overlap_score}")
|
||||||
|
|
||||||
|
# 5. Project assignment boost (+20)
|
||||||
|
if project_skills and name in project_skills:
|
||||||
|
score += 20
|
||||||
|
reasons.append("project_boost")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"score": score,
|
||||||
|
"reasons": reasons,
|
||||||
|
"location": skill.get("location", ""),
|
||||||
|
"skill_md": skill.get("skill_md", ""),
|
||||||
|
"capabilities": capabilities,
|
||||||
|
"status": skill.get("status", "unknown"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def match(query: str, project: str = None, top_n: int = 5, threshold: int = 5) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Match a query against all registered skills.
|
||||||
|
|
||||||
|
Returns top N skills with score >= threshold, sorted by score descending.
|
||||||
|
"""
|
||||||
|
skills = load_registry()
|
||||||
|
if not skills:
|
||||||
|
return []
|
||||||
|
|
||||||
|
project_skills = get_project_skills(project) if project else set()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for skill in skills:
|
||||||
|
result = score_skill(skill, query, project_skills)
|
||||||
|
if result["score"] >= threshold:
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
results.sort(key=lambda x: x["score"], reverse=True)
|
||||||
|
return results[:top_n]
|
||||||
|
|
||||||
|
|
||||||
|
# ── CLI Entry Point ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[1:]
|
||||||
|
project = None
|
||||||
|
query_parts = []
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(args):
|
||||||
|
if args[i] == "--project" and i + 1 < len(args):
|
||||||
|
project = args[i + 1]
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
query_parts.append(args[i])
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
query = " ".join(query_parts)
|
||||||
|
|
||||||
|
if not query:
|
||||||
|
print(json.dumps({
|
||||||
|
"error": "No query provided",
|
||||||
|
"usage": 'python match_skills.py "your query here"'
|
||||||
|
}, indent=2))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
results = match(query, project=project)
|
||||||
|
|
||||||
|
output = {
|
||||||
|
"query": query,
|
||||||
|
"project": project,
|
||||||
|
"matched": len(results),
|
||||||
|
"skills": results,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) == 0:
|
||||||
|
output["recommendation"] = "No skills matched. Operate without skills or suggest creating a new one."
|
||||||
|
elif len(results) == 1:
|
||||||
|
output["recommendation"] = f"Single skill match: use '{results[0]['name']}' directly."
|
||||||
|
output["action"] = "load_skill"
|
||||||
|
else:
|
||||||
|
output["recommendation"] = f"Multiple skills matched ({len(results)}). Use orchestration."
|
||||||
|
output["action"] = "orchestrate"
|
||||||
|
|
||||||
|
print(json.dumps(output, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Multi-Skill Orchestration Engine for Agent Orchestrator.
|
||||||
|
|
||||||
|
Given matched skills and a query, determines the orchestration pattern
|
||||||
|
and generates an execution plan for Claude to follow.
|
||||||
|
|
||||||
|
Patterns:
|
||||||
|
- single: One skill handles the entire request
|
||||||
|
- sequential: Skills form a pipeline (A output -> B input)
|
||||||
|
- parallel: Skills work independently on different aspects
|
||||||
|
- primary_support: One skill leads, others provide supporting data
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python orchestrate.py --skills web-scraper,whatsapp-cloud-api --query "monitorar precos e enviar alerta"
|
||||||
|
python orchestrate.py --match-result '{"skills": [...]}' --query "query"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# ── Configuration ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Resolve paths relative to this script's location
|
||||||
|
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
ORCHESTRATOR_DIR = _SCRIPT_DIR.parent
|
||||||
|
SKILLS_ROOT = ORCHESTRATOR_DIR.parent
|
||||||
|
DATA_DIR = ORCHESTRATOR_DIR / "data"
|
||||||
|
REGISTRY_PATH = DATA_DIR / "registry.json"
|
||||||
|
|
||||||
|
# Define which capabilities are typically "producers" vs "consumers"
|
||||||
|
# Producers generate data; consumers act on data
|
||||||
|
PRODUCER_CAPABILITIES = {"data-extraction", "government-data", "analytics"}
|
||||||
|
CONSUMER_CAPABILITIES = {"messaging", "social-media", "content-management"}
|
||||||
|
HYBRID_CAPABILITIES = {"api-integration", "web-automation"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Functions ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def load_registry() -> dict[str, dict]:
|
||||||
|
"""Load registry as name->skill dict."""
|
||||||
|
if not REGISTRY_PATH.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
data = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
|
||||||
|
return {s["name"]: s for s in data.get("skills", [])}
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_skill_role(skill: dict) -> str:
|
||||||
|
"""Determine if a skill is primarily a producer, consumer, or hybrid.
|
||||||
|
|
||||||
|
Uses weighted scoring: more specific capabilities (data-extraction,
|
||||||
|
messaging) outweigh generic ones (api-integration, content-management).
|
||||||
|
"""
|
||||||
|
caps = set(skill.get("capabilities", []))
|
||||||
|
|
||||||
|
producer_count = len(caps & PRODUCER_CAPABILITIES)
|
||||||
|
consumer_count = len(caps & CONSUMER_CAPABILITIES)
|
||||||
|
|
||||||
|
# If skill has both producer and consumer caps, use the dominant one
|
||||||
|
if producer_count > consumer_count:
|
||||||
|
return "producer"
|
||||||
|
elif consumer_count > producer_count:
|
||||||
|
return "consumer"
|
||||||
|
elif producer_count > 0 and consumer_count > 0:
|
||||||
|
# Equal weight - check if core name suggests a role
|
||||||
|
name = skill.get("name", "").lower()
|
||||||
|
if any(kw in name for kw in ["scraper", "extract", "collect", "data", "junta"]):
|
||||||
|
return "producer"
|
||||||
|
if any(kw in name for kw in ["whatsapp", "instagram", "messenger", "notify"]):
|
||||||
|
return "consumer"
|
||||||
|
return "hybrid"
|
||||||
|
else:
|
||||||
|
return "hybrid"
|
||||||
|
|
||||||
|
|
||||||
|
def classify_pattern(skills: list[dict], query: str) -> str:
|
||||||
|
"""
|
||||||
|
Determine the orchestration pattern based on skill roles and query.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
1. Single skill -> "single"
|
||||||
|
2. Producer(s) + Consumer(s) -> "sequential" (data flows producer->consumer)
|
||||||
|
3. All same role -> "parallel" (independent work)
|
||||||
|
4. One high-score + others lower -> "primary_support"
|
||||||
|
"""
|
||||||
|
if len(skills) <= 1:
|
||||||
|
return "single"
|
||||||
|
|
||||||
|
roles = [get_skill_role(s) for s in skills]
|
||||||
|
has_producer = "producer" in roles
|
||||||
|
has_consumer = "consumer" in roles
|
||||||
|
|
||||||
|
# Producer -> Consumer pipeline
|
||||||
|
if has_producer and has_consumer:
|
||||||
|
return "sequential"
|
||||||
|
|
||||||
|
# Check if one skill dominates by score
|
||||||
|
scores = [s.get("score", 0) for s in skills]
|
||||||
|
if len(scores) >= 2:
|
||||||
|
scores_sorted = sorted(scores, reverse=True)
|
||||||
|
if scores_sorted[0] >= scores_sorted[1] * 2:
|
||||||
|
return "primary_support"
|
||||||
|
|
||||||
|
# All same role or no clear pipeline
|
||||||
|
return "parallel"
|
||||||
|
|
||||||
|
|
||||||
|
def generate_plan(skills: list[dict], query: str, pattern: str) -> dict:
|
||||||
|
"""Generate an execution plan based on the pattern."""
|
||||||
|
|
||||||
|
if pattern == "single":
|
||||||
|
skill = skills[0]
|
||||||
|
return {
|
||||||
|
"pattern": "single",
|
||||||
|
"description": f"Use '{skill['name']}' to handle the entire request.",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"order": 1,
|
||||||
|
"skill": skill["name"],
|
||||||
|
"skill_md": skill.get("skill_md", skill.get("location", "")),
|
||||||
|
"action": f"Load SKILL.md and follow its workflow for: {query}",
|
||||||
|
"input": "user_query",
|
||||||
|
"output": "result",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data_flow": "user_query -> result",
|
||||||
|
}
|
||||||
|
|
||||||
|
elif pattern == "sequential":
|
||||||
|
# Order: producers first, then consumers
|
||||||
|
producers = [s for s in skills if get_skill_role(s) in ("producer", "hybrid")]
|
||||||
|
consumers = [s for s in skills if get_skill_role(s) == "consumer"]
|
||||||
|
|
||||||
|
# If no clear producers, use score order
|
||||||
|
if not producers:
|
||||||
|
producers = [skills[0]]
|
||||||
|
consumers = skills[1:]
|
||||||
|
|
||||||
|
ordered = producers + consumers
|
||||||
|
steps = []
|
||||||
|
for i, skill in enumerate(ordered):
|
||||||
|
role = get_skill_role(skill)
|
||||||
|
if i == 0:
|
||||||
|
input_src = "user_query"
|
||||||
|
action = f"Extract/collect data: {query}"
|
||||||
|
else:
|
||||||
|
prev = ordered[i - 1]["name"]
|
||||||
|
input_src = f"{prev}.output"
|
||||||
|
if role == "consumer":
|
||||||
|
action = f"Process/deliver data from {prev}"
|
||||||
|
else:
|
||||||
|
action = f"Continue processing with data from {prev}"
|
||||||
|
|
||||||
|
steps.append({
|
||||||
|
"order": i + 1,
|
||||||
|
"skill": skill["name"],
|
||||||
|
"skill_md": skill.get("skill_md", skill.get("location", "")),
|
||||||
|
"action": action,
|
||||||
|
"input": input_src,
|
||||||
|
"output": f"{skill['name']}.output",
|
||||||
|
"role": role,
|
||||||
|
})
|
||||||
|
|
||||||
|
flow_parts = [s["skill"] for s in steps]
|
||||||
|
data_flow = " -> ".join(["user_query"] + flow_parts + ["result"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pattern": "sequential",
|
||||||
|
"description": f"Pipeline: {' -> '.join(flow_parts)}",
|
||||||
|
"steps": steps,
|
||||||
|
"data_flow": data_flow,
|
||||||
|
}
|
||||||
|
|
||||||
|
elif pattern == "parallel":
|
||||||
|
steps = []
|
||||||
|
for i, skill in enumerate(skills):
|
||||||
|
steps.append({
|
||||||
|
"order": 1, # All run at the same "order" level
|
||||||
|
"skill": skill["name"],
|
||||||
|
"skill_md": skill.get("skill_md", skill.get("location", "")),
|
||||||
|
"action": f"Handle independently: aspect of '{query}' related to {', '.join(skill.get('capabilities', []))}",
|
||||||
|
"input": "user_query",
|
||||||
|
"output": f"{skill['name']}.output",
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pattern": "parallel",
|
||||||
|
"description": f"Execute {len(skills)} skills in parallel, each handling their domain.",
|
||||||
|
"steps": steps,
|
||||||
|
"data_flow": "user_query -> [parallel] -> aggregated_result",
|
||||||
|
"aggregation": "Combine results from all skills into a unified response.",
|
||||||
|
}
|
||||||
|
|
||||||
|
elif pattern == "primary_support":
|
||||||
|
primary = skills[0] # Highest score
|
||||||
|
support = skills[1:]
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
{
|
||||||
|
"order": 1,
|
||||||
|
"skill": primary["name"],
|
||||||
|
"skill_md": primary.get("skill_md", primary.get("location", "")),
|
||||||
|
"action": f"Primary: handle main request: {query}",
|
||||||
|
"input": "user_query",
|
||||||
|
"output": f"{primary['name']}.output",
|
||||||
|
"role": "primary",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, skill in enumerate(support):
|
||||||
|
steps.append({
|
||||||
|
"order": 2,
|
||||||
|
"skill": skill["name"],
|
||||||
|
"skill_md": skill.get("skill_md", skill.get("location", "")),
|
||||||
|
"action": f"Support: provide {', '.join(skill.get('capabilities', []))} data if needed",
|
||||||
|
"input": "user_query",
|
||||||
|
"output": f"{skill['name']}.output",
|
||||||
|
"role": "support",
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pattern": "primary_support",
|
||||||
|
"description": f"Primary: '{primary['name']}'. Support: {', '.join(s['name'] for s in support)}.",
|
||||||
|
"steps": steps,
|
||||||
|
"data_flow": f"user_query -> {primary['name']} (primary) + support skills as needed -> result",
|
||||||
|
}
|
||||||
|
|
||||||
|
return {"pattern": "unknown", "steps": [], "data_flow": ""}
|
||||||
|
|
||||||
|
|
||||||
|
# ── CLI Entry Point ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[1:]
|
||||||
|
skill_names = []
|
||||||
|
query = ""
|
||||||
|
match_result = None
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(args):
|
||||||
|
if args[i] == "--skills" and i + 1 < len(args):
|
||||||
|
skill_names = [s.strip() for s in args[i + 1].split(",")]
|
||||||
|
i += 2
|
||||||
|
elif args[i] == "--query" and i + 1 < len(args):
|
||||||
|
query = args[i + 1]
|
||||||
|
i += 2
|
||||||
|
elif args[i] == "--match-result" and i + 1 < len(args):
|
||||||
|
match_result = json.loads(args[i + 1])
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
# Treat as query if no flag
|
||||||
|
query = args[i]
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Get skill data from match result or registry
|
||||||
|
skills = []
|
||||||
|
if match_result:
|
||||||
|
skills = match_result.get("skills", [])
|
||||||
|
elif skill_names:
|
||||||
|
registry = load_registry()
|
||||||
|
for name in skill_names:
|
||||||
|
if name in registry:
|
||||||
|
skill_data = registry[name]
|
||||||
|
skill_data["score"] = 10 # default score
|
||||||
|
skills.append(skill_data)
|
||||||
|
|
||||||
|
if not skills:
|
||||||
|
print(json.dumps({
|
||||||
|
"error": "No skills provided",
|
||||||
|
"usage": 'python orchestrate.py --skills skill1,skill2 --query "your query"'
|
||||||
|
}, indent=2))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not query:
|
||||||
|
print(json.dumps({
|
||||||
|
"error": "No query provided",
|
||||||
|
"usage": 'python orchestrate.py --skills skill1,skill2 --query "your query"'
|
||||||
|
}, indent=2))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Classify and generate plan
|
||||||
|
pattern = classify_pattern(skills, query)
|
||||||
|
plan = generate_plan(skills, query, pattern)
|
||||||
|
plan["query"] = query
|
||||||
|
plan["skill_count"] = len(skills)
|
||||||
|
|
||||||
|
# Add instructions for Claude
|
||||||
|
plan["instructions"] = []
|
||||||
|
for step in plan.get("steps", []):
|
||||||
|
skill_md = step.get("skill_md", "")
|
||||||
|
if skill_md:
|
||||||
|
plan["instructions"].append(
|
||||||
|
f"Step {step['order']}: Read {skill_md} and follow its workflow for: {step['action']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(json.dumps(plan, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pyyaml>=6.0
|
||||||
@@ -0,0 +1,508 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Auto-Discovery Engine for Agent Orchestrator.
|
||||||
|
|
||||||
|
Scans the skills ecosystem for SKILL.md files, parses metadata,
|
||||||
|
and maintains a centralized registry (registry.json).
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Runs automatically on every request (called by CLAUDE.md)
|
||||||
|
- Ultra-fast via MD5 hash caching (~<100ms when nothing changed)
|
||||||
|
- Auto-includes new skills, auto-removes deleted skills
|
||||||
|
- Zero manual intervention required
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scan_registry.py # Quick scan (hash-based)
|
||||||
|
python scan_registry.py --status # Verbose status table
|
||||||
|
python scan_registry.py --force # Full re-scan ignoring hashes
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# ── Configuration ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Resolve paths relative to this script's location
|
||||||
|
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
ORCHESTRATOR_DIR = _SCRIPT_DIR.parent
|
||||||
|
SKILLS_ROOT = ORCHESTRATOR_DIR.parent
|
||||||
|
DATA_DIR = ORCHESTRATOR_DIR / "data"
|
||||||
|
REGISTRY_PATH = DATA_DIR / "registry.json"
|
||||||
|
HASHES_PATH = DATA_DIR / "registry_hashes.json"
|
||||||
|
|
||||||
|
# Where to search for SKILL.md files
|
||||||
|
SEARCH_PATHS = [
|
||||||
|
SKILLS_ROOT / ".claude" / "skills", # registered skills
|
||||||
|
SKILLS_ROOT, # top-level standalone
|
||||||
|
]
|
||||||
|
MAX_DEPTH = 3 # max directory depth for SKILL.md search
|
||||||
|
|
||||||
|
# Capability keyword mapping (PT + EN)
|
||||||
|
CAPABILITY_MAP = {
|
||||||
|
"data-extraction": [
|
||||||
|
"scrape", "extract", "crawl", "parse", "harvest", "collect",
|
||||||
|
"raspar", "extrair", "coletar", "dados",
|
||||||
|
],
|
||||||
|
"messaging": [
|
||||||
|
"whatsapp", "message", "send", "chat", "notification", "sms",
|
||||||
|
"mensagem", "enviar", "notificacao", "atendimento",
|
||||||
|
],
|
||||||
|
"social-media": [
|
||||||
|
"instagram", "facebook", "twitter", "post", "stories", "reels",
|
||||||
|
"social", "engagement", "feed", "follower",
|
||||||
|
],
|
||||||
|
"government-data": [
|
||||||
|
"junta", "leiloeiro", "cadastro", "governo", "comercial",
|
||||||
|
"tribunal", "diario oficial", "certidao", "registro",
|
||||||
|
],
|
||||||
|
"web-automation": [
|
||||||
|
"browser", "selenium", "playwright", "automate", "click",
|
||||||
|
"navegador", "automatizar", "automacao",
|
||||||
|
],
|
||||||
|
"api-integration": [
|
||||||
|
"api", "endpoint", "webhook", "rest", "graph", "oauth",
|
||||||
|
"integracao", "integrar",
|
||||||
|
],
|
||||||
|
"analytics": [
|
||||||
|
"insight", "analytics", "metrics", "dashboard", "report",
|
||||||
|
"relatorio", "metricas", "analise",
|
||||||
|
],
|
||||||
|
"content-management": [
|
||||||
|
"publish", "schedule", "template", "content", "media",
|
||||||
|
"publicar", "agendar", "conteudo", "midia",
|
||||||
|
],
|
||||||
|
"legal": [
|
||||||
|
"advogado", "direito", "juridico", "lei", "processo",
|
||||||
|
"acao", "peticao", "recurso", "sentenca", "juiz",
|
||||||
|
"divorcio", "guarda", "alimentos", "pensao", "alimenticia", "inventario", "heranca", "partilha",
|
||||||
|
"acidente de trabalho", "acidente",
|
||||||
|
"familia", "criminal", "penal", "crime", "feminicidio", "maria da penha",
|
||||||
|
"violencia domestica", "medida protetiva", "stalking",
|
||||||
|
"danos morais", "responsabilidade civil", "indenizacao", "dano",
|
||||||
|
"consumidor", "cdc", "plano de saude",
|
||||||
|
"trabalhista", "clt", "rescisao", "fgts", "horas extras",
|
||||||
|
"previdenciario", "aposentadoria", "aposentar", "inss",
|
||||||
|
"imobiliario", "usucapiao", "despejo", "inquilinato",
|
||||||
|
"alienacao fiduciaria", "bem de familia",
|
||||||
|
"tributario", "imposto", "icms", "execucao fiscal",
|
||||||
|
"administrativo", "licitacao", "improbidade", "mandado de seguranca",
|
||||||
|
"empresarial", "societario", "falencia", "recuperacao judicial",
|
||||||
|
"empresa", "ltda", "cnpj", "mei", "eireli", "contrato social",
|
||||||
|
"contrato", "clausula", "contestacao", "apelacao", "agravo",
|
||||||
|
"habeas corpus", "mandado", "liminar", "tutela",
|
||||||
|
"cpc", "stj", "stf", "sumula", "jurisprudencia",
|
||||||
|
"oab", "honorarios", "custas",
|
||||||
|
],
|
||||||
|
"auction": [
|
||||||
|
"leilao", "leilao judicial", "leilao extrajudicial", "hasta publica",
|
||||||
|
"arrematacao", "arrematar", "arrematante", "lance", "desagio",
|
||||||
|
"edital leilao", "penhora", "adjudicacao", "praca",
|
||||||
|
"imissao na posse", "carta arrematacao", "vil preco",
|
||||||
|
"avaliacao imovel", "laudo", "perito", "matricula",
|
||||||
|
"leiloeiro", "comissao leiloeiro",
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
"seguranca", "security", "owasp", "vulnerability", "incident",
|
||||||
|
"pentest", "firewall", "malware", "phishing", "cve",
|
||||||
|
"autenticacao", "criptografia", "encryption",
|
||||||
|
],
|
||||||
|
"image-generation": [
|
||||||
|
"imagem", "image", "gerar imagem", "generate image",
|
||||||
|
"stable diffusion", "comfyui", "midjourney", "dall-e",
|
||||||
|
"foto", "ilustracao", "arte", "design",
|
||||||
|
],
|
||||||
|
"monitoring": [
|
||||||
|
"monitor", "monitorar", "health", "status",
|
||||||
|
"audit", "auditoria", "sentinel", "check",
|
||||||
|
],
|
||||||
|
"context-management": [
|
||||||
|
"contexto", "context", "sessao", "session", "compactacao", "compaction",
|
||||||
|
"comprimir", "compress", "snapshot", "checkpoint", "briefing",
|
||||||
|
"continuidade", "continuity", "preservar", "preserve",
|
||||||
|
"memoria", "memory", "resumo", "summary",
|
||||||
|
"salvar estado", "save state", "context window", "janela de contexto",
|
||||||
|
"perda de dados", "data loss", "backup",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Utility Functions ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def md5_file(path: Path) -> str:
|
||||||
|
"""Compute MD5 hash of a file."""
|
||||||
|
h = hashlib.md5()
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
for chunk in iter(lambda: f.read(8192), b""):
|
||||||
|
h.update(chunk)
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_yaml_frontmatter(path: Path) -> dict:
|
||||||
|
"""Extract YAML frontmatter from a SKILL.md file."""
|
||||||
|
try:
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
match = re.match(r"^---\s*\n(.*?)\n---", text, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
return yaml.safe_load(match.group(1)) or {}
|
||||||
|
except Exception:
|
||||||
|
# Fallback: manual parsing for name/description
|
||||||
|
result = {}
|
||||||
|
block = match.group(1)
|
||||||
|
for key in ("name", "description", "version"):
|
||||||
|
m = re.search(rf'^{key}:\s*["\']?(.+?)["\']?\s*$', block, re.MULTILINE)
|
||||||
|
if m:
|
||||||
|
result[key] = m.group(1).strip()
|
||||||
|
else:
|
||||||
|
# Handle multi-line description with >- or >
|
||||||
|
m2 = re.search(rf'^{key}:\s*>-?\s*\n((?:\s+.+\n?)+)', block, re.MULTILINE)
|
||||||
|
if m2:
|
||||||
|
lines = m2.group(1).strip().split("\n")
|
||||||
|
result[key] = " ".join(line.strip() for line in lines)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def find_skill_files() -> list[Path]:
|
||||||
|
"""Find all SKILL.md files in the ecosystem."""
|
||||||
|
found = set()
|
||||||
|
|
||||||
|
for base in SEARCH_PATHS:
|
||||||
|
if not base.exists():
|
||||||
|
continue
|
||||||
|
for root, dirs, files in os.walk(base):
|
||||||
|
depth = len(Path(root).relative_to(base).parts)
|
||||||
|
if depth > MAX_DEPTH:
|
||||||
|
dirs.clear()
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip the orchestrator itself
|
||||||
|
if "agent-orchestrator" in Path(root).parts:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "SKILL.md" in files:
|
||||||
|
found.add(Path(root) / "SKILL.md")
|
||||||
|
|
||||||
|
return sorted(found)
|
||||||
|
|
||||||
|
|
||||||
|
def detect_language(skill_dir: Path) -> str:
|
||||||
|
"""Detect primary language from scripts/ directory."""
|
||||||
|
scripts_dir = skill_dir / "scripts"
|
||||||
|
if not scripts_dir.exists():
|
||||||
|
return "none"
|
||||||
|
|
||||||
|
extensions = set()
|
||||||
|
for f in scripts_dir.rglob("*"):
|
||||||
|
if f.is_file():
|
||||||
|
extensions.add(f.suffix.lower())
|
||||||
|
|
||||||
|
if ".py" in extensions:
|
||||||
|
return "python"
|
||||||
|
if ".ts" in extensions or ".js" in extensions:
|
||||||
|
return "nodejs"
|
||||||
|
if ".sh" in extensions:
|
||||||
|
return "bash"
|
||||||
|
return "none"
|
||||||
|
|
||||||
|
|
||||||
|
def extract_capabilities(description: str) -> list[str]:
|
||||||
|
"""Map description keywords to capability tags using word boundary matching."""
|
||||||
|
if not description:
|
||||||
|
return []
|
||||||
|
|
||||||
|
desc_lower = description.lower()
|
||||||
|
desc_words = set(re.findall(r'[a-zA-ZÀ-ÿ]+', desc_lower))
|
||||||
|
caps = []
|
||||||
|
for cap, keywords in CAPABILITY_MAP.items():
|
||||||
|
for kw in keywords:
|
||||||
|
# Multi-word keywords: substring match. Single-word: exact word match.
|
||||||
|
if " " in kw:
|
||||||
|
if kw in desc_lower:
|
||||||
|
caps.append(cap)
|
||||||
|
break
|
||||||
|
elif kw in desc_words:
|
||||||
|
caps.append(cap)
|
||||||
|
break
|
||||||
|
return sorted(caps)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_triggers(description: str) -> list[str]:
|
||||||
|
"""Extract trigger keywords from description text using word boundary matching."""
|
||||||
|
if not description:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Collect all keywords from all capability categories
|
||||||
|
all_keywords = set()
|
||||||
|
for keywords in CAPABILITY_MAP.values():
|
||||||
|
all_keywords.update(keywords)
|
||||||
|
|
||||||
|
desc_lower = description.lower()
|
||||||
|
desc_words = set(re.findall(r'[a-zA-ZÀ-ÿ]+', desc_lower))
|
||||||
|
found = []
|
||||||
|
for kw in sorted(all_keywords):
|
||||||
|
if " " in kw:
|
||||||
|
if kw in desc_lower:
|
||||||
|
found.append(kw)
|
||||||
|
elif kw in desc_words:
|
||||||
|
found.append(kw)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def assess_status(skill_dir: Path) -> str:
|
||||||
|
"""Check if skill is complete (active) or incomplete."""
|
||||||
|
skill_md = skill_dir / "SKILL.md"
|
||||||
|
if not skill_md.exists():
|
||||||
|
return "missing"
|
||||||
|
|
||||||
|
has_scripts = (skill_dir / "scripts").exists()
|
||||||
|
has_refs = (skill_dir / "references").exists()
|
||||||
|
|
||||||
|
# Parse frontmatter to check for required fields
|
||||||
|
meta = parse_yaml_frontmatter(skill_md)
|
||||||
|
has_name = bool(meta.get("name"))
|
||||||
|
has_desc = bool(meta.get("description"))
|
||||||
|
|
||||||
|
if has_name and has_desc:
|
||||||
|
return "active"
|
||||||
|
return "incomplete"
|
||||||
|
|
||||||
|
|
||||||
|
def is_registered(skill_dir: Path) -> bool:
|
||||||
|
"""Check if skill is in .claude/skills/."""
|
||||||
|
claude_skills = SKILLS_ROOT / ".claude" / "skills"
|
||||||
|
try:
|
||||||
|
skill_dir.relative_to(claude_skills)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ── Main Logic ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def load_hashes() -> dict:
|
||||||
|
"""Load stored hashes from registry_hashes.json."""
|
||||||
|
if HASHES_PATH.exists():
|
||||||
|
try:
|
||||||
|
return json.loads(HASHES_PATH.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def save_hashes(hashes: dict):
|
||||||
|
"""Save hashes to registry_hashes.json."""
|
||||||
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
HASHES_PATH.write_text(json.dumps(hashes, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def load_registry() -> dict:
|
||||||
|
"""Load existing registry.json."""
|
||||||
|
if REGISTRY_PATH.exists():
|
||||||
|
try:
|
||||||
|
return json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"generated_at": None, "skills_root": str(SKILLS_ROOT), "skills": []}
|
||||||
|
|
||||||
|
|
||||||
|
def save_registry(registry: dict):
|
||||||
|
"""Save registry.json."""
|
||||||
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
registry["generated_at"] = datetime.now().isoformat()
|
||||||
|
REGISTRY_PATH.write_text(json.dumps(registry, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def build_skill_entry(skill_md_path: Path) -> dict:
|
||||||
|
"""Build a registry entry from a SKILL.md file."""
|
||||||
|
skill_dir = skill_md_path.parent
|
||||||
|
meta = parse_yaml_frontmatter(skill_md_path)
|
||||||
|
description = meta.get("description", "")
|
||||||
|
|
||||||
|
# Support explicit capabilities in frontmatter
|
||||||
|
explicit_caps = meta.get("capabilities", [])
|
||||||
|
if isinstance(explicit_caps, str):
|
||||||
|
explicit_caps = [c.strip() for c in explicit_caps.split(",")]
|
||||||
|
|
||||||
|
auto_caps = extract_capabilities(description)
|
||||||
|
all_caps = sorted(set(auto_caps + explicit_caps))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": meta.get("name", skill_dir.name),
|
||||||
|
"description": description,
|
||||||
|
"version": meta.get("version", ""),
|
||||||
|
"location": str(skill_dir),
|
||||||
|
"skill_md": str(skill_md_path),
|
||||||
|
"registered": is_registered(skill_dir),
|
||||||
|
"has_scripts": (skill_dir / "scripts").exists(),
|
||||||
|
"has_references": (skill_dir / "references").exists(),
|
||||||
|
"has_data": (skill_dir / "data").exists(),
|
||||||
|
"capabilities": all_caps,
|
||||||
|
"triggers": extract_triggers(description),
|
||||||
|
"language": detect_language(skill_dir),
|
||||||
|
"status": assess_status(skill_dir),
|
||||||
|
"last_modified": datetime.fromtimestamp(
|
||||||
|
skill_md_path.stat().st_mtime
|
||||||
|
).isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def scan(force: bool = False) -> dict:
|
||||||
|
"""
|
||||||
|
Main scan function.
|
||||||
|
|
||||||
|
With hash caching:
|
||||||
|
1. Find all SKILL.md files
|
||||||
|
2. Compare MD5 hashes with stored values
|
||||||
|
3. Only re-parse files that changed, were added, or removed
|
||||||
|
4. Update registry incrementally
|
||||||
|
"""
|
||||||
|
current_files = find_skill_files()
|
||||||
|
current_paths = {str(f): f for f in current_files}
|
||||||
|
|
||||||
|
stored_hashes = load_hashes()
|
||||||
|
registry = load_registry()
|
||||||
|
|
||||||
|
# Build lookup of existing registry entries by skill_md path
|
||||||
|
existing_by_path = {}
|
||||||
|
for entry in registry.get("skills", []):
|
||||||
|
existing_by_path[entry.get("skill_md", "")] = entry
|
||||||
|
|
||||||
|
# Compute current hashes
|
||||||
|
new_hashes = {}
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
for path_str, path_obj in current_paths.items():
|
||||||
|
current_hash = md5_file(path_obj)
|
||||||
|
new_hashes[path_str] = current_hash
|
||||||
|
|
||||||
|
if force or path_str not in stored_hashes or stored_hashes[path_str] != current_hash:
|
||||||
|
# New or modified - rebuild entry
|
||||||
|
entry = build_skill_entry(path_obj)
|
||||||
|
existing_by_path[path_str] = entry
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
# Detect removed skills
|
||||||
|
for old_path in list(existing_by_path.keys()):
|
||||||
|
if old_path not in current_paths and old_path != "":
|
||||||
|
del existing_by_path[old_path]
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
# Check if file set changed (additions/removals)
|
||||||
|
if set(new_hashes.keys()) != set(stored_hashes.keys()):
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
# Deduplicate by skill name (case-insensitive).
|
||||||
|
# When the same skill exists in both skills/ and .claude/skills/,
|
||||||
|
# prefer the primary location (skills/) over the registered copy.
|
||||||
|
if changed or not REGISTRY_PATH.exists():
|
||||||
|
by_name = {}
|
||||||
|
for entry in existing_by_path.values():
|
||||||
|
name = entry.get("name", "").lower()
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
if name not in by_name:
|
||||||
|
by_name[name] = entry
|
||||||
|
else:
|
||||||
|
# Prefer the version NOT in .claude/skills/ (the primary source)
|
||||||
|
existing = by_name[name]
|
||||||
|
existing_is_registered = existing.get("registered", False)
|
||||||
|
new_is_registered = entry.get("registered", False)
|
||||||
|
if existing_is_registered and not new_is_registered:
|
||||||
|
by_name[name] = entry
|
||||||
|
# If both are primary or both registered, keep first found
|
||||||
|
|
||||||
|
registry["skills"] = sorted(by_name.values(), key=lambda s: s.get("name", ""))
|
||||||
|
save_registry(registry)
|
||||||
|
save_hashes(new_hashes)
|
||||||
|
return registry
|
||||||
|
else:
|
||||||
|
# Nothing changed, return existing
|
||||||
|
return registry
|
||||||
|
|
||||||
|
|
||||||
|
def print_status(registry: dict):
|
||||||
|
"""Print a formatted status table."""
|
||||||
|
skills = registry.get("skills", [])
|
||||||
|
|
||||||
|
if not skills:
|
||||||
|
print("No skills found in the ecosystem.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n{'='*80}")
|
||||||
|
print(f" Agent Orchestrator - Skill Registry Status")
|
||||||
|
print(f" Scanned at: {registry.get('generated_at', 'N/A')}")
|
||||||
|
print(f" Root: {registry.get('skills_root', 'N/A')}")
|
||||||
|
print(f"{'='*80}\n")
|
||||||
|
|
||||||
|
# Header
|
||||||
|
print(f" {'Name':<22} {'Status':<12} {'Lang':<10} {'Registered':<12} {'Capabilities'}")
|
||||||
|
print(f" {'-'*22} {'-'*12} {'-'*10} {'-'*12} {'-'*30}")
|
||||||
|
|
||||||
|
for s in sorted(skills, key=lambda x: x.get("name", "")):
|
||||||
|
name = s.get("name", "?")[:20]
|
||||||
|
status = s.get("status", "?")
|
||||||
|
lang = s.get("language", "none")
|
||||||
|
reg = "Yes" if s.get("registered") else "No"
|
||||||
|
caps = ", ".join(s.get("capabilities", []))[:30]
|
||||||
|
print(f" {name:<22} {status:<12} {lang:<10} {reg:<12} {caps}")
|
||||||
|
|
||||||
|
print(f"\n Total: {len(skills)} skills")
|
||||||
|
|
||||||
|
# Recommendations
|
||||||
|
unregistered = [s for s in skills if not s.get("registered")]
|
||||||
|
incomplete = [s for s in skills if s.get("status") == "incomplete"]
|
||||||
|
|
||||||
|
if unregistered:
|
||||||
|
print(f"\n [!] {len(unregistered)} skill(s) not registered in .claude/skills/:")
|
||||||
|
for s in unregistered:
|
||||||
|
print(f" - {s['name']} ({s['location']})")
|
||||||
|
|
||||||
|
if incomplete:
|
||||||
|
print(f"\n [!] {len(incomplete)} skill(s) with incomplete status:")
|
||||||
|
for s in incomplete:
|
||||||
|
print(f" - {s['name']} ({s['location']})")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
# ── CLI Entry Point ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main():
|
||||||
|
force = "--force" in sys.argv
|
||||||
|
show_status = "--status" in sys.argv
|
||||||
|
|
||||||
|
registry = scan(force=force)
|
||||||
|
|
||||||
|
if show_status:
|
||||||
|
print_status(registry)
|
||||||
|
else:
|
||||||
|
# Default: output JSON summary for Claude to parse
|
||||||
|
skills = registry.get("skills", [])
|
||||||
|
summary = {
|
||||||
|
"total": len(skills),
|
||||||
|
"active": len([s for s in skills if s.get("status") == "active"]),
|
||||||
|
"incomplete": len([s for s in skills if s.get("status") == "incomplete"]),
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"name": s.get("name"),
|
||||||
|
"status": s.get("status"),
|
||||||
|
"capabilities": s.get("capabilities", []),
|
||||||
|
}
|
||||||
|
for s in skills
|
||||||
|
],
|
||||||
|
}
|
||||||
|
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
---
|
||||||
|
name: agentmail
|
||||||
|
description: Email infrastructure for AI agents. Create accounts, send/receive emails, manage webhooks, and check karma balance via the AgentMail API.
|
||||||
|
risk: safe
|
||||||
|
source: community
|
||||||
|
---
|
||||||
|
|
||||||
|
# AgentMail — Email for AI Agents
|
||||||
|
|
||||||
|
AgentMail gives AI agents real email addresses (`@theagentmail.net`) with a REST API. Agents can send and receive email, sign up for services (GitHub, AWS, Slack, etc.), and get verification codes. A karma system prevents spam and keeps the shared domain's reputation high.
|
||||||
|
|
||||||
|
Base URL: `https://api.theagentmail.net`
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
- An AI agent needs a real inbox/outbox for signups, verification flows, or transactional communication.
|
||||||
|
- You need to provision AgentMail accounts, send messages, read inbox contents, or register inbound webhooks.
|
||||||
|
- You need to monitor karma usage or wire email events into agent automation.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
All requests require `Authorization: Bearer am_...` header (API key from dashboard).
|
||||||
|
|
||||||
|
### Create an email account (-10 karma)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://api.theagentmail.net/v1/accounts \
|
||||||
|
-H "Authorization: Bearer am_..." \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"address": "my-agent@theagentmail.net"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Response: `{"data": {"id": "...", "address": "my-agent@theagentmail.net", "displayName": null, "createdAt": 123}}`
|
||||||
|
|
||||||
|
### Send email (-1 karma)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://api.theagentmail.net/v1/accounts/{accountId}/messages \
|
||||||
|
-H "Authorization: Bearer am_..." \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"to": ["recipient@example.com"],
|
||||||
|
"subject": "Hello from my agent",
|
||||||
|
"text": "Plain text body",
|
||||||
|
"html": "<p>Optional HTML body</p>"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional fields: `cc`, `bcc` (string arrays), `inReplyTo`, `references` (strings for threading), `attachments` (array of `{filename, contentType, content}` where content is base64).
|
||||||
|
|
||||||
|
### Read inbox
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List messages
|
||||||
|
curl https://api.theagentmail.net/v1/accounts/{accountId}/messages \
|
||||||
|
-H "Authorization: Bearer am_..."
|
||||||
|
|
||||||
|
# Get full message (with body and attachments)
|
||||||
|
curl https://api.theagentmail.net/v1/accounts/{accountId}/messages/{messageId} \
|
||||||
|
-H "Authorization: Bearer am_..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check karma
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://api.theagentmail.net/v1/karma \
|
||||||
|
-H "Authorization: Bearer am_..."
|
||||||
|
```
|
||||||
|
|
||||||
|
Response: `{"data": {"balance": 90, "events": [...]}}`
|
||||||
|
|
||||||
|
### Register webhook (real-time inbound)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://api.theagentmail.net/v1/accounts/{accountId}/webhooks \
|
||||||
|
-H "Authorization: Bearer am_..." \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"url": "https://my-agent.example.com/inbox"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Webhook deliveries include two security headers:
|
||||||
|
- `X-AgentMail-Signature` -- HMAC-SHA256 hex digest of the request body, signed with the webhook secret
|
||||||
|
- `X-AgentMail-Timestamp` -- millisecond timestamp of when the delivery was sent
|
||||||
|
|
||||||
|
Verify the signature and reject requests with timestamps older than 5 minutes to prevent replay attacks:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createHmac } from "crypto";
|
||||||
|
|
||||||
|
const verifyWebhook = (body: string, signature: string, timestamp: string, secret: string) => {
|
||||||
|
if (Date.now() - Number(timestamp) > 5 * 60 * 1000) return false;
|
||||||
|
return createHmac("sha256", secret).update(body).digest("hex") === signature;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Download attachment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://api.theagentmail.net/v1/accounts/{accountId}/messages/{messageId}/attachments/{attachmentId} \
|
||||||
|
-H "Authorization: Bearer am_..."
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns `{"data": {"url": "https://signed-download-url..."}}`.
|
||||||
|
|
||||||
|
## Full API reference
|
||||||
|
|
||||||
|
| Method | Path | Description | Karma |
|
||||||
|
|--------|------|-------------|-------|
|
||||||
|
| POST | `/v1/accounts` | Create email account | -10 |
|
||||||
|
| GET | `/v1/accounts` | List all accounts | |
|
||||||
|
| GET | `/v1/accounts/:id` | Get account details | |
|
||||||
|
| DELETE | `/v1/accounts/:id` | Delete account | +10 |
|
||||||
|
| POST | `/v1/accounts/:id/messages` | Send email | -1 |
|
||||||
|
| GET | `/v1/accounts/:id/messages` | List messages | |
|
||||||
|
| GET | `/v1/accounts/:id/messages/:msgId` | Get full message | |
|
||||||
|
| GET | `/v1/accounts/:id/messages/:msgId/attachments/:attId` | Get attachment URL | |
|
||||||
|
| POST | `/v1/accounts/:id/webhooks` | Register webhook | |
|
||||||
|
| GET | `/v1/accounts/:id/webhooks` | List webhooks | |
|
||||||
|
| DELETE | `/v1/accounts/:id/webhooks/:whId` | Delete webhook | |
|
||||||
|
| GET | `/v1/karma` | Get balance + events | |
|
||||||
|
|
||||||
|
## Karma system
|
||||||
|
|
||||||
|
Every action has a karma cost or reward:
|
||||||
|
|
||||||
|
| Event | Karma | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| `money_paid` | +100 | Purchase credits |
|
||||||
|
| `email_received` | +2 | Someone replied from a trusted domain |
|
||||||
|
| `account_deleted` | +10 | Karma refunded when you delete an address |
|
||||||
|
| `email_sent` | -1 | Sending costs karma |
|
||||||
|
| `account_created` | -10 | Creating addresses costs karma |
|
||||||
|
|
||||||
|
**Important rules:**
|
||||||
|
- Karma is only awarded for inbound emails from trusted providers (Gmail, Outlook, Yahoo, iCloud, ProtonMail, Fastmail, Hey, etc.). Emails from unknown/throwaway domains don't earn karma.
|
||||||
|
- You only earn karma once per sender until the agent replies. If sender X emails you 5 times without a reply, only the first earns karma. Reply to X, and the next email from X earns karma again.
|
||||||
|
- Deleting an account refunds the 10 karma it cost to create.
|
||||||
|
|
||||||
|
When karma reaches 0, sends and account creation return HTTP 402. Always check balance before operations that cost karma.
|
||||||
|
|
||||||
|
## TypeScript SDK
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createClient } from "@agentmail/sdk";
|
||||||
|
|
||||||
|
const mail = createClient({ apiKey: "am_..." });
|
||||||
|
|
||||||
|
// Create account
|
||||||
|
const account = await mail.accounts.create({
|
||||||
|
address: "my-agent@theagentmail.net",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send email
|
||||||
|
await mail.messages.send(account.id, {
|
||||||
|
to: ["human@example.com"],
|
||||||
|
subject: "Hello",
|
||||||
|
text: "Sent by an AI agent.",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read inbox
|
||||||
|
const messages = await mail.messages.list(account.id);
|
||||||
|
const detail = await mail.messages.get(account.id, messages[0].id);
|
||||||
|
|
||||||
|
// Attachments
|
||||||
|
const att = await mail.attachments.getUrl(accountId, messageId, attachmentId);
|
||||||
|
// att.url is a signed download URL
|
||||||
|
|
||||||
|
// Webhooks
|
||||||
|
await mail.webhooks.create(account.id, {
|
||||||
|
url: "https://my-agent.example.com/inbox",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Karma
|
||||||
|
const karma = await mail.karma.getBalance();
|
||||||
|
console.log(karma.balance);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AgentMailError } from "@agentmail/sdk";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mail.messages.send(accountId, { to: ["a@b.com"], subject: "Hi", text: "Hey" });
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof AgentMailError) {
|
||||||
|
console.log(e.status); // 402, 404, 401, etc.
|
||||||
|
console.log(e.code); // "INSUFFICIENT_KARMA", "NOT_FOUND", etc.
|
||||||
|
console.log(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common patterns
|
||||||
|
|
||||||
|
### Sign up for a service and read verification email
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const account = await mail.accounts.create({
|
||||||
|
address: "signup-bot@theagentmail.net",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use the address to sign up (browser automation, API, etc.)
|
||||||
|
|
||||||
|
// Poll for verification email
|
||||||
|
for (let i = 0; i < 30; i++) {
|
||||||
|
const messages = await mail.messages.list(account.id);
|
||||||
|
const verification = messages.find(m =>
|
||||||
|
m.subject.toLowerCase().includes("verify") ||
|
||||||
|
m.subject.toLowerCase().includes("confirm")
|
||||||
|
);
|
||||||
|
if (verification) {
|
||||||
|
const detail = await mail.messages.get(account.id, verification.id);
|
||||||
|
// Parse verification link/code from detail.bodyText or detail.bodyHtml
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Send email and wait for reply
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const sent = await mail.messages.send(account.id, {
|
||||||
|
to: ["human@company.com"],
|
||||||
|
subject: "Question about order #12345",
|
||||||
|
text: "Can you check the status?",
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < 60; i++) {
|
||||||
|
const messages = await mail.messages.list(account.id);
|
||||||
|
const reply = messages.find(m =>
|
||||||
|
m.direction === "inbound" && m.timestamp > sent.timestamp
|
||||||
|
);
|
||||||
|
if (reply) {
|
||||||
|
const detail = await mail.messages.get(account.id, reply.id);
|
||||||
|
// Process reply
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await new Promise(r => setTimeout(r, 5000));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type Account = { id: string; address: string; displayName: string | null; createdAt: number };
|
||||||
|
type Message = { id: string; from: string; to: string[]; subject: string; direction: "inbound" | "outbound"; status: string; timestamp: number };
|
||||||
|
type MessageDetail = Message & { cc: string[] | null; bcc: string[] | null; bodyText: string | null; bodyHtml: string | null; inReplyTo: string | null; references: string | null; attachments: AttachmentMeta[] };
|
||||||
|
type AttachmentMeta = { id: string; filename: string; contentType: string; size: number };
|
||||||
|
type KarmaBalance = { balance: number; events: KarmaEvent[] };
|
||||||
|
type KarmaEvent = { id: string; type: string; amount: number; timestamp: number; metadata?: Record<string, unknown> };
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
---
|
||||||
|
name: ai-seo
|
||||||
|
description: "Optimize content for AI search and LLM citations across AI Overviews, ChatGPT, Perplexity, Claude, Gemini, and similar systems. Use when improving AI visibility, answer engine optimization, or citation readiness."
|
||||||
|
risk: unknown
|
||||||
|
source: "https://github.com/coreyhaines31/marketingskills"
|
||||||
|
date_added: "2026-03-21"
|
||||||
|
metadata:
|
||||||
|
version: 1.1.0
|
||||||
|
---
|
||||||
|
|
||||||
|
# AI SEO
|
||||||
|
|
||||||
|
You are an expert in AI search optimization — the practice of making content discoverable, extractable, and citable by AI systems including Google AI Overviews, ChatGPT, Perplexity, Claude, Gemini, and Copilot. Your goal is to help users get their content cited as a source in AI-generated answers.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
- Use when optimizing content to be cited by LLMs and AI search systems.
|
||||||
|
- Use when the user asks about AI SEO, AEO, GEO, LLM visibility, or AI citations.
|
||||||
|
- Use when traditional SEO alone is not the full question and AI-specific discoverability matters.
|
||||||
|
|
||||||
|
## Before Starting
|
||||||
|
|
||||||
|
**Check for product marketing context first:**
|
||||||
|
If `.agents/product-marketing-context.md` exists (or `.claude/product-marketing-context.md` 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. Current AI Visibility
|
||||||
|
- Do you know if your brand appears in AI-generated answers today?
|
||||||
|
- Have you checked ChatGPT, Perplexity, or Google AI Overviews for your key queries?
|
||||||
|
- What queries matter most to your business?
|
||||||
|
|
||||||
|
### 2. Content & Domain
|
||||||
|
- What type of content do you produce? (Blog, docs, comparisons, product pages)
|
||||||
|
- What's your domain authority / traditional SEO strength?
|
||||||
|
- Do you have existing structured data (schema markup)?
|
||||||
|
|
||||||
|
### 3. Goals
|
||||||
|
- Get cited as a source in AI answers?
|
||||||
|
- Appear in Google AI Overviews for specific queries?
|
||||||
|
- Compete with specific brands already getting cited?
|
||||||
|
- Optimize existing content or create new AI-optimized content?
|
||||||
|
|
||||||
|
### 4. Competitive Landscape
|
||||||
|
- Who are your top competitors in AI search results?
|
||||||
|
- Are they being cited where you're not?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How AI Search Works
|
||||||
|
|
||||||
|
### The AI Search Landscape
|
||||||
|
|
||||||
|
| Platform | How It Works | Source Selection |
|
||||||
|
|----------|-------------|----------------|
|
||||||
|
| **Google AI Overviews** | Summarizes top-ranking pages | Strong correlation with traditional rankings |
|
||||||
|
| **ChatGPT (with search)** | Searches web, cites sources | Draws from wider range, not just top-ranked |
|
||||||
|
| **Perplexity** | Always cites sources with links | Favors authoritative, recent, well-structured content |
|
||||||
|
| **Gemini** | Google's AI assistant | Pulls from Google index + Knowledge Graph |
|
||||||
|
| **Copilot** | Bing-powered AI search | Bing index + authoritative sources |
|
||||||
|
| **Claude** | Brave Search (when enabled) | Training data + Brave search results |
|
||||||
|
|
||||||
|
For a deep dive on how each platform selects sources and what to optimize per platform, see [references/platform-ranking-factors.md](references/platform-ranking-factors.md).
|
||||||
|
|
||||||
|
### Key Difference from Traditional SEO
|
||||||
|
|
||||||
|
Traditional SEO gets you ranked. AI SEO gets you **cited**.
|
||||||
|
|
||||||
|
In traditional search, you need to rank on page 1. In AI search, a well-structured page can get cited even if it ranks on page 2 or 3 — AI systems select sources based on content quality, structure, and relevance, not just rank position.
|
||||||
|
|
||||||
|
**Critical stats:**
|
||||||
|
- AI Overviews appear in ~45% of Google searches
|
||||||
|
- AI Overviews reduce clicks to websites by up to 58%
|
||||||
|
- Brands are 6.5x more likely to be cited via third-party sources than their own domains
|
||||||
|
- Optimized content gets cited 3x more often than non-optimized
|
||||||
|
- Statistics and citations boost visibility by 40%+ across queries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI Visibility Audit
|
||||||
|
|
||||||
|
Before optimizing, assess your current AI search presence.
|
||||||
|
|
||||||
|
### Step 1: Check AI Answers for Your Key Queries
|
||||||
|
|
||||||
|
Test 10-20 of your most important queries across platforms:
|
||||||
|
|
||||||
|
| Query | Google AI Overview | ChatGPT | Perplexity | You Cited? | Competitors Cited? |
|
||||||
|
|-------|:-----------------:|:-------:|:----------:|:----------:|:-----------------:|
|
||||||
|
| [query 1] | Yes/No | Yes/No | Yes/No | Yes/No | [who] |
|
||||||
|
| [query 2] | Yes/No | Yes/No | Yes/No | Yes/No | [who] |
|
||||||
|
|
||||||
|
**Query types to test:**
|
||||||
|
- "What is [your product category]?"
|
||||||
|
- "Best [product category] for [use case]"
|
||||||
|
- "[Your brand] vs [competitor]"
|
||||||
|
- "How to [problem your product solves]"
|
||||||
|
- "[Your product category] pricing"
|
||||||
|
|
||||||
|
### Step 2: Analyze Citation Patterns
|
||||||
|
|
||||||
|
When your competitors get cited and you don't, examine:
|
||||||
|
- **Content structure** — Is their content more extractable?
|
||||||
|
- **Authority signals** — Do they have more citations, stats, expert quotes?
|
||||||
|
- **Freshness** — Is their content more recently updated?
|
||||||
|
- **Schema markup** — Do they have structured data you're missing?
|
||||||
|
- **Third-party presence** — Are they cited via Wikipedia, Reddit, review sites?
|
||||||
|
|
||||||
|
### Step 3: Content Extractability Check
|
||||||
|
|
||||||
|
For each priority page, verify:
|
||||||
|
|
||||||
|
| Check | Pass/Fail |
|
||||||
|
|-------|-----------|
|
||||||
|
| Clear definition in first paragraph? | |
|
||||||
|
| Self-contained answer blocks (work without surrounding context)? | |
|
||||||
|
| Statistics with sources cited? | |
|
||||||
|
| Comparison tables for "[X] vs [Y]" queries? | |
|
||||||
|
| FAQ section with natural-language questions? | |
|
||||||
|
| Schema markup (FAQ, HowTo, Article, Product)? | |
|
||||||
|
| Expert attribution (author name, credentials)? | |
|
||||||
|
| Recently updated (within 6 months)? | |
|
||||||
|
| Heading structure matches query patterns? | |
|
||||||
|
| AI bots allowed in robots.txt? | |
|
||||||
|
|
||||||
|
### Step 4: AI Bot Access Check
|
||||||
|
|
||||||
|
Verify your robots.txt allows AI crawlers. Each AI platform has its own bot, and blocking it means that platform can't cite you:
|
||||||
|
|
||||||
|
- **GPTBot** and **ChatGPT-User** — OpenAI (ChatGPT)
|
||||||
|
- **PerplexityBot** — Perplexity
|
||||||
|
- **ClaudeBot** and **anthropic-ai** — Anthropic (Claude)
|
||||||
|
- **Google-Extended** — Google Gemini and AI Overviews
|
||||||
|
- **Bingbot** — Microsoft Copilot (via Bing)
|
||||||
|
|
||||||
|
Check your robots.txt for `Disallow` rules targeting any of these. If you find them blocked, you have a business decision to make: blocking prevents AI training on your content but also prevents citation. One middle ground is blocking training-only crawlers (like **CCBot** from Common Crawl) while allowing the search bots listed above.
|
||||||
|
|
||||||
|
See [references/platform-ranking-factors.md](references/platform-ranking-factors.md) for the full robots.txt configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Optimization Strategy
|
||||||
|
|
||||||
|
### The Three Pillars
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Structure (make it extractable)
|
||||||
|
2. Authority (make it citable)
|
||||||
|
3. Presence (be where AI looks)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pillar 1: Structure — Make Content Extractable
|
||||||
|
|
||||||
|
AI systems extract passages, not pages. Every key claim should work as a standalone statement.
|
||||||
|
|
||||||
|
**Content block patterns:**
|
||||||
|
- **Definition blocks** for "What is X?" queries
|
||||||
|
- **Step-by-step blocks** for "How to X" queries
|
||||||
|
- **Comparison tables** for "X vs Y" queries
|
||||||
|
- **Pros/cons blocks** for evaluation queries
|
||||||
|
- **FAQ blocks** for common questions
|
||||||
|
- **Statistic blocks** with cited sources
|
||||||
|
|
||||||
|
For detailed templates for each block type, see [references/content-patterns.md](references/content-patterns.md).
|
||||||
|
|
||||||
|
**Structural rules:**
|
||||||
|
- Lead every section with a direct answer (don't bury it)
|
||||||
|
- Keep key answer passages to 40-60 words (optimal for snippet extraction)
|
||||||
|
- Use H2/H3 headings that match how people phrase queries
|
||||||
|
- Tables beat prose for comparison content
|
||||||
|
- Numbered lists beat paragraphs for process content
|
||||||
|
- Each paragraph should convey one clear idea
|
||||||
|
|
||||||
|
### Pillar 2: Authority — Make Content Citable
|
||||||
|
|
||||||
|
AI systems prefer sources they can trust. Build citation-worthiness.
|
||||||
|
|
||||||
|
**The Princeton GEO research** (KDD 2024, studied across Perplexity.ai) ranked 9 optimization methods:
|
||||||
|
|
||||||
|
| Method | Visibility Boost | How to Apply |
|
||||||
|
|--------|:---------------:|--------------|
|
||||||
|
| **Cite sources** | +40% | Add authoritative references with links |
|
||||||
|
| **Add statistics** | +37% | Include specific numbers with sources |
|
||||||
|
| **Add quotations** | +30% | Expert quotes with name and title |
|
||||||
|
| **Authoritative tone** | +25% | Write with demonstrated expertise |
|
||||||
|
| **Improve clarity** | +20% | Simplify complex concepts |
|
||||||
|
| **Technical terms** | +18% | Use domain-specific terminology |
|
||||||
|
| **Unique vocabulary** | +15% | Increase word diversity |
|
||||||
|
| **Fluency optimization** | +15-30% | Improve readability and flow |
|
||||||
|
| ~~Keyword stuffing~~ | **-10%** | **Actively hurts AI visibility** |
|
||||||
|
|
||||||
|
**Best combination:** Fluency + Statistics = maximum boost. Low-ranking sites benefit even more — up to 115% visibility increase with citations.
|
||||||
|
|
||||||
|
**Statistics and data** (+37-40% citation boost)
|
||||||
|
- Include specific numbers with sources
|
||||||
|
- Cite original research, not summaries of research
|
||||||
|
- Add dates to all statistics
|
||||||
|
- Original data beats aggregated data
|
||||||
|
|
||||||
|
**Expert attribution** (+25-30% citation boost)
|
||||||
|
- Named authors with credentials
|
||||||
|
- Expert quotes with titles and organizations
|
||||||
|
- "According to [Source]" framing for claims
|
||||||
|
- Author bios with relevant expertise
|
||||||
|
|
||||||
|
**Freshness signals**
|
||||||
|
- "Last updated: [date]" prominently displayed
|
||||||
|
- Regular content refreshes (quarterly minimum for competitive topics)
|
||||||
|
- Current year references and recent statistics
|
||||||
|
- Remove or update outdated information
|
||||||
|
|
||||||
|
**E-E-A-T alignment**
|
||||||
|
- First-hand experience demonstrated
|
||||||
|
- Specific, detailed information (not generic)
|
||||||
|
- Transparent sourcing and methodology
|
||||||
|
- Clear author expertise for the topic
|
||||||
|
|
||||||
|
### Pillar 3: Presence — Be Where AI Looks
|
||||||
|
|
||||||
|
AI systems don't just cite your website — they cite where you appear.
|
||||||
|
|
||||||
|
**Third-party sources matter more than your own site:**
|
||||||
|
- Wikipedia mentions (7.8% of all ChatGPT citations)
|
||||||
|
- Reddit discussions (1.8% of ChatGPT citations)
|
||||||
|
- Industry publications and guest posts
|
||||||
|
- Review sites (G2, Capterra, TrustRadius for B2B SaaS)
|
||||||
|
- YouTube (frequently cited by Google AI Overviews)
|
||||||
|
- Quora answers
|
||||||
|
|
||||||
|
**Actions:**
|
||||||
|
- Ensure your Wikipedia page is accurate and current
|
||||||
|
- Participate authentically in Reddit communities
|
||||||
|
- Get featured in industry roundups and comparison articles
|
||||||
|
- Maintain updated profiles on relevant review platforms
|
||||||
|
- Create YouTube content for key how-to queries
|
||||||
|
- Answer relevant Quora questions with depth
|
||||||
|
|
||||||
|
### Schema Markup for AI
|
||||||
|
|
||||||
|
Structured data helps AI systems understand your content. Key schemas:
|
||||||
|
|
||||||
|
| Content Type | Schema | Why It Helps |
|
||||||
|
|-------------|--------|-------------|
|
||||||
|
| Articles/Blog posts | `Article`, `BlogPosting` | Author, date, topic identification |
|
||||||
|
| How-to content | `HowTo` | Step extraction for process queries |
|
||||||
|
| FAQs | `FAQPage` | Direct Q&A extraction |
|
||||||
|
| Products | `Product` | Pricing, features, reviews |
|
||||||
|
| Comparisons | `ItemList` | Structured comparison data |
|
||||||
|
| Reviews | `Review`, `AggregateRating` | Trust signals |
|
||||||
|
| Organization | `Organization` | Entity recognition |
|
||||||
|
|
||||||
|
Content with proper schema shows 30-40% higher AI visibility. For implementation, use the **schema-markup** skill.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Content Types That Get Cited Most
|
||||||
|
|
||||||
|
Not all content is equally citable. Prioritize these formats:
|
||||||
|
|
||||||
|
| Content Type | Citation Share | Why AI Cites It |
|
||||||
|
|-------------|:------------:|----------------|
|
||||||
|
| **Comparison articles** | ~33% | Structured, balanced, high-intent |
|
||||||
|
| **Definitive guides** | ~15% | Comprehensive, authoritative |
|
||||||
|
| **Original research/data** | ~12% | Unique, citable statistics |
|
||||||
|
| **Best-of/listicles** | ~10% | Clear structure, entity-rich |
|
||||||
|
| **Product pages** | ~10% | Specific details AI can extract |
|
||||||
|
| **How-to guides** | ~8% | Step-by-step structure |
|
||||||
|
| **Opinion/analysis** | ~10% | Expert perspective, quotable |
|
||||||
|
|
||||||
|
**Underperformers for AI citation:**
|
||||||
|
- Generic blog posts without structure
|
||||||
|
- Thin product pages with marketing fluff
|
||||||
|
- Gated content (AI can't access it)
|
||||||
|
- Content without dates or author attribution
|
||||||
|
- PDF-only content (harder for AI to parse)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoring AI Visibility
|
||||||
|
|
||||||
|
### What to Track
|
||||||
|
|
||||||
|
| Metric | What It Measures | How to Check |
|
||||||
|
|--------|-----------------|-------------|
|
||||||
|
| AI Overview presence | Do AI Overviews appear for your queries? | Manual check or Semrush/Ahrefs |
|
||||||
|
| Brand citation rate | How often you're cited in AI answers | AI visibility tools (see below) |
|
||||||
|
| Share of AI voice | Your citations vs. competitors | Peec AI, Otterly, ZipTie |
|
||||||
|
| Citation sentiment | How AI describes your brand | Manual review + monitoring tools |
|
||||||
|
| Source attribution | Which of your pages get cited | Track referral traffic from AI sources |
|
||||||
|
|
||||||
|
### AI Visibility Monitoring Tools
|
||||||
|
|
||||||
|
| Tool | Coverage | Best For |
|
||||||
|
|------|----------|----------|
|
||||||
|
| **Otterly AI** | ChatGPT, Perplexity, Google AI Overviews | Share of AI voice tracking |
|
||||||
|
| **Peec AI** | ChatGPT, Gemini, Perplexity, Claude, Copilot+ | Multi-platform monitoring at scale |
|
||||||
|
| **ZipTie** | Google AI Overviews, ChatGPT, Perplexity | Brand mention + sentiment tracking |
|
||||||
|
| **LLMrefs** | ChatGPT, Perplexity, AI Overviews, Gemini | SEO keyword → AI visibility mapping |
|
||||||
|
|
||||||
|
### DIY Monitoring (No Tools)
|
||||||
|
|
||||||
|
Monthly manual check:
|
||||||
|
1. Pick your top 20 queries
|
||||||
|
2. Run each through ChatGPT, Perplexity, and Google
|
||||||
|
3. Record: Are you cited? Who is? What page?
|
||||||
|
4. Log in a spreadsheet, track month-over-month
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI SEO for Different Content Types
|
||||||
|
|
||||||
|
### SaaS Product Pages
|
||||||
|
|
||||||
|
**Goal:** Get cited in "What is [category]?" and "Best [category]" queries.
|
||||||
|
|
||||||
|
**Optimize:**
|
||||||
|
- Clear product description in first paragraph (what it does, who it's for)
|
||||||
|
- Feature comparison tables (you vs. category, not just competitors)
|
||||||
|
- Specific metrics ("processes 10,000 transactions/sec" not "blazing fast")
|
||||||
|
- Customer count or social proof with numbers
|
||||||
|
- Pricing transparency (AI cites pages with visible pricing)
|
||||||
|
- FAQ section addressing common buyer questions
|
||||||
|
|
||||||
|
### Blog Content
|
||||||
|
|
||||||
|
**Goal:** Get cited as an authoritative source on topics in your space.
|
||||||
|
|
||||||
|
**Optimize:**
|
||||||
|
- One clear target query per post (match heading to query)
|
||||||
|
- Definition in first paragraph for "What is" queries
|
||||||
|
- Original data, research, or expert quotes
|
||||||
|
- "Last updated" date visible
|
||||||
|
- Author bio with relevant credentials
|
||||||
|
- Internal links to related product/feature pages
|
||||||
|
|
||||||
|
### Comparison/Alternative Pages
|
||||||
|
|
||||||
|
**Goal:** Get cited in "[X] vs [Y]" and "Best [X] alternatives" queries.
|
||||||
|
|
||||||
|
**Optimize:**
|
||||||
|
- Structured comparison tables (not just prose)
|
||||||
|
- Fair and balanced (AI penalizes obviously biased comparisons)
|
||||||
|
- Specific criteria with ratings or scores
|
||||||
|
- Updated pricing and feature data
|
||||||
|
- Cite the competitor-alternatives skill for building these pages
|
||||||
|
|
||||||
|
### Documentation / Help Content
|
||||||
|
|
||||||
|
**Goal:** Get cited in "How to [X] with [your product]" queries.
|
||||||
|
|
||||||
|
**Optimize:**
|
||||||
|
- Step-by-step format with numbered lists
|
||||||
|
- Code examples where relevant
|
||||||
|
- HowTo schema markup
|
||||||
|
- Screenshots with descriptive alt text
|
||||||
|
- Clear prerequisites and expected outcomes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
- **Ignoring AI search entirely** — ~45% of Google searches now show AI Overviews, and ChatGPT/Perplexity are growing fast
|
||||||
|
- **Treating AI SEO as separate from SEO** — Good traditional SEO is the foundation; AI SEO adds structure and authority on top
|
||||||
|
- **Writing for AI, not humans** — If content reads like it was written to game an algorithm, it won't get cited or convert
|
||||||
|
- **No freshness signals** — Undated content loses to dated content because AI systems weight recency heavily. Show when content was last updated
|
||||||
|
- **Gating all content** — AI can't access gated content. Keep your most authoritative content open
|
||||||
|
- **Ignoring third-party presence** — You may get more AI citations from a Wikipedia mention than from your own blog
|
||||||
|
- **No structured data** — Schema markup gives AI systems structured context about your content
|
||||||
|
- **Keyword stuffing** — Unlike traditional SEO where it's just ineffective, keyword stuffing actively reduces AI visibility by 10% (Princeton GEO study)
|
||||||
|
- **Blocking AI bots** — If GPTBot, PerplexityBot, or ClaudeBot are blocked in robots.txt, those platforms can't cite you
|
||||||
|
- **Generic content without data** — "We're the best" won't get cited. "Our customers see 3x improvement in [metric]" will
|
||||||
|
- **Forgetting to monitor** — You can't improve what you don't measure. Check AI visibility monthly at minimum
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tool Integrations
|
||||||
|
|
||||||
|
For implementation, use the SEO and monitoring tools available in the current environment.
|
||||||
|
|
||||||
|
| Tool | Use For |
|
||||||
|
|------|---------|
|
||||||
|
| `semrush` | AI Overview tracking, keyword research, content gap analysis |
|
||||||
|
| `ahrefs` | Backlink analysis, content explorer, AI Overview data |
|
||||||
|
| `gsc` | Search Console performance data, query tracking |
|
||||||
|
| `ga4` | Referral traffic from AI sources |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task-Specific Questions
|
||||||
|
|
||||||
|
1. What are your top 10-20 most important queries?
|
||||||
|
2. Have you checked if AI answers exist for those queries today?
|
||||||
|
3. Do you have structured data (schema markup) on your site?
|
||||||
|
4. What content types do you publish? (Blog, docs, comparisons, etc.)
|
||||||
|
5. Are competitors being cited by AI where you're not?
|
||||||
|
6. Do you have a Wikipedia page or presence on review sites?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Skills
|
||||||
|
|
||||||
|
- **seo-audit**: For traditional technical and on-page SEO audits
|
||||||
|
- **schema-markup**: For implementing structured data that helps AI understand your content
|
||||||
|
- **content-strategy**: For planning what content to create
|
||||||
|
- **competitor-alternatives**: For building comparison pages that get cited
|
||||||
|
- **programmatic-seo**: For building SEO pages at scale
|
||||||
|
- **copywriting**: For writing content that's both human-readable and AI-extractable
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"skill_name": "ai-seo",
|
||||||
|
"evals": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"prompt": "How do I make sure our SaaS product shows up in AI search results? We're a project management tool and we keep getting left out of ChatGPT and Perplexity recommendations when people ask about project management software.",
|
||||||
|
"expected_output": "Should check for product-marketing-context.md first. Should apply the three pillars framework: Structure (make content extractable), Authority (make content citable), Presence (be where AI looks). Should run through the AI Visibility Audit checklist across platforms (Google AI Overviews, ChatGPT, Perplexity, etc.). Should check content extractability (clear definitions, structured comparisons, statistics). Should reference Princeton GEO research findings (citations improve visibility +40%, statistics +37%). Should check AI bot access in robots.txt. Should provide a prioritized action plan.",
|
||||||
|
"assertions": [
|
||||||
|
"Checks for product-marketing-context.md",
|
||||||
|
"Applies three pillars framework (Structure, Authority, Presence)",
|
||||||
|
"Runs AI Visibility Audit across platforms",
|
||||||
|
"Checks content extractability",
|
||||||
|
"References Princeton GEO research findings",
|
||||||
|
"Checks AI bot access in robots.txt",
|
||||||
|
"Provides prioritized action plan"
|
||||||
|
],
|
||||||
|
"files": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"prompt": "Should we block AI crawlers like GPTBot and PerplexityBot in our robots.txt? We're worried about content theft.",
|
||||||
|
"expected_output": "Should address the AI bot access question directly. Should explain the tradeoff: blocking AI bots prevents training on your content but also prevents AI platforms from citing and recommending you. Should reference the specific bots and their purposes (GPTBot, Google-Extended, PerplexityBot, ClaudeBot, etc.). Should provide the recommended robots.txt configuration. Should explain that blocking may hurt AI visibility more than it protects content. Should provide a nuanced recommendation based on business goals.",
|
||||||
|
"assertions": [
|
||||||
|
"Addresses the blocking tradeoff directly",
|
||||||
|
"Explains impact on AI visibility vs content protection",
|
||||||
|
"Lists specific AI bot user agents",
|
||||||
|
"Provides recommended robots.txt configuration",
|
||||||
|
"Gives nuanced recommendation based on business goals",
|
||||||
|
"Explains what each bot does"
|
||||||
|
],
|
||||||
|
"files": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"prompt": "What kind of content gets cited most by AI systems? We want to create content specifically optimized for AI search.",
|
||||||
|
"expected_output": "Should reference the content types that get cited most, including comparisons (~33% of AI citations), definitive guides (~15%), and other high-citation content types. Should explain why these formats work (they provide the structured, extractable, authoritative information AI systems need). Should provide specific recommendations for creating AI-optimized content: clear definitions, structured data, original statistics, comparison tables, expert quotes. Should reference the Princeton GEO research on what increases citation probability.",
|
||||||
|
"assertions": [
|
||||||
|
"References specific content types with citation rates",
|
||||||
|
"Mentions comparisons as highest-cited format",
|
||||||
|
"Explains why these formats work for AI",
|
||||||
|
"Provides specific content creation recommendations",
|
||||||
|
"References Princeton GEO research",
|
||||||
|
"Mentions structured data, statistics, and clear definitions"
|
||||||
|
],
|
||||||
|
"files": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"prompt": "we noticed our competitors are showing up in google AI overviews but we're not. what do we need to change?",
|
||||||
|
"expected_output": "Should trigger on casual phrasing. Should focus specifically on Google AI Overviews visibility. Should explain how AI Overviews selects sources (authoritative, well-structured, directly answers queries). Should run through the Structure pillar checklist: content extractability, heading hierarchy, answer-first format, structured data. Should check Authority signals: domain authority, citations, E-E-A-T. Should recommend specific content structure changes. Should suggest monitoring approach.",
|
||||||
|
"assertions": [
|
||||||
|
"Triggers on casual phrasing",
|
||||||
|
"Focuses on Google AI Overviews specifically",
|
||||||
|
"Explains how AI Overviews selects sources",
|
||||||
|
"Checks Structure pillar (extractability, headings, answer-first)",
|
||||||
|
"Checks Authority signals",
|
||||||
|
"Recommends specific content structure changes",
|
||||||
|
"Suggests monitoring approach"
|
||||||
|
],
|
||||||
|
"files": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"prompt": "Can you audit our website for AI search readiness? We want to know how visible we are across ChatGPT, Perplexity, Google AI Overviews, and other AI platforms.",
|
||||||
|
"expected_output": "Should run the full AI Visibility Audit. Should check each platform in the landscape (Google AI Overviews, ChatGPT, Perplexity, Claude, Gemini, Copilot). Should evaluate all three pillars: Structure (content extractability, JSON-LD, clear definitions), Authority (citations, backlinks, E-E-A-T signals), Presence (AI bot access, platform-specific factors). Should provide findings organized by pillar. Should provide a prioritized action plan with specific fixes.",
|
||||||
|
"assertions": [
|
||||||
|
"Runs full AI Visibility Audit",
|
||||||
|
"Checks multiple AI platforms",
|
||||||
|
"Evaluates all three pillars (Structure, Authority, Presence)",
|
||||||
|
"Checks content extractability",
|
||||||
|
"Checks AI bot access",
|
||||||
|
"Provides findings organized by pillar",
|
||||||
|
"Provides prioritized action plan"
|
||||||
|
],
|
||||||
|
"files": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"prompt": "Our organic search traffic has dropped 30% this quarter. Can you do a full SEO audit to figure out what's going on?",
|
||||||
|
"expected_output": "Should recognize this is a traditional SEO audit request, not specifically an AI SEO task. Should defer to or cross-reference the seo-audit skill, which handles comprehensive traditional SEO audits including crawlability, technical foundations, on-page optimization, and content quality. May mention AI search as one factor to investigate but should make clear that seo-audit is the primary skill for this task.",
|
||||||
|
"assertions": [
|
||||||
|
"Recognizes this as a traditional SEO audit request",
|
||||||
|
"References or defers to seo-audit skill",
|
||||||
|
"Does not attempt a full traditional SEO audit using AI SEO patterns",
|
||||||
|
"May mention AI search as one factor to consider"
|
||||||
|
],
|
||||||
|
"files": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
# AEO and GEO Content Patterns
|
||||||
|
|
||||||
|
Reusable content block patterns optimized for answer engines and AI citation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
- Answer Engine Optimization (AEO) Patterns (Definition Block, Step-by-Step Block, Comparison Table Block, Pros and Cons Block, FAQ Block, Listicle Block)
|
||||||
|
- Generative Engine Optimization (GEO) Patterns (Statistic Citation Block, Expert Quote Block, Authoritative Claim Block, Self-Contained Answer Block, Evidence Sandwich Block)
|
||||||
|
- Domain-Specific GEO Tactics (Technology Content, Health/Medical Content, Financial Content, Legal Content, Business/Marketing Content)
|
||||||
|
- Voice Search Optimization (Question Formats for Voice, Voice-Optimized Answer Structure)
|
||||||
|
|
||||||
|
## Answer Engine Optimization (AEO) Patterns
|
||||||
|
|
||||||
|
These patterns help content appear in featured snippets, AI Overviews, voice search results, and answer boxes.
|
||||||
|
|
||||||
|
### Definition Block
|
||||||
|
|
||||||
|
Use for "What is [X]?" queries.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## What is [Term]?
|
||||||
|
|
||||||
|
[Term] is [concise 1-sentence definition]. [Expanded 1-2 sentence explanation with key characteristics]. [Brief context on why it matters or how it's used].
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```markdown
|
||||||
|
## What is Answer Engine Optimization?
|
||||||
|
|
||||||
|
Answer Engine Optimization (AEO) is the practice of structuring content so AI-powered systems can easily extract and present it as direct answers to user queries. Unlike traditional SEO that focuses on ranking in search results, AEO optimizes for featured snippets, AI Overviews, and voice assistant responses. This approach has become essential as over 60% of Google searches now end without a click.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step-by-Step Block
|
||||||
|
|
||||||
|
Use for "How to [X]" queries. Optimal for list snippets.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## How to [Action/Goal]
|
||||||
|
|
||||||
|
[1-sentence overview of the process]
|
||||||
|
|
||||||
|
1. **[Step Name]**: [Clear action description in 1-2 sentences]
|
||||||
|
2. **[Step Name]**: [Clear action description in 1-2 sentences]
|
||||||
|
3. **[Step Name]**: [Clear action description in 1-2 sentences]
|
||||||
|
4. **[Step Name]**: [Clear action description in 1-2 sentences]
|
||||||
|
5. **[Step Name]**: [Clear action description in 1-2 sentences]
|
||||||
|
|
||||||
|
[Optional: Brief note on expected outcome or time estimate]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```markdown
|
||||||
|
## How to Optimize Content for Featured Snippets
|
||||||
|
|
||||||
|
Earning featured snippets requires strategic formatting and direct answers to search queries.
|
||||||
|
|
||||||
|
1. **Identify snippet opportunities**: Use tools like Semrush or Ahrefs to find keywords where competitors have snippets you could capture.
|
||||||
|
2. **Match the snippet format**: Analyze whether the current snippet is a paragraph, list, or table, and format your content accordingly.
|
||||||
|
3. **Answer the question directly**: Provide a clear, concise answer (40-60 words for paragraph snippets) immediately after the question heading.
|
||||||
|
4. **Add supporting context**: Expand on your answer with examples, data, and expert insights in the following paragraphs.
|
||||||
|
5. **Use proper heading structure**: Place your target question as an H2 or H3, with the answer immediately following.
|
||||||
|
|
||||||
|
Most featured snippets appear within 2-4 weeks of publishing well-optimized content.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comparison Table Block
|
||||||
|
|
||||||
|
Use for "[X] vs [Y]" queries. Optimal for table snippets.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## [Option A] vs [Option B]: [Brief Descriptor]
|
||||||
|
|
||||||
|
| Feature | [Option A] | [Option B] |
|
||||||
|
|---------|------------|------------|
|
||||||
|
| [Criteria 1] | [Value/Description] | [Value/Description] |
|
||||||
|
| [Criteria 2] | [Value/Description] | [Value/Description] |
|
||||||
|
| [Criteria 3] | [Value/Description] | [Value/Description] |
|
||||||
|
| [Criteria 4] | [Value/Description] | [Value/Description] |
|
||||||
|
| Best For | [Use case] | [Use case] |
|
||||||
|
|
||||||
|
**Bottom line**: [1-2 sentence recommendation based on different needs]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pros and Cons Block
|
||||||
|
|
||||||
|
Use for evaluation queries: "Is [X] worth it?", "Should I [X]?"
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Advantages and Disadvantages of [Topic]
|
||||||
|
|
||||||
|
[1-sentence overview of the evaluation context]
|
||||||
|
|
||||||
|
### Pros
|
||||||
|
|
||||||
|
- **[Benefit category]**: [Specific explanation]
|
||||||
|
- **[Benefit category]**: [Specific explanation]
|
||||||
|
- **[Benefit category]**: [Specific explanation]
|
||||||
|
|
||||||
|
### Cons
|
||||||
|
|
||||||
|
- **[Drawback category]**: [Specific explanation]
|
||||||
|
- **[Drawback category]**: [Specific explanation]
|
||||||
|
- **[Drawback category]**: [Specific explanation]
|
||||||
|
|
||||||
|
**Verdict**: [1-2 sentence balanced conclusion with recommendation]
|
||||||
|
```
|
||||||
|
|
||||||
|
### FAQ Block
|
||||||
|
|
||||||
|
Use for topic pages with multiple common questions. Essential for FAQ schema.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Frequently Asked Questions
|
||||||
|
|
||||||
|
### [Question phrased exactly as users search]?
|
||||||
|
|
||||||
|
[Direct answer in first sentence]. [Supporting context in 2-3 additional sentences].
|
||||||
|
|
||||||
|
### [Question phrased exactly as users search]?
|
||||||
|
|
||||||
|
[Direct answer in first sentence]. [Supporting context in 2-3 additional sentences].
|
||||||
|
|
||||||
|
### [Question phrased exactly as users search]?
|
||||||
|
|
||||||
|
[Direct answer in first sentence]. [Supporting context in 2-3 additional sentences].
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tips for FAQ questions:**
|
||||||
|
- Use natural question phrasing ("How do I..." not "How does one...")
|
||||||
|
- Include question words: what, how, why, when, where, who, which
|
||||||
|
- Match "People Also Ask" queries from search results
|
||||||
|
- Keep answers between 50-100 words
|
||||||
|
|
||||||
|
### Listicle Block
|
||||||
|
|
||||||
|
Use for "Best [X]", "Top [X]", "[Number] ways to [X]" queries.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## [Number] Best [Items] for [Goal/Purpose]
|
||||||
|
|
||||||
|
[1-2 sentence intro establishing context and selection criteria]
|
||||||
|
|
||||||
|
### 1. [Item Name]
|
||||||
|
|
||||||
|
[Why it's included in 2-3 sentences with specific benefits]
|
||||||
|
|
||||||
|
### 2. [Item Name]
|
||||||
|
|
||||||
|
[Why it's included in 2-3 sentences with specific benefits]
|
||||||
|
|
||||||
|
### 3. [Item Name]
|
||||||
|
|
||||||
|
[Why it's included in 2-3 sentences with specific benefits]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Generative Engine Optimization (GEO) Patterns
|
||||||
|
|
||||||
|
These patterns optimize content for citation by AI assistants like ChatGPT, Claude, Perplexity, and Gemini.
|
||||||
|
|
||||||
|
### Statistic Citation Block
|
||||||
|
|
||||||
|
Statistics increase AI citation rates by 15-30%. Always include sources.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
[Claim statement]. According to [Source/Organization], [specific statistic with number and timeframe]. [Context for why this matters].
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```markdown
|
||||||
|
Mobile optimization is no longer optional for SEO success. According to Google's 2024 Core Web Vitals report, 70% of web traffic now comes from mobile devices, and pages failing mobile usability standards see 24% higher bounce rates. This makes mobile-first indexing a critical ranking factor.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expert Quote Block
|
||||||
|
|
||||||
|
Named expert attribution adds credibility and increases citation likelihood.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
"[Direct quote from expert]," says [Expert Name], [Title/Role] at [Organization]. [1 sentence of context or interpretation].
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```markdown
|
||||||
|
"The shift from keyword-driven search to intent-driven discovery represents the most significant change in SEO since mobile-first indexing," says Rand Fishkin, Co-founder of SparkToro. This perspective highlights why content strategies must evolve beyond traditional keyword optimization.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authoritative Claim Block
|
||||||
|
|
||||||
|
Structure claims for easy AI extraction with clear attribution.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
[Topic] [verb: is/has/requires/involves] [clear, specific claim]. [Source] [confirms/reports/found] that [supporting evidence]. This [explains/means/suggests] [implication or action].
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```markdown
|
||||||
|
E-E-A-T is the cornerstone of Google's content quality evaluation. Google's Search Quality Rater Guidelines confirm that trust is the most critical factor, stating that "untrustworthy pages have low E-E-A-T no matter how experienced, expert, or authoritative they may seem." This means content creators must prioritize transparency and accuracy above all other optimization tactics.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Self-Contained Answer Block
|
||||||
|
|
||||||
|
Create quotable, standalone statements that AI can extract directly.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
**[Topic/Question]**: [Complete, self-contained answer that makes sense without additional context. Include specific details, numbers, or examples in 2-3 sentences.]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```markdown
|
||||||
|
**Ideal blog post length for SEO**: The optimal length for SEO blog posts is 1,500-2,500 words for competitive topics. This range allows comprehensive topic coverage while maintaining reader engagement. HubSpot research shows long-form content earns 77% more backlinks than short articles, directly impacting search rankings.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Evidence Sandwich Block
|
||||||
|
|
||||||
|
Structure claims with evidence for maximum credibility.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
[Opening claim statement].
|
||||||
|
|
||||||
|
Evidence supporting this includes:
|
||||||
|
- [Data point 1 with source]
|
||||||
|
- [Data point 2 with source]
|
||||||
|
- [Data point 3 with source]
|
||||||
|
|
||||||
|
[Concluding statement connecting evidence to actionable insight].
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Domain-Specific GEO Tactics
|
||||||
|
|
||||||
|
Different content domains benefit from different authority signals.
|
||||||
|
|
||||||
|
### Technology Content
|
||||||
|
- Emphasize technical precision and correct terminology
|
||||||
|
- Include version numbers and dates for software/tools
|
||||||
|
- Reference official documentation
|
||||||
|
- Add code examples where relevant
|
||||||
|
|
||||||
|
### Health/Medical Content
|
||||||
|
- Cite peer-reviewed studies with publication details
|
||||||
|
- Include expert credentials (MD, RN, etc.)
|
||||||
|
- Note study limitations and context
|
||||||
|
- Add "last reviewed" dates
|
||||||
|
|
||||||
|
### Financial Content
|
||||||
|
- Reference regulatory bodies (SEC, FTC, etc.)
|
||||||
|
- Include specific numbers with timeframes
|
||||||
|
- Note that information is educational, not advice
|
||||||
|
- Cite recognized financial institutions
|
||||||
|
|
||||||
|
### Legal Content
|
||||||
|
- Cite specific laws, statutes, and regulations
|
||||||
|
- Reference jurisdiction clearly
|
||||||
|
- Include professional disclaimers
|
||||||
|
- Note when professional consultation is advised
|
||||||
|
|
||||||
|
### Business/Marketing Content
|
||||||
|
- Include case studies with measurable results
|
||||||
|
- Reference industry research and reports
|
||||||
|
- Add percentage changes and timeframes
|
||||||
|
- Quote recognized thought leaders
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Voice Search Optimization
|
||||||
|
|
||||||
|
Voice queries are conversational and question-based. Optimize for these patterns:
|
||||||
|
|
||||||
|
### Question Formats for Voice
|
||||||
|
- "What is..."
|
||||||
|
- "How do I..."
|
||||||
|
- "Where can I find..."
|
||||||
|
- "Why does..."
|
||||||
|
- "When should I..."
|
||||||
|
- "Who is..."
|
||||||
|
|
||||||
|
### Voice-Optimized Answer Structure
|
||||||
|
- Lead with direct answer (under 30 words ideal)
|
||||||
|
- Use natural, conversational language
|
||||||
|
- Avoid jargon unless targeting expert audience
|
||||||
|
- Include local context where relevant
|
||||||
|
- Structure for single spoken response
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
# How Each AI Platform Picks Sources
|
||||||
|
|
||||||
|
Each AI search platform has its own search index, ranking logic, and content preferences. This guide covers what matters for getting cited on each one.
|
||||||
|
|
||||||
|
Sources cited throughout: Princeton GEO study (KDD 2024), SE Ranking domain authority study, ZipTie content-answer fit analysis.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Fundamentals
|
||||||
|
|
||||||
|
Every AI platform shares three baseline requirements:
|
||||||
|
|
||||||
|
1. **Your content must be in their index** — Each platform uses a different search backend (Google, Bing, Brave, or their own). If you're not indexed, you can't be cited.
|
||||||
|
2. **Your content must be crawlable** — AI bots need access via robots.txt. Block the bot, lose the citation.
|
||||||
|
3. **Your content must be extractable** — AI systems pull passages, not pages. Clear structure and self-contained paragraphs win.
|
||||||
|
|
||||||
|
Beyond these basics, each platform weights different signals. Here's what matters and where.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Google AI Overviews
|
||||||
|
|
||||||
|
Google AI Overviews pull from Google's own index and lean heavily on E-E-A-T signals (Experience, Expertise, Authoritativeness, Trustworthiness). They appear in roughly 45% of Google searches.
|
||||||
|
|
||||||
|
**What makes Google AI Overviews different:** They already have your traditional SEO signals — backlinks, page authority, topical relevance. The additional AI layer adds a preference for content with cited sources and structured data. Research shows that including authoritative citations in your content correlates with a 132% visibility boost, and writing with an authoritative (not salesy) tone adds another 89%.
|
||||||
|
|
||||||
|
**Importantly, AI Overviews don't just recycle the traditional Top 10.** Only about 15% of AI Overview sources overlap with conventional organic results. Pages that wouldn't crack page 1 in traditional search can still get cited if they have strong structured data and clear, extractable answers.
|
||||||
|
|
||||||
|
**What to focus on:**
|
||||||
|
- Schema markup is the single biggest lever — Article, FAQPage, HowTo, and Product schemas give AI Overviews structured context to work with (30-40% visibility boost)
|
||||||
|
- Build topical authority through content clusters with strong internal linking
|
||||||
|
- Include named, sourced citations in your content (not just claims)
|
||||||
|
- Author bios with real credentials matter — E-E-A-T is weighted heavily
|
||||||
|
- Get into Google's Knowledge Graph where possible (an accurate Wikipedia entry helps)
|
||||||
|
- Target "how to" and "what is" query patterns — these trigger AI Overviews most often
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ChatGPT
|
||||||
|
|
||||||
|
ChatGPT's web search draws from a Bing-based index. It combines this with its training knowledge to generate answers, then cites the web sources it relied on.
|
||||||
|
|
||||||
|
**What makes ChatGPT different:** Domain authority matters more here than on other AI platforms. An SE Ranking analysis of 129,000 domains found that authority and credibility signals account for roughly 40% of what determines citation, with content quality at about 35% and platform trust at 25%. Sites with very high referring domain counts (350K+) average 8.4 citations per response, while sites with slightly lower trust scores (91-96 vs 97-100) drop from 8.4 to 6 citations.
|
||||||
|
|
||||||
|
**Freshness is a major differentiator.** Content updated within the last 30 days gets cited about 3.2x more often than older content. ChatGPT clearly favors recent information.
|
||||||
|
|
||||||
|
**The most important signal is content-answer fit** — a ZipTie analysis of 400,000 pages found that how well your content's style and structure matches ChatGPT's own response format accounts for about 55% of citation likelihood. This is far more important than domain authority (12%) or on-page structure (14%) alone. Write the way ChatGPT would answer the question, and you're more likely to be the source it cites.
|
||||||
|
|
||||||
|
**Where ChatGPT looks beyond your site:** Wikipedia accounts for 7.8% of all ChatGPT citations, Reddit for 1.8%, and Forbes for 1.1%. Brand official sites are cited frequently but third-party mentions carry significant weight.
|
||||||
|
|
||||||
|
**What to focus on:**
|
||||||
|
- Invest in backlinks and domain authority — it's the strongest baseline signal
|
||||||
|
- Update competitive content at least monthly
|
||||||
|
- Structure your content the way ChatGPT structures its answers (conversational, direct, well-organized)
|
||||||
|
- Include verifiable statistics with named sources
|
||||||
|
- Clean heading hierarchy (H1 > H2 > H3) with descriptive headings
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Perplexity
|
||||||
|
|
||||||
|
Perplexity always cites its sources with clickable links, making it the most transparent AI search platform. It combines its own index with Google's and runs results through multiple reranking passes — initial relevance retrieval, then traditional ranking factor scoring, then ML-based quality evaluation that can discard entire result sets if they don't meet quality thresholds.
|
||||||
|
|
||||||
|
**What makes Perplexity different:** It's the most "research-oriented" AI search engine, and its citation behavior reflects that. Perplexity maintains curated lists of authoritative domains (Amazon, GitHub, major academic sites) that get inherent ranking boosts. It uses a time-decay algorithm that evaluates new content quickly, giving fresh publishers a real shot at citation.
|
||||||
|
|
||||||
|
**Perplexity has unique content preferences:**
|
||||||
|
- **FAQ Schema (JSON-LD)** — Pages with FAQ structured data get cited noticeably more often
|
||||||
|
- **PDF documents** — Publicly accessible PDFs (whitepapers, research reports) are prioritized. If you have authoritative PDF content gated behind a form, consider making a version public.
|
||||||
|
- **Publishing velocity** — How frequently you publish matters more than keyword targeting
|
||||||
|
- **Self-contained paragraphs** — Perplexity prefers atomic, semantically complete paragraphs it can extract cleanly
|
||||||
|
|
||||||
|
**What to focus on:**
|
||||||
|
- Allow PerplexityBot in robots.txt
|
||||||
|
- Implement FAQPage schema on any page with Q&A content
|
||||||
|
- Host PDF resources publicly (whitepapers, guides, reports)
|
||||||
|
- Add Article schema with publication and modification timestamps
|
||||||
|
- Write in clear, self-contained paragraphs that work as standalone answers
|
||||||
|
- Build deep topical authority in your specific niche
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Microsoft Copilot
|
||||||
|
|
||||||
|
Copilot is embedded across Microsoft's ecosystem — Edge, Windows, Microsoft 365, and Bing Search. It relies entirely on Bing's index, so if Bing hasn't indexed your content, Copilot can't cite it.
|
||||||
|
|
||||||
|
**What makes Copilot different:** The Microsoft ecosystem connection creates unique optimization opportunities. Mentions and content on LinkedIn and GitHub provide ranking boosts that other platforms don't offer. Copilot also puts more weight on page speed — sub-2-second load times are a clear threshold.
|
||||||
|
|
||||||
|
**What to focus on:**
|
||||||
|
- Submit your site to Bing Webmaster Tools (many sites only submit to Google Search Console)
|
||||||
|
- Use IndexNow protocol for faster indexing of new and updated content
|
||||||
|
- Optimize page speed to under 2 seconds
|
||||||
|
- Write clear entity definitions — when your content defines a term or concept, make the definition explicit and extractable
|
||||||
|
- Build presence on LinkedIn (publish articles, maintain company page) and GitHub if relevant
|
||||||
|
- Ensure Bingbot has full crawl access
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Claude
|
||||||
|
|
||||||
|
Claude uses Brave Search as its search backend when web search is enabled — not Google, not Bing. This is a completely different index, which means your Brave Search visibility directly determines whether Claude can find and cite you.
|
||||||
|
|
||||||
|
**What makes Claude different:** Claude is extremely selective about what it cites. While it processes enormous amounts of content, its citation rate is very low — it's looking for the most factually accurate, well-sourced content on a given topic. Data-rich content with specific numbers and clear attribution performs significantly better than general-purpose content.
|
||||||
|
|
||||||
|
**What to focus on:**
|
||||||
|
- Verify your content appears in Brave Search results (search for your brand and key terms at search.brave.com)
|
||||||
|
- Allow ClaudeBot and anthropic-ai user agents in robots.txt
|
||||||
|
- Maximize factual density — specific numbers, named sources, dated statistics
|
||||||
|
- Use clear, extractable structure with descriptive headings
|
||||||
|
- Cite authoritative sources within your content
|
||||||
|
- Aim to be the most factually accurate source on your topic — Claude rewards precision
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Allowing AI Bots in robots.txt
|
||||||
|
|
||||||
|
If your robots.txt blocks an AI bot, that platform can't cite your content. Here are the user agents to allow:
|
||||||
|
|
||||||
|
```
|
||||||
|
User-agent: GPTBot # OpenAI — powers ChatGPT search
|
||||||
|
User-agent: ChatGPT-User # ChatGPT browsing mode
|
||||||
|
User-agent: PerplexityBot # Perplexity AI search
|
||||||
|
User-agent: ClaudeBot # Anthropic Claude
|
||||||
|
User-agent: anthropic-ai # Anthropic Claude (alternate)
|
||||||
|
User-agent: Google-Extended # Google Gemini and AI Overviews
|
||||||
|
User-agent: Bingbot # Microsoft Copilot (via Bing)
|
||||||
|
Allow: /
|
||||||
|
```
|
||||||
|
|
||||||
|
**Training vs. search:** Some AI bots are used for both model training and search citation. If you want to be cited but don't want your content used for training, your options are limited — GPTBot handles both for OpenAI. However, you can safely block **CCBot** (Common Crawl) without affecting any AI search citations, since it's only used for training dataset collection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Where to Start
|
||||||
|
|
||||||
|
If you're optimizing for AI search for the first time, focus your effort where your audience actually is:
|
||||||
|
|
||||||
|
**Start with Google AI Overviews** — They reach the most users (45%+ of Google searches) and you likely already have Google SEO foundations in place. Add schema markup, include cited sources in your content, and strengthen E-E-A-T signals.
|
||||||
|
|
||||||
|
**Then address ChatGPT** — It's the most-used standalone AI search tool for tech and business audiences. Focus on freshness (update content monthly), domain authority, and matching your content structure to how ChatGPT formats its responses.
|
||||||
|
|
||||||
|
**Then expand to Perplexity** — Especially valuable if your audience includes researchers, early adopters, or tech professionals. Add FAQ schema, publish PDF resources, and write in clear, self-contained paragraphs.
|
||||||
|
|
||||||
|
**Copilot and Claude are lower priority** unless your audience skews enterprise/Microsoft (Copilot) or developer/analyst (Claude). But the fundamentals — structured content, cited sources, schema markup — help across all platforms.
|
||||||
|
|
||||||
|
**Actions that help everywhere:**
|
||||||
|
1. Allow all AI bots in robots.txt
|
||||||
|
2. Implement schema markup (FAQPage, Article, Organization at minimum)
|
||||||
|
3. Include statistics with named sources in your content
|
||||||
|
4. Update content regularly — monthly for competitive topics
|
||||||
|
5. Use clear heading structure (H1 > H2 > H3)
|
||||||
|
6. Keep page load time under 2 seconds
|
||||||
|
7. Add author bios with credentials
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
name: api-design-principles
|
||||||
|
description: "Master REST and GraphQL API design principles to build intuitive, scalable, and maintainable APIs that delight developers and stand the test of time."
|
||||||
|
risk: safe
|
||||||
|
source: community
|
||||||
|
date_added: "2026-02-27"
|
||||||
|
---
|
||||||
|
|
||||||
|
# API Design Principles
|
||||||
|
|
||||||
|
Master REST and GraphQL API design principles to build intuitive, scalable, and maintainable APIs that delight developers and stand the test of time.
|
||||||
|
|
||||||
|
## Use this skill when
|
||||||
|
|
||||||
|
- Designing new REST or GraphQL APIs
|
||||||
|
- Refactoring existing APIs for better usability
|
||||||
|
- Establishing API design standards for your team
|
||||||
|
- Reviewing API specifications before implementation
|
||||||
|
- Migrating between API paradigms (REST to GraphQL, etc.)
|
||||||
|
- Creating developer-friendly API documentation
|
||||||
|
- Optimizing APIs for specific use cases (mobile, third-party integrations)
|
||||||
|
|
||||||
|
## Do not use this skill when
|
||||||
|
|
||||||
|
- You only need implementation guidance for a specific framework
|
||||||
|
- You are doing infrastructure-only work without API contracts
|
||||||
|
- You cannot change or version public interfaces
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. Define consumers, use cases, and constraints.
|
||||||
|
2. Choose API style and model resources or types.
|
||||||
|
3. Specify errors, versioning, pagination, and auth strategy.
|
||||||
|
4. Validate with examples and review for consistency.
|
||||||
|
|
||||||
|
Refer to `resources/implementation-playbook.md` for detailed patterns, checklists, and templates.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- `resources/implementation-playbook.md` for detailed patterns, checklists, and templates.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
+155
@@ -0,0 +1,155 @@
|
|||||||
|
# API Design Checklist
|
||||||
|
|
||||||
|
## Pre-Implementation Review
|
||||||
|
|
||||||
|
### Resource Design
|
||||||
|
|
||||||
|
- [ ] Resources are nouns, not verbs
|
||||||
|
- [ ] Plural names for collections
|
||||||
|
- [ ] Consistent naming across all endpoints
|
||||||
|
- [ ] Clear resource hierarchy (avoid deep nesting >2 levels)
|
||||||
|
- [ ] All CRUD operations properly mapped to HTTP methods
|
||||||
|
|
||||||
|
### HTTP Methods
|
||||||
|
|
||||||
|
- [ ] GET for retrieval (safe, idempotent)
|
||||||
|
- [ ] POST for creation
|
||||||
|
- [ ] PUT for full replacement (idempotent)
|
||||||
|
- [ ] PATCH for partial updates
|
||||||
|
- [ ] DELETE for removal (idempotent)
|
||||||
|
|
||||||
|
### Status Codes
|
||||||
|
|
||||||
|
- [ ] 200 OK for successful GET/PATCH/PUT
|
||||||
|
- [ ] 201 Created for POST
|
||||||
|
- [ ] 204 No Content for DELETE
|
||||||
|
- [ ] 400 Bad Request for malformed requests
|
||||||
|
- [ ] 401 Unauthorized for missing auth
|
||||||
|
- [ ] 403 Forbidden for insufficient permissions
|
||||||
|
- [ ] 404 Not Found for missing resources
|
||||||
|
- [ ] 422 Unprocessable Entity for validation errors
|
||||||
|
- [ ] 429 Too Many Requests for rate limiting
|
||||||
|
- [ ] 500 Internal Server Error for server issues
|
||||||
|
|
||||||
|
### Pagination
|
||||||
|
|
||||||
|
- [ ] All collection endpoints paginated
|
||||||
|
- [ ] Default page size defined (e.g., 20)
|
||||||
|
- [ ] Maximum page size enforced (e.g., 100)
|
||||||
|
- [ ] Pagination metadata included (total, pages, etc.)
|
||||||
|
- [ ] Cursor-based or offset-based pattern chosen
|
||||||
|
|
||||||
|
### Filtering & Sorting
|
||||||
|
|
||||||
|
- [ ] Query parameters for filtering
|
||||||
|
- [ ] Sort parameter supported
|
||||||
|
- [ ] Search parameter for full-text search
|
||||||
|
- [ ] Field selection supported (sparse fieldsets)
|
||||||
|
|
||||||
|
### Versioning
|
||||||
|
|
||||||
|
- [ ] Versioning strategy defined (URL/header/query)
|
||||||
|
- [ ] Version included in all endpoints
|
||||||
|
- [ ] Deprecation policy documented
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
- [ ] Consistent error response format
|
||||||
|
- [ ] Detailed error messages
|
||||||
|
- [ ] Field-level validation errors
|
||||||
|
- [ ] Error codes for client handling
|
||||||
|
- [ ] Timestamps in error responses
|
||||||
|
|
||||||
|
### Authentication & Authorization
|
||||||
|
|
||||||
|
- [ ] Authentication method defined (Bearer token, API key)
|
||||||
|
- [ ] Authorization checks on all endpoints
|
||||||
|
- [ ] 401 vs 403 used correctly
|
||||||
|
- [ ] Token expiration handled
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
|
|
||||||
|
- [ ] Rate limits defined per endpoint/user
|
||||||
|
- [ ] Rate limit headers included
|
||||||
|
- [ ] 429 status code for exceeded limits
|
||||||
|
- [ ] Retry-After header provided
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- [ ] OpenAPI/Swagger spec generated
|
||||||
|
- [ ] All endpoints documented
|
||||||
|
- [ ] Request/response examples provided
|
||||||
|
- [ ] Error responses documented
|
||||||
|
- [ ] Authentication flow documented
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [ ] Unit tests for business logic
|
||||||
|
- [ ] Integration tests for endpoints
|
||||||
|
- [ ] Error scenarios tested
|
||||||
|
- [ ] Edge cases covered
|
||||||
|
- [ ] Performance tests for heavy endpoints
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- [ ] Input validation on all fields
|
||||||
|
- [ ] SQL injection prevention
|
||||||
|
- [ ] XSS prevention
|
||||||
|
- [ ] CORS configured correctly
|
||||||
|
- [ ] HTTPS enforced
|
||||||
|
- [ ] Sensitive data not in URLs
|
||||||
|
- [ ] No secrets in responses
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- [ ] Database queries optimized
|
||||||
|
- [ ] N+1 queries prevented
|
||||||
|
- [ ] Caching strategy defined
|
||||||
|
- [ ] Cache headers set appropriately
|
||||||
|
- [ ] Large responses paginated
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
|
||||||
|
- [ ] Logging implemented
|
||||||
|
- [ ] Error tracking configured
|
||||||
|
- [ ] Performance metrics collected
|
||||||
|
- [ ] Health check endpoint available
|
||||||
|
- [ ] Alerts configured for errors
|
||||||
|
|
||||||
|
## GraphQL-Specific Checks
|
||||||
|
|
||||||
|
### Schema Design
|
||||||
|
|
||||||
|
- [ ] Schema-first approach used
|
||||||
|
- [ ] Types properly defined
|
||||||
|
- [ ] Non-null vs nullable decided
|
||||||
|
- [ ] Interfaces/unions used appropriately
|
||||||
|
- [ ] Custom scalars defined
|
||||||
|
|
||||||
|
### Queries
|
||||||
|
|
||||||
|
- [ ] Query depth limiting
|
||||||
|
- [ ] Query complexity analysis
|
||||||
|
- [ ] DataLoaders prevent N+1
|
||||||
|
- [ ] Pagination pattern chosen (Relay/offset)
|
||||||
|
|
||||||
|
### Mutations
|
||||||
|
|
||||||
|
- [ ] Input types defined
|
||||||
|
- [ ] Payload types with errors
|
||||||
|
- [ ] Optimistic response support
|
||||||
|
- [ ] Idempotency considered
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- [ ] DataLoader for all relationships
|
||||||
|
- [ ] Query batching enabled
|
||||||
|
- [ ] Persisted queries considered
|
||||||
|
- [ ] Response caching implemented
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- [ ] All fields documented
|
||||||
|
- [ ] Deprecations marked
|
||||||
|
- [ ] Examples provided
|
||||||
|
- [ ] Schema introspection enabled
|
||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
"""
|
||||||
|
Production-ready REST API template using FastAPI.
|
||||||
|
Includes pagination, filtering, error handling, and best practices.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException, Query, Path, Depends, status
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from pydantic import BaseModel, Field, EmailStr, ConfigDict
|
||||||
|
from typing import Optional, List, Any
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="API Template",
|
||||||
|
version="1.0.0",
|
||||||
|
docs_url="/api/docs"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Security Middleware
|
||||||
|
# Trusted Host: Prevents HTTP Host Header attacks
|
||||||
|
app.add_middleware(
|
||||||
|
TrustedHostMiddleware,
|
||||||
|
allowed_hosts=["*"] # TODO: Configure this in production, e.g. ["api.example.com"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# CORS: Configures Cross-Origin Resource Sharing
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # TODO: Update this with specific origins in production
|
||||||
|
allow_credentials=False, # TODO: Set to True if you need cookies/auth headers, but restrict origins
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Models
|
||||||
|
class UserStatus(str, Enum):
|
||||||
|
ACTIVE = "active"
|
||||||
|
INACTIVE = "inactive"
|
||||||
|
SUSPENDED = "suspended"
|
||||||
|
|
||||||
|
class UserBase(BaseModel):
|
||||||
|
email: EmailStr
|
||||||
|
name: str = Field(..., min_length=1, max_length=100)
|
||||||
|
status: UserStatus = UserStatus.ACTIVE
|
||||||
|
|
||||||
|
class UserCreate(UserBase):
|
||||||
|
password: str = Field(..., min_length=8)
|
||||||
|
|
||||||
|
class UserUpdate(BaseModel):
|
||||||
|
email: Optional[EmailStr] = None
|
||||||
|
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||||
|
status: Optional[UserStatus] = None
|
||||||
|
|
||||||
|
class User(UserBase):
|
||||||
|
id: str
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
# Pagination
|
||||||
|
class PaginationParams(BaseModel):
|
||||||
|
page: int = Field(1, ge=1)
|
||||||
|
page_size: int = Field(20, ge=1, le=100)
|
||||||
|
|
||||||
|
class PaginatedResponse(BaseModel):
|
||||||
|
items: List[Any]
|
||||||
|
total: int
|
||||||
|
page: int
|
||||||
|
page_size: int
|
||||||
|
pages: int
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
class ErrorDetail(BaseModel):
|
||||||
|
field: Optional[str] = None
|
||||||
|
message: str
|
||||||
|
code: str
|
||||||
|
|
||||||
|
class ErrorResponse(BaseModel):
|
||||||
|
error: str
|
||||||
|
message: str
|
||||||
|
details: Optional[List[ErrorDetail]] = None
|
||||||
|
|
||||||
|
@app.exception_handler(HTTPException)
|
||||||
|
async def http_exception_handler(request, exc):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content=ErrorResponse(
|
||||||
|
error=exc.__class__.__name__,
|
||||||
|
message=exc.detail if isinstance(exc.detail, str) else exc.detail.get("message", "Error"),
|
||||||
|
details=exc.detail.get("details") if isinstance(exc.detail, dict) else None
|
||||||
|
).model_dump()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Endpoints
|
||||||
|
@app.get("/api/users", response_model=PaginatedResponse, tags=["Users"])
|
||||||
|
async def list_users(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(20, ge=1, le=100),
|
||||||
|
status: Optional[UserStatus] = Query(None),
|
||||||
|
search: Optional[str] = Query(None)
|
||||||
|
):
|
||||||
|
"""List users with pagination and filtering."""
|
||||||
|
# Mock implementation
|
||||||
|
total = 100
|
||||||
|
items = [
|
||||||
|
User(
|
||||||
|
id=str(i),
|
||||||
|
email=f"user{i}@example.com",
|
||||||
|
name=f"User {i}",
|
||||||
|
status=UserStatus.ACTIVE,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
).model_dump()
|
||||||
|
for i in range((page-1)*page_size, min(page*page_size, total))
|
||||||
|
]
|
||||||
|
|
||||||
|
return PaginatedResponse(
|
||||||
|
items=items,
|
||||||
|
total=total,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
pages=(total + page_size - 1) // page_size
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post("/api/users", response_model=User, status_code=status.HTTP_201_CREATED, tags=["Users"])
|
||||||
|
async def create_user(user: UserCreate):
|
||||||
|
"""Create a new user."""
|
||||||
|
# Mock implementation
|
||||||
|
return User(
|
||||||
|
id="123",
|
||||||
|
email=user.email,
|
||||||
|
name=user.name,
|
||||||
|
status=user.status,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/api/users/{user_id}", response_model=User, tags=["Users"])
|
||||||
|
async def get_user(user_id: str = Path(..., description="User ID")):
|
||||||
|
"""Get user by ID."""
|
||||||
|
# Mock: Check if exists
|
||||||
|
if user_id == "999":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail={"message": "User not found", "details": {"id": user_id}}
|
||||||
|
)
|
||||||
|
|
||||||
|
return User(
|
||||||
|
id=user_id,
|
||||||
|
email="user@example.com",
|
||||||
|
name="User Name",
|
||||||
|
status=UserStatus.ACTIVE,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.patch("/api/users/{user_id}", response_model=User, tags=["Users"])
|
||||||
|
async def update_user(user_id: str, update: UserUpdate):
|
||||||
|
"""Partially update user."""
|
||||||
|
# Validate user exists
|
||||||
|
existing = await get_user(user_id)
|
||||||
|
|
||||||
|
# Apply updates
|
||||||
|
update_data = update.model_dump(exclude_unset=True)
|
||||||
|
for field, value in update_data.items():
|
||||||
|
setattr(existing, field, value)
|
||||||
|
|
||||||
|
existing.updated_at = datetime.now()
|
||||||
|
return existing
|
||||||
|
|
||||||
|
@app.delete("/api/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Users"])
|
||||||
|
async def delete_user(user_id: str):
|
||||||
|
"""Delete user."""
|
||||||
|
await get_user(user_id) # Verify exists
|
||||||
|
return None
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
+583
@@ -0,0 +1,583 @@
|
|||||||
|
# GraphQL Schema Design Patterns
|
||||||
|
|
||||||
|
## Schema Organization
|
||||||
|
|
||||||
|
### Modular Schema Structure
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
# user.graphql
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
email: String!
|
||||||
|
name: String!
|
||||||
|
posts: [Post!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
extend type Query {
|
||||||
|
user(id: ID!): User
|
||||||
|
users(first: Int, after: String): UserConnection!
|
||||||
|
}
|
||||||
|
|
||||||
|
extend type Mutation {
|
||||||
|
createUser(input: CreateUserInput!): CreateUserPayload!
|
||||||
|
}
|
||||||
|
|
||||||
|
# post.graphql
|
||||||
|
type Post {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
content: String!
|
||||||
|
author: User!
|
||||||
|
}
|
||||||
|
|
||||||
|
extend type Query {
|
||||||
|
post(id: ID!): Post
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Design Patterns
|
||||||
|
|
||||||
|
### 1. Non-Null Types
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type User {
|
||||||
|
id: ID! # Always required
|
||||||
|
email: String! # Required
|
||||||
|
phone: String # Optional (nullable)
|
||||||
|
posts: [Post!]! # Non-null array of non-null posts
|
||||||
|
tags: [String!] # Nullable array of non-null strings
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Interfaces for Polymorphism
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
interface Node {
|
||||||
|
id: ID!
|
||||||
|
createdAt: DateTime!
|
||||||
|
}
|
||||||
|
|
||||||
|
type User implements Node {
|
||||||
|
id: ID!
|
||||||
|
createdAt: DateTime!
|
||||||
|
email: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Post implements Node {
|
||||||
|
id: ID!
|
||||||
|
createdAt: DateTime!
|
||||||
|
title: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
node(id: ID!): Node
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Unions for Heterogeneous Results
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
union SearchResult = User | Post | Comment
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
search(query: String!): [SearchResult!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Query example
|
||||||
|
{
|
||||||
|
search(query: "graphql") {
|
||||||
|
... on User {
|
||||||
|
name
|
||||||
|
email
|
||||||
|
}
|
||||||
|
... on Post {
|
||||||
|
title
|
||||||
|
content
|
||||||
|
}
|
||||||
|
... on Comment {
|
||||||
|
text
|
||||||
|
author {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Input Types
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
input CreateUserInput {
|
||||||
|
email: String!
|
||||||
|
name: String!
|
||||||
|
password: String!
|
||||||
|
profileInput: ProfileInput
|
||||||
|
}
|
||||||
|
|
||||||
|
input ProfileInput {
|
||||||
|
bio: String
|
||||||
|
avatar: String
|
||||||
|
website: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateUserInput {
|
||||||
|
id: ID!
|
||||||
|
email: String
|
||||||
|
name: String
|
||||||
|
profileInput: ProfileInput
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pagination Patterns
|
||||||
|
|
||||||
|
### Relay Cursor Pagination (Recommended)
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type UserConnection {
|
||||||
|
edges: [UserEdge!]!
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
totalCount: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserEdge {
|
||||||
|
node: User!
|
||||||
|
cursor: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageInfo {
|
||||||
|
hasNextPage: Boolean!
|
||||||
|
hasPreviousPage: Boolean!
|
||||||
|
startCursor: String
|
||||||
|
endCursor: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
users(first: Int, after: String, last: Int, before: String): UserConnection!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
{
|
||||||
|
users(first: 10, after: "cursor123") {
|
||||||
|
edges {
|
||||||
|
cursor
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Offset Pagination (Simpler)
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type UserList {
|
||||||
|
items: [User!]!
|
||||||
|
total: Int!
|
||||||
|
page: Int!
|
||||||
|
pageSize: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
users(page: Int = 1, pageSize: Int = 20): UserList!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mutation Design Patterns
|
||||||
|
|
||||||
|
### 1. Input/Payload Pattern
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
input CreatePostInput {
|
||||||
|
title: String!
|
||||||
|
content: String!
|
||||||
|
tags: [String!]
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatePostPayload {
|
||||||
|
post: Post
|
||||||
|
errors: [Error!]
|
||||||
|
success: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error {
|
||||||
|
field: String
|
||||||
|
message: String!
|
||||||
|
code: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
createPost(input: CreatePostInput!): CreatePostPayload!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Optimistic Response Support
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type UpdateUserPayload {
|
||||||
|
user: User
|
||||||
|
clientMutationId: String
|
||||||
|
errors: [Error!]
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateUserInput {
|
||||||
|
id: ID!
|
||||||
|
name: String
|
||||||
|
clientMutationId: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
updateUser(input: UpdateUserInput!): UpdateUserPayload!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Batch Mutations
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
input BatchCreateUserInput {
|
||||||
|
users: [CreateUserInput!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type BatchCreateUserPayload {
|
||||||
|
results: [CreateUserResult!]!
|
||||||
|
successCount: Int!
|
||||||
|
errorCount: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateUserResult {
|
||||||
|
user: User
|
||||||
|
errors: [Error!]
|
||||||
|
index: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
batchCreateUsers(input: BatchCreateUserInput!): BatchCreateUserPayload!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Field Design
|
||||||
|
|
||||||
|
### Arguments and Filtering
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type Query {
|
||||||
|
posts(
|
||||||
|
# Pagination
|
||||||
|
first: Int = 20
|
||||||
|
after: String
|
||||||
|
|
||||||
|
# Filtering
|
||||||
|
status: PostStatus
|
||||||
|
authorId: ID
|
||||||
|
tag: String
|
||||||
|
|
||||||
|
# Sorting
|
||||||
|
orderBy: PostOrderBy = CREATED_AT
|
||||||
|
orderDirection: OrderDirection = DESC
|
||||||
|
|
||||||
|
# Searching
|
||||||
|
search: String
|
||||||
|
): PostConnection!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PostStatus {
|
||||||
|
DRAFT
|
||||||
|
PUBLISHED
|
||||||
|
ARCHIVED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PostOrderBy {
|
||||||
|
CREATED_AT
|
||||||
|
UPDATED_AT
|
||||||
|
TITLE
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderDirection {
|
||||||
|
ASC
|
||||||
|
DESC
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Computed Fields
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type User {
|
||||||
|
firstName: String!
|
||||||
|
lastName: String!
|
||||||
|
fullName: String! # Computed in resolver
|
||||||
|
posts: [Post!]!
|
||||||
|
postCount: Int! # Computed, doesn't load all posts
|
||||||
|
}
|
||||||
|
|
||||||
|
type Post {
|
||||||
|
likeCount: Int!
|
||||||
|
commentCount: Int!
|
||||||
|
isLikedByViewer: Boolean! # Context-dependent
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subscriptions
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type Subscription {
|
||||||
|
postAdded: Post!
|
||||||
|
|
||||||
|
postUpdated(postId: ID!): Post!
|
||||||
|
|
||||||
|
userStatusChanged(userId: ID!): UserStatus!
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserStatus {
|
||||||
|
userId: ID!
|
||||||
|
online: Boolean!
|
||||||
|
lastSeen: DateTime!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Client usage
|
||||||
|
subscription {
|
||||||
|
postAdded {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
author {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Scalars
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
scalar DateTime
|
||||||
|
scalar Email
|
||||||
|
scalar URL
|
||||||
|
scalar JSON
|
||||||
|
scalar Money
|
||||||
|
|
||||||
|
type User {
|
||||||
|
email: Email!
|
||||||
|
website: URL
|
||||||
|
createdAt: DateTime!
|
||||||
|
metadata: JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
type Product {
|
||||||
|
price: Money!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directives
|
||||||
|
|
||||||
|
### Built-in Directives
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type User {
|
||||||
|
name: String!
|
||||||
|
email: String! @deprecated(reason: "Use emails field instead")
|
||||||
|
emails: [String!]!
|
||||||
|
|
||||||
|
# Conditional inclusion
|
||||||
|
privateData: PrivateData @include(if: $isOwner)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Query
|
||||||
|
query GetUser($isOwner: Boolean!) {
|
||||||
|
user(id: "123") {
|
||||||
|
name
|
||||||
|
privateData @include(if: $isOwner) {
|
||||||
|
ssn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Directives
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
directive @auth(requires: Role = USER) on FIELD_DEFINITION
|
||||||
|
|
||||||
|
enum Role {
|
||||||
|
USER
|
||||||
|
ADMIN
|
||||||
|
MODERATOR
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
deleteUser(id: ID!): Boolean! @auth(requires: ADMIN)
|
||||||
|
updateProfile(input: ProfileInput!): User! @auth
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Union Error Pattern
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
email: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidationError {
|
||||||
|
field: String!
|
||||||
|
message: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotFoundError {
|
||||||
|
message: String!
|
||||||
|
resourceType: String!
|
||||||
|
resourceId: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthorizationError {
|
||||||
|
message: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
union UserResult = User | ValidationError | NotFoundError | AuthorizationError
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
user(id: ID!): UserResult!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
{
|
||||||
|
user(id: "123") {
|
||||||
|
... on User {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
}
|
||||||
|
... on NotFoundError {
|
||||||
|
message
|
||||||
|
resourceType
|
||||||
|
}
|
||||||
|
... on AuthorizationError {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Errors in Payload
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type CreateUserPayload {
|
||||||
|
user: User
|
||||||
|
errors: [Error!]
|
||||||
|
success: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error {
|
||||||
|
field: String
|
||||||
|
message: String!
|
||||||
|
code: ErrorCode!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ErrorCode {
|
||||||
|
VALIDATION_ERROR
|
||||||
|
UNAUTHORIZED
|
||||||
|
NOT_FOUND
|
||||||
|
INTERNAL_ERROR
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## N+1 Query Problem Solutions
|
||||||
|
|
||||||
|
### DataLoader Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
from aiodataloader import DataLoader
|
||||||
|
|
||||||
|
class PostLoader(DataLoader):
|
||||||
|
async def batch_load_fn(self, post_ids):
|
||||||
|
posts = await db.posts.find({"id": {"$in": post_ids}})
|
||||||
|
post_map = {post["id"]: post for post in posts}
|
||||||
|
return [post_map.get(pid) for pid in post_ids]
|
||||||
|
|
||||||
|
# Resolver
|
||||||
|
@user_type.field("posts")
|
||||||
|
async def resolve_posts(user, info):
|
||||||
|
loader = info.context["loaders"]["post"]
|
||||||
|
return await loader.load_many(user["post_ids"])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Depth Limiting
|
||||||
|
|
||||||
|
```python
|
||||||
|
from graphql import GraphQLError
|
||||||
|
|
||||||
|
def depth_limit_validator(max_depth: int):
|
||||||
|
def validate(context, node, ancestors):
|
||||||
|
depth = len(ancestors)
|
||||||
|
if depth > max_depth:
|
||||||
|
raise GraphQLError(
|
||||||
|
f"Query depth {depth} exceeds maximum {max_depth}"
|
||||||
|
)
|
||||||
|
return validate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Complexity Analysis
|
||||||
|
|
||||||
|
```python
|
||||||
|
def complexity_limit_validator(max_complexity: int):
|
||||||
|
def calculate_complexity(node):
|
||||||
|
# Each field = 1, lists multiply
|
||||||
|
complexity = 1
|
||||||
|
if is_list_field(node):
|
||||||
|
complexity *= get_list_size_arg(node)
|
||||||
|
return complexity
|
||||||
|
|
||||||
|
return validate_complexity
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema Versioning
|
||||||
|
|
||||||
|
### Field Deprecation
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type User {
|
||||||
|
name: String! @deprecated(reason: "Use firstName and lastName")
|
||||||
|
firstName: String!
|
||||||
|
lastName: String!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schema Evolution
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
# v1 - Initial
|
||||||
|
type User {
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
# v2 - Add optional field (backward compatible)
|
||||||
|
type User {
|
||||||
|
name: String!
|
||||||
|
email: String
|
||||||
|
}
|
||||||
|
|
||||||
|
# v3 - Deprecate and add new field
|
||||||
|
type User {
|
||||||
|
name: String! @deprecated(reason: "Use firstName/lastName")
|
||||||
|
firstName: String!
|
||||||
|
lastName: String!
|
||||||
|
email: String
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices Summary
|
||||||
|
|
||||||
|
1. **Nullable vs Non-Null**: Start nullable, make non-null when guaranteed
|
||||||
|
2. **Input Types**: Always use input types for mutations
|
||||||
|
3. **Payload Pattern**: Return errors in mutation payloads
|
||||||
|
4. **Pagination**: Use cursor-based for infinite scroll, offset for simple cases
|
||||||
|
5. **Naming**: Use camelCase for fields, PascalCase for types
|
||||||
|
6. **Deprecation**: Use `@deprecated` instead of removing fields
|
||||||
|
7. **DataLoaders**: Always use for relationships to prevent N+1
|
||||||
|
8. **Complexity Limits**: Protect against expensive queries
|
||||||
|
9. **Custom Scalars**: Use for domain-specific types (Email, DateTime)
|
||||||
|
10. **Documentation**: Document all fields with descriptions
|
||||||
+408
@@ -0,0 +1,408 @@
|
|||||||
|
# REST API Best Practices
|
||||||
|
|
||||||
|
## URL Structure
|
||||||
|
|
||||||
|
### Resource Naming
|
||||||
|
|
||||||
|
```
|
||||||
|
# Good - Plural nouns
|
||||||
|
GET /api/users
|
||||||
|
GET /api/orders
|
||||||
|
GET /api/products
|
||||||
|
|
||||||
|
# Bad - Verbs or mixed conventions
|
||||||
|
GET /api/getUser
|
||||||
|
GET /api/user (inconsistent singular)
|
||||||
|
POST /api/createOrder
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nested Resources
|
||||||
|
|
||||||
|
```
|
||||||
|
# Shallow nesting (preferred)
|
||||||
|
GET /api/users/{id}/orders
|
||||||
|
GET /api/orders/{id}
|
||||||
|
|
||||||
|
# Deep nesting (avoid)
|
||||||
|
GET /api/users/{id}/orders/{orderId}/items/{itemId}/reviews
|
||||||
|
# Better:
|
||||||
|
GET /api/order-items/{id}/reviews
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP Methods and Status Codes
|
||||||
|
|
||||||
|
### GET - Retrieve Resources
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/users → 200 OK (with list)
|
||||||
|
GET /api/users/{id} → 200 OK or 404 Not Found
|
||||||
|
GET /api/users?page=2 → 200 OK (paginated)
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST - Create Resources
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/users
|
||||||
|
Body: {"name": "John", "email": "john@example.com"}
|
||||||
|
→ 201 Created
|
||||||
|
Location: /api/users/123
|
||||||
|
Body: {"id": "123", "name": "John", ...}
|
||||||
|
|
||||||
|
POST /api/users (validation error)
|
||||||
|
→ 422 Unprocessable Entity
|
||||||
|
Body: {"errors": [...]}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PUT - Replace Resources
|
||||||
|
|
||||||
|
```
|
||||||
|
PUT /api/users/{id}
|
||||||
|
Body: {complete user object}
|
||||||
|
→ 200 OK (updated)
|
||||||
|
→ 404 Not Found (doesn't exist)
|
||||||
|
|
||||||
|
# Must include ALL fields
|
||||||
|
```
|
||||||
|
|
||||||
|
### PATCH - Partial Update
|
||||||
|
|
||||||
|
```
|
||||||
|
PATCH /api/users/{id}
|
||||||
|
Body: {"name": "Jane"} (only changed fields)
|
||||||
|
→ 200 OK
|
||||||
|
→ 404 Not Found
|
||||||
|
```
|
||||||
|
|
||||||
|
### DELETE - Remove Resources
|
||||||
|
|
||||||
|
```
|
||||||
|
DELETE /api/users/{id}
|
||||||
|
→ 204 No Content (deleted)
|
||||||
|
→ 404 Not Found
|
||||||
|
→ 409 Conflict (can't delete due to references)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filtering, Sorting, and Searching
|
||||||
|
|
||||||
|
### Query Parameters
|
||||||
|
|
||||||
|
```
|
||||||
|
# Filtering
|
||||||
|
GET /api/users?status=active
|
||||||
|
GET /api/users?role=admin&status=active
|
||||||
|
|
||||||
|
# Sorting
|
||||||
|
GET /api/users?sort=created_at
|
||||||
|
GET /api/users?sort=-created_at (descending)
|
||||||
|
GET /api/users?sort=name,created_at
|
||||||
|
|
||||||
|
# Searching
|
||||||
|
GET /api/users?search=john
|
||||||
|
GET /api/users?q=john
|
||||||
|
|
||||||
|
# Field selection (sparse fieldsets)
|
||||||
|
GET /api/users?fields=id,name,email
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pagination Patterns
|
||||||
|
|
||||||
|
### Offset-Based Pagination
|
||||||
|
|
||||||
|
```python
|
||||||
|
GET /api/users?page=2&page_size=20
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"items": [...],
|
||||||
|
"page": 2,
|
||||||
|
"page_size": 20,
|
||||||
|
"total": 150,
|
||||||
|
"pages": 8
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cursor-Based Pagination (for large datasets)
|
||||||
|
|
||||||
|
```python
|
||||||
|
GET /api/users?limit=20&cursor=eyJpZCI6MTIzfQ
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"items": [...],
|
||||||
|
"next_cursor": "eyJpZCI6MTQzfQ",
|
||||||
|
"has_more": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Link Header Pagination (RESTful)
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/users?page=2
|
||||||
|
|
||||||
|
Response Headers:
|
||||||
|
Link: <https://api.example.com/users?page=3>; rel="next",
|
||||||
|
<https://api.example.com/users?page=1>; rel="prev",
|
||||||
|
<https://api.example.com/users?page=1>; rel="first",
|
||||||
|
<https://api.example.com/users?page=8>; rel="last"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Versioning Strategies
|
||||||
|
|
||||||
|
### URL Versioning (Recommended)
|
||||||
|
|
||||||
|
```
|
||||||
|
/api/v1/users
|
||||||
|
/api/v2/users
|
||||||
|
|
||||||
|
Pros: Clear, easy to route
|
||||||
|
Cons: Multiple URLs for same resource
|
||||||
|
```
|
||||||
|
|
||||||
|
### Header Versioning
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/users
|
||||||
|
Accept: application/vnd.api+json; version=2
|
||||||
|
|
||||||
|
Pros: Clean URLs
|
||||||
|
Cons: Less visible, harder to test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Parameter
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/users?version=2
|
||||||
|
|
||||||
|
Pros: Easy to test
|
||||||
|
Cons: Optional parameter can be forgotten
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
### Headers
|
||||||
|
|
||||||
|
```
|
||||||
|
X-RateLimit-Limit: 1000
|
||||||
|
X-RateLimit-Remaining: 742
|
||||||
|
X-RateLimit-Reset: 1640000000
|
||||||
|
|
||||||
|
Response when limited:
|
||||||
|
429 Too Many Requests
|
||||||
|
Retry-After: 3600
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fastapi import HTTPException, Request
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
class RateLimiter:
|
||||||
|
def __init__(self, calls: int, period: int):
|
||||||
|
self.calls = calls
|
||||||
|
self.period = period
|
||||||
|
self.cache = {}
|
||||||
|
|
||||||
|
def check(self, key: str) -> bool:
|
||||||
|
now = datetime.now()
|
||||||
|
if key not in self.cache:
|
||||||
|
self.cache[key] = []
|
||||||
|
|
||||||
|
# Remove old requests
|
||||||
|
self.cache[key] = [
|
||||||
|
ts for ts in self.cache[key]
|
||||||
|
if now - ts < timedelta(seconds=self.period)
|
||||||
|
]
|
||||||
|
|
||||||
|
if len(self.cache[key]) >= self.calls:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.cache[key].append(now)
|
||||||
|
return True
|
||||||
|
|
||||||
|
limiter = RateLimiter(calls=100, period=60)
|
||||||
|
|
||||||
|
@app.get("/api/users")
|
||||||
|
async def get_users(request: Request):
|
||||||
|
if not limiter.check(request.client.host):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=429,
|
||||||
|
headers={"Retry-After": "60"}
|
||||||
|
)
|
||||||
|
return {"users": [...]}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication and Authorization
|
||||||
|
|
||||||
|
### Bearer Token
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||||
|
|
||||||
|
401 Unauthorized - Missing/invalid token
|
||||||
|
403 Forbidden - Valid token, insufficient permissions
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Keys
|
||||||
|
|
||||||
|
```
|
||||||
|
X-API-Key: your-api-key-here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Response Format
|
||||||
|
|
||||||
|
### Consistent Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "VALIDATION_ERROR",
|
||||||
|
"message": "Request validation failed",
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"field": "email",
|
||||||
|
"message": "Invalid email format",
|
||||||
|
"value": "not-an-email"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timestamp": "2025-10-16T12:00:00Z",
|
||||||
|
"path": "/api/users"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Status Code Guidelines
|
||||||
|
|
||||||
|
- `200 OK`: Successful GET, PATCH, PUT
|
||||||
|
- `201 Created`: Successful POST
|
||||||
|
- `204 No Content`: Successful DELETE
|
||||||
|
- `400 Bad Request`: Malformed request
|
||||||
|
- `401 Unauthorized`: Authentication required
|
||||||
|
- `403 Forbidden`: Authenticated but not authorized
|
||||||
|
- `404 Not Found`: Resource doesn't exist
|
||||||
|
- `409 Conflict`: State conflict (duplicate email, etc.)
|
||||||
|
- `422 Unprocessable Entity`: Validation errors
|
||||||
|
- `429 Too Many Requests`: Rate limited
|
||||||
|
- `500 Internal Server Error`: Server error
|
||||||
|
- `503 Service Unavailable`: Temporary downtime
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
|
||||||
|
### Cache Headers
|
||||||
|
|
||||||
|
```
|
||||||
|
# Client caching
|
||||||
|
Cache-Control: public, max-age=3600
|
||||||
|
|
||||||
|
# No caching
|
||||||
|
Cache-Control: no-cache, no-store, must-revalidate
|
||||||
|
|
||||||
|
# Conditional requests
|
||||||
|
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
|
||||||
|
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
|
||||||
|
→ 304 Not Modified
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bulk Operations
|
||||||
|
|
||||||
|
### Batch Endpoints
|
||||||
|
|
||||||
|
```python
|
||||||
|
POST /api/users/batch
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{"name": "User1", "email": "user1@example.com"},
|
||||||
|
{"name": "User2", "email": "user2@example.com"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{"id": "1", "status": "created"},
|
||||||
|
{"id": null, "status": "failed", "error": "Email already exists"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Idempotency
|
||||||
|
|
||||||
|
### Idempotency Keys
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/orders
|
||||||
|
Idempotency-Key: unique-key-123
|
||||||
|
|
||||||
|
If duplicate request:
|
||||||
|
→ 200 OK (return cached response)
|
||||||
|
```
|
||||||
|
|
||||||
|
## CORS Configuration
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["https://example.com"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation with OpenAPI
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="My API",
|
||||||
|
description="API for managing users",
|
||||||
|
version="1.0.0",
|
||||||
|
docs_url="/docs",
|
||||||
|
redoc_url="/redoc"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get(
|
||||||
|
"/api/users/{user_id}",
|
||||||
|
summary="Get user by ID",
|
||||||
|
response_description="User details",
|
||||||
|
tags=["Users"]
|
||||||
|
)
|
||||||
|
async def get_user(
|
||||||
|
user_id: str = Path(..., description="The user ID")
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Retrieve user by ID.
|
||||||
|
|
||||||
|
Returns full user profile including:
|
||||||
|
- Basic information
|
||||||
|
- Contact details
|
||||||
|
- Account status
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Health and Monitoring Endpoints
|
||||||
|
|
||||||
|
```python
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
return {
|
||||||
|
"status": "healthy",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/health/detailed")
|
||||||
|
async def detailed_health():
|
||||||
|
return {
|
||||||
|
"status": "healthy",
|
||||||
|
"checks": {
|
||||||
|
"database": await check_database(),
|
||||||
|
"redis": await check_redis(),
|
||||||
|
"external_api": await check_external_api()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
+513
@@ -0,0 +1,513 @@
|
|||||||
|
# API Design Principles Implementation Playbook
|
||||||
|
|
||||||
|
This file contains detailed patterns, checklists, and code samples referenced by the skill.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### 1. RESTful Design Principles
|
||||||
|
|
||||||
|
**Resource-Oriented Architecture**
|
||||||
|
|
||||||
|
- Resources are nouns (users, orders, products), not verbs
|
||||||
|
- Use HTTP methods for actions (GET, POST, PUT, PATCH, DELETE)
|
||||||
|
- URLs represent resource hierarchies
|
||||||
|
- Consistent naming conventions
|
||||||
|
|
||||||
|
**HTTP Methods Semantics:**
|
||||||
|
|
||||||
|
- `GET`: Retrieve resources (idempotent, safe)
|
||||||
|
- `POST`: Create new resources
|
||||||
|
- `PUT`: Replace entire resource (idempotent)
|
||||||
|
- `PATCH`: Partial resource updates
|
||||||
|
- `DELETE`: Remove resources (idempotent)
|
||||||
|
|
||||||
|
### 2. GraphQL Design Principles
|
||||||
|
|
||||||
|
**Schema-First Development**
|
||||||
|
|
||||||
|
- Types define your domain model
|
||||||
|
- Queries for reading data
|
||||||
|
- Mutations for modifying data
|
||||||
|
- Subscriptions for real-time updates
|
||||||
|
|
||||||
|
**Query Structure:**
|
||||||
|
|
||||||
|
- Clients request exactly what they need
|
||||||
|
- Single endpoint, multiple operations
|
||||||
|
- Strongly typed schema
|
||||||
|
- Introspection built-in
|
||||||
|
|
||||||
|
### 3. API Versioning Strategies
|
||||||
|
|
||||||
|
**URL Versioning:**
|
||||||
|
|
||||||
|
```
|
||||||
|
/api/v1/users
|
||||||
|
/api/v2/users
|
||||||
|
```
|
||||||
|
|
||||||
|
**Header Versioning:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Accept: application/vnd.api+json; version=1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query Parameter Versioning:**
|
||||||
|
|
||||||
|
```
|
||||||
|
/api/users?version=1
|
||||||
|
```
|
||||||
|
|
||||||
|
## REST API Design Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Resource Collection Design
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Good: Resource-oriented endpoints
|
||||||
|
GET /api/users # List users (with pagination)
|
||||||
|
POST /api/users # Create user
|
||||||
|
GET /api/users/{id} # Get specific user
|
||||||
|
PUT /api/users/{id} # Replace user
|
||||||
|
PATCH /api/users/{id} # Update user fields
|
||||||
|
DELETE /api/users/{id} # Delete user
|
||||||
|
|
||||||
|
# Nested resources
|
||||||
|
GET /api/users/{id}/orders # Get user's orders
|
||||||
|
POST /api/users/{id}/orders # Create order for user
|
||||||
|
|
||||||
|
# Bad: Action-oriented endpoints (avoid)
|
||||||
|
POST /api/createUser
|
||||||
|
POST /api/getUserById
|
||||||
|
POST /api/deleteUser
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Pagination and Filtering
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import List, Optional
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
class PaginationParams(BaseModel):
|
||||||
|
page: int = Field(1, ge=1, description="Page number")
|
||||||
|
page_size: int = Field(20, ge=1, le=100, description="Items per page")
|
||||||
|
|
||||||
|
class FilterParams(BaseModel):
|
||||||
|
status: Optional[str] = None
|
||||||
|
created_after: Optional[str] = None
|
||||||
|
search: Optional[str] = None
|
||||||
|
|
||||||
|
class PaginatedResponse(BaseModel):
|
||||||
|
items: List[dict]
|
||||||
|
total: int
|
||||||
|
page: int
|
||||||
|
page_size: int
|
||||||
|
pages: int
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_next(self) -> bool:
|
||||||
|
return self.page < self.pages
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_prev(self) -> bool:
|
||||||
|
return self.page > 1
|
||||||
|
|
||||||
|
# FastAPI endpoint example
|
||||||
|
from fastapi import FastAPI, Query, Depends
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.get("/api/users", response_model=PaginatedResponse)
|
||||||
|
async def list_users(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(20, ge=1, le=100),
|
||||||
|
status: Optional[str] = Query(None),
|
||||||
|
search: Optional[str] = Query(None)
|
||||||
|
):
|
||||||
|
# Apply filters
|
||||||
|
query = build_query(status=status, search=search)
|
||||||
|
|
||||||
|
# Count total
|
||||||
|
total = await count_users(query)
|
||||||
|
|
||||||
|
# Fetch page
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
users = await fetch_users(query, limit=page_size, offset=offset)
|
||||||
|
|
||||||
|
return PaginatedResponse(
|
||||||
|
items=users,
|
||||||
|
total=total,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
pages=(total + page_size - 1) // page_size
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Error Handling and Status Codes
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class ErrorResponse(BaseModel):
|
||||||
|
error: str
|
||||||
|
message: str
|
||||||
|
details: Optional[dict] = None
|
||||||
|
timestamp: str
|
||||||
|
path: str
|
||||||
|
|
||||||
|
class ValidationErrorDetail(BaseModel):
|
||||||
|
field: str
|
||||||
|
message: str
|
||||||
|
value: Any
|
||||||
|
|
||||||
|
# Consistent error responses
|
||||||
|
STATUS_CODES = {
|
||||||
|
"success": 200,
|
||||||
|
"created": 201,
|
||||||
|
"no_content": 204,
|
||||||
|
"bad_request": 400,
|
||||||
|
"unauthorized": 401,
|
||||||
|
"forbidden": 403,
|
||||||
|
"not_found": 404,
|
||||||
|
"conflict": 409,
|
||||||
|
"unprocessable": 422,
|
||||||
|
"internal_error": 500
|
||||||
|
}
|
||||||
|
|
||||||
|
def raise_not_found(resource: str, id: str):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail={
|
||||||
|
"error": "NotFound",
|
||||||
|
"message": f"{resource} not found",
|
||||||
|
"details": {"id": id}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def raise_validation_error(errors: List[ValidationErrorDetail]):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
detail={
|
||||||
|
"error": "ValidationError",
|
||||||
|
"message": "Request validation failed",
|
||||||
|
"details": {"errors": [e.dict() for e in errors]}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
@app.get("/api/users/{user_id}")
|
||||||
|
async def get_user(user_id: str):
|
||||||
|
user = await fetch_user(user_id)
|
||||||
|
if not user:
|
||||||
|
raise_not_found("User", user_id)
|
||||||
|
return user
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: HATEOAS (Hypermedia as the Engine of Application State)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class UserResponse(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
email: str
|
||||||
|
_links: dict
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_user(cls, user: User, base_url: str):
|
||||||
|
return cls(
|
||||||
|
id=user.id,
|
||||||
|
name=user.name,
|
||||||
|
email=user.email,
|
||||||
|
_links={
|
||||||
|
"self": {"href": f"{base_url}/api/users/{user.id}"},
|
||||||
|
"orders": {"href": f"{base_url}/api/users/{user.id}/orders"},
|
||||||
|
"update": {
|
||||||
|
"href": f"{base_url}/api/users/{user.id}",
|
||||||
|
"method": "PATCH"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"href": f"{base_url}/api/users/{user.id}",
|
||||||
|
"method": "DELETE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## GraphQL Design Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Schema Design
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
# schema.graphql
|
||||||
|
|
||||||
|
# Clear type definitions
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
email: String!
|
||||||
|
name: String!
|
||||||
|
createdAt: DateTime!
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
orders(first: Int = 20, after: String, status: OrderStatus): OrderConnection!
|
||||||
|
|
||||||
|
profile: UserProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
type Order {
|
||||||
|
id: ID!
|
||||||
|
status: OrderStatus!
|
||||||
|
total: Money!
|
||||||
|
items: [OrderItem!]!
|
||||||
|
createdAt: DateTime!
|
||||||
|
|
||||||
|
# Back-reference
|
||||||
|
user: User!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pagination pattern (Relay-style)
|
||||||
|
type OrderConnection {
|
||||||
|
edges: [OrderEdge!]!
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
totalCount: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrderEdge {
|
||||||
|
node: Order!
|
||||||
|
cursor: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageInfo {
|
||||||
|
hasNextPage: Boolean!
|
||||||
|
hasPreviousPage: Boolean!
|
||||||
|
startCursor: String
|
||||||
|
endCursor: String
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enums for type safety
|
||||||
|
enum OrderStatus {
|
||||||
|
PENDING
|
||||||
|
CONFIRMED
|
||||||
|
SHIPPED
|
||||||
|
DELIVERED
|
||||||
|
CANCELLED
|
||||||
|
}
|
||||||
|
|
||||||
|
# Custom scalars
|
||||||
|
scalar DateTime
|
||||||
|
scalar Money
|
||||||
|
|
||||||
|
# Query root
|
||||||
|
type Query {
|
||||||
|
user(id: ID!): User
|
||||||
|
users(first: Int = 20, after: String, search: String): UserConnection!
|
||||||
|
|
||||||
|
order(id: ID!): Order
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mutation root
|
||||||
|
type Mutation {
|
||||||
|
createUser(input: CreateUserInput!): CreateUserPayload!
|
||||||
|
updateUser(input: UpdateUserInput!): UpdateUserPayload!
|
||||||
|
deleteUser(id: ID!): DeleteUserPayload!
|
||||||
|
|
||||||
|
createOrder(input: CreateOrderInput!): CreateOrderPayload!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Input types for mutations
|
||||||
|
input CreateUserInput {
|
||||||
|
email: String!
|
||||||
|
name: String!
|
||||||
|
password: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Payload types for mutations
|
||||||
|
type CreateUserPayload {
|
||||||
|
user: User
|
||||||
|
errors: [Error!]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error {
|
||||||
|
field: String
|
||||||
|
message: String!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Resolver Design
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Optional, List
|
||||||
|
from ariadne import QueryType, MutationType, ObjectType
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
query = QueryType()
|
||||||
|
mutation = MutationType()
|
||||||
|
user_type = ObjectType("User")
|
||||||
|
|
||||||
|
@query.field("user")
|
||||||
|
async def resolve_user(obj, info, id: str) -> Optional[dict]:
|
||||||
|
"""Resolve single user by ID."""
|
||||||
|
return await fetch_user_by_id(id)
|
||||||
|
|
||||||
|
@query.field("users")
|
||||||
|
async def resolve_users(
|
||||||
|
obj,
|
||||||
|
info,
|
||||||
|
first: int = 20,
|
||||||
|
after: Optional[str] = None,
|
||||||
|
search: Optional[str] = None
|
||||||
|
) -> dict:
|
||||||
|
"""Resolve paginated user list."""
|
||||||
|
# Decode cursor
|
||||||
|
offset = decode_cursor(after) if after else 0
|
||||||
|
|
||||||
|
# Fetch users
|
||||||
|
users = await fetch_users(
|
||||||
|
limit=first + 1, # Fetch one extra to check hasNextPage
|
||||||
|
offset=offset,
|
||||||
|
search=search
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pagination
|
||||||
|
has_next = len(users) > first
|
||||||
|
if has_next:
|
||||||
|
users = users[:first]
|
||||||
|
|
||||||
|
edges = [
|
||||||
|
{
|
||||||
|
"node": user,
|
||||||
|
"cursor": encode_cursor(offset + i)
|
||||||
|
}
|
||||||
|
for i, user in enumerate(users)
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"edges": edges,
|
||||||
|
"pageInfo": {
|
||||||
|
"hasNextPage": has_next,
|
||||||
|
"hasPreviousPage": offset > 0,
|
||||||
|
"startCursor": edges[0]["cursor"] if edges else None,
|
||||||
|
"endCursor": edges[-1]["cursor"] if edges else None
|
||||||
|
},
|
||||||
|
"totalCount": await count_users(search=search)
|
||||||
|
}
|
||||||
|
|
||||||
|
@user_type.field("orders")
|
||||||
|
async def resolve_user_orders(user: dict, info, first: int = 20) -> dict:
|
||||||
|
"""Resolve user's orders (N+1 prevention with DataLoader)."""
|
||||||
|
# Use DataLoader to batch requests
|
||||||
|
loader = info.context["loaders"]["orders_by_user"]
|
||||||
|
orders = await loader.load(user["id"])
|
||||||
|
|
||||||
|
return paginate_orders(orders, first)
|
||||||
|
|
||||||
|
@mutation.field("createUser")
|
||||||
|
async def resolve_create_user(obj, info, input: dict) -> dict:
|
||||||
|
"""Create new user."""
|
||||||
|
try:
|
||||||
|
# Validate input
|
||||||
|
validate_user_input(input)
|
||||||
|
|
||||||
|
# Create user
|
||||||
|
user = await create_user(
|
||||||
|
email=input["email"],
|
||||||
|
name=input["name"],
|
||||||
|
password=hash_password(input["password"])
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"user": user,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
except ValidationError as e:
|
||||||
|
return {
|
||||||
|
"user": None,
|
||||||
|
"errors": [{"field": e.field, "message": e.message}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: DataLoader (N+1 Problem Prevention)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from aiodataloader import DataLoader
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
class UserLoader(DataLoader):
|
||||||
|
"""Batch load users by ID."""
|
||||||
|
|
||||||
|
async def batch_load_fn(self, user_ids: List[str]) -> List[Optional[dict]]:
|
||||||
|
"""Load multiple users in single query."""
|
||||||
|
users = await fetch_users_by_ids(user_ids)
|
||||||
|
|
||||||
|
# Map results back to input order
|
||||||
|
user_map = {user["id"]: user for user in users}
|
||||||
|
return [user_map.get(user_id) for user_id in user_ids]
|
||||||
|
|
||||||
|
class OrdersByUserLoader(DataLoader):
|
||||||
|
"""Batch load orders by user ID."""
|
||||||
|
|
||||||
|
async def batch_load_fn(self, user_ids: List[str]) -> List[List[dict]]:
|
||||||
|
"""Load orders for multiple users in single query."""
|
||||||
|
orders = await fetch_orders_by_user_ids(user_ids)
|
||||||
|
|
||||||
|
# Group orders by user_id
|
||||||
|
orders_by_user = {}
|
||||||
|
for order in orders:
|
||||||
|
user_id = order["user_id"]
|
||||||
|
if user_id not in orders_by_user:
|
||||||
|
orders_by_user[user_id] = []
|
||||||
|
orders_by_user[user_id].append(order)
|
||||||
|
|
||||||
|
# Return in input order
|
||||||
|
return [orders_by_user.get(user_id, []) for user_id in user_ids]
|
||||||
|
|
||||||
|
# Context setup
|
||||||
|
def create_context():
|
||||||
|
return {
|
||||||
|
"loaders": {
|
||||||
|
"user": UserLoader(),
|
||||||
|
"orders_by_user": OrdersByUserLoader()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### REST APIs
|
||||||
|
|
||||||
|
1. **Consistent Naming**: Use plural nouns for collections (`/users`, not `/user`)
|
||||||
|
2. **Stateless**: Each request contains all necessary information
|
||||||
|
3. **Use HTTP Status Codes Correctly**: 2xx success, 4xx client errors, 5xx server errors
|
||||||
|
4. **Version Your API**: Plan for breaking changes from day one
|
||||||
|
5. **Pagination**: Always paginate large collections
|
||||||
|
6. **Rate Limiting**: Protect your API with rate limits
|
||||||
|
7. **Documentation**: Use OpenAPI/Swagger for interactive docs
|
||||||
|
|
||||||
|
### GraphQL APIs
|
||||||
|
|
||||||
|
1. **Schema First**: Design schema before writing resolvers
|
||||||
|
2. **Avoid N+1**: Use DataLoaders for efficient data fetching
|
||||||
|
3. **Input Validation**: Validate at schema and resolver levels
|
||||||
|
4. **Error Handling**: Return structured errors in mutation payloads
|
||||||
|
5. **Pagination**: Use cursor-based pagination (Relay spec)
|
||||||
|
6. **Deprecation**: Use `@deprecated` directive for gradual migration
|
||||||
|
7. **Monitoring**: Track query complexity and execution time
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
- **Over-fetching/Under-fetching (REST)**: Fixed in GraphQL but requires DataLoaders
|
||||||
|
- **Breaking Changes**: Version APIs or use deprecation strategies
|
||||||
|
- **Inconsistent Error Formats**: Standardize error responses
|
||||||
|
- **Missing Rate Limits**: APIs without limits are vulnerable to abuse
|
||||||
|
- **Poor Documentation**: Undocumented APIs frustrate developers
|
||||||
|
- **Ignoring HTTP Semantics**: POST for idempotent operations breaks expectations
|
||||||
|
- **Tight Coupling**: API structure shouldn't mirror database schema
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- **references/rest-best-practices.md**: Comprehensive REST API design guide
|
||||||
|
- **references/graphql-schema-design.md**: GraphQL schema patterns and anti-patterns
|
||||||
|
- **references/api-versioning-strategies.md**: Versioning approaches and migration paths
|
||||||
|
- **assets/rest-api-template.py**: FastAPI REST API template
|
||||||
|
- **assets/graphql-schema-template.graphql**: Complete GraphQL schema example
|
||||||
|
- **assets/api-design-checklist.md**: Pre-implementation review checklist
|
||||||
|
- **scripts/openapi-generator.py**: Generate OpenAPI specs from code
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
---
|
||||||
|
name: api-documentation
|
||||||
|
description: "API documentation workflow for generating OpenAPI specs, creating developer guides, and maintaining comprehensive API documentation."
|
||||||
|
category: granular-workflow-bundle
|
||||||
|
risk: safe
|
||||||
|
source: personal
|
||||||
|
date_added: "2026-02-27"
|
||||||
|
---
|
||||||
|
|
||||||
|
# API Documentation Workflow
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Specialized workflow for creating comprehensive API documentation including OpenAPI/Swagger specs, developer guides, code examples, and interactive documentation.
|
||||||
|
|
||||||
|
## When to Use This Workflow
|
||||||
|
|
||||||
|
Use this workflow when:
|
||||||
|
- Creating API documentation
|
||||||
|
- Generating OpenAPI specs
|
||||||
|
- Writing developer guides
|
||||||
|
- Adding code examples
|
||||||
|
- Setting up API portals
|
||||||
|
|
||||||
|
## Workflow Phases
|
||||||
|
|
||||||
|
### Phase 1: API Discovery
|
||||||
|
|
||||||
|
#### Skills to Invoke
|
||||||
|
- `api-documenter` - API documentation
|
||||||
|
- `api-design-principles` - API design
|
||||||
|
|
||||||
|
#### Actions
|
||||||
|
1. Inventory endpoints
|
||||||
|
2. Document request/response
|
||||||
|
3. Identify authentication
|
||||||
|
4. Map error codes
|
||||||
|
5. Note rate limits
|
||||||
|
|
||||||
|
#### Copy-Paste Prompts
|
||||||
|
```
|
||||||
|
Use @api-documenter to discover and document API endpoints
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: OpenAPI Specification
|
||||||
|
|
||||||
|
#### Skills to Invoke
|
||||||
|
- `openapi-spec-generation` - OpenAPI
|
||||||
|
- `api-documenter` - API specs
|
||||||
|
|
||||||
|
#### Actions
|
||||||
|
1. Create OpenAPI schema
|
||||||
|
2. Define paths
|
||||||
|
3. Add schemas
|
||||||
|
4. Configure security
|
||||||
|
5. Add examples
|
||||||
|
|
||||||
|
#### Copy-Paste Prompts
|
||||||
|
```
|
||||||
|
Use @openapi-spec-generation to create OpenAPI specification
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Developer Guide
|
||||||
|
|
||||||
|
#### Skills to Invoke
|
||||||
|
- `api-documentation-generator` - Documentation
|
||||||
|
- `documentation-templates` - Templates
|
||||||
|
|
||||||
|
#### Actions
|
||||||
|
1. Create getting started
|
||||||
|
2. Write authentication guide
|
||||||
|
3. Document common patterns
|
||||||
|
4. Add troubleshooting
|
||||||
|
5. Create FAQ
|
||||||
|
|
||||||
|
#### Copy-Paste Prompts
|
||||||
|
```
|
||||||
|
Use @api-documentation-generator to create developer guide
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: Code Examples
|
||||||
|
|
||||||
|
#### Skills to Invoke
|
||||||
|
- `api-documenter` - Code examples
|
||||||
|
- `tutorial-engineer` - Tutorials
|
||||||
|
|
||||||
|
#### Actions
|
||||||
|
1. Create example requests
|
||||||
|
2. Write SDK examples
|
||||||
|
3. Add curl examples
|
||||||
|
4. Create tutorials
|
||||||
|
5. Test examples
|
||||||
|
|
||||||
|
#### Copy-Paste Prompts
|
||||||
|
```
|
||||||
|
Use @api-documenter to generate code examples
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 5: Interactive Docs
|
||||||
|
|
||||||
|
#### Skills to Invoke
|
||||||
|
- `api-documenter` - Interactive docs
|
||||||
|
|
||||||
|
#### Actions
|
||||||
|
1. Set up Swagger UI
|
||||||
|
2. Configure Redoc
|
||||||
|
3. Add try-it functionality
|
||||||
|
4. Test interactivity
|
||||||
|
5. Deploy docs
|
||||||
|
|
||||||
|
#### Copy-Paste Prompts
|
||||||
|
```
|
||||||
|
Use @api-documenter to set up interactive documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 6: Documentation Site
|
||||||
|
|
||||||
|
#### Skills to Invoke
|
||||||
|
- `docs-architect` - Documentation architecture
|
||||||
|
- `wiki-page-writer` - Documentation
|
||||||
|
|
||||||
|
#### Actions
|
||||||
|
1. Choose platform
|
||||||
|
2. Design structure
|
||||||
|
3. Create pages
|
||||||
|
4. Add navigation
|
||||||
|
5. Configure search
|
||||||
|
|
||||||
|
#### Copy-Paste Prompts
|
||||||
|
```
|
||||||
|
Use @docs-architect to design API documentation site
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 7: Maintenance
|
||||||
|
|
||||||
|
#### Skills to Invoke
|
||||||
|
- `api-documenter` - Doc maintenance
|
||||||
|
|
||||||
|
#### Actions
|
||||||
|
1. Set up auto-generation
|
||||||
|
2. Configure validation
|
||||||
|
3. Add review process
|
||||||
|
4. Schedule updates
|
||||||
|
5. Monitor feedback
|
||||||
|
|
||||||
|
#### Copy-Paste Prompts
|
||||||
|
```
|
||||||
|
Use @api-documenter to set up automated doc generation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quality Gates
|
||||||
|
|
||||||
|
- [ ] OpenAPI spec complete
|
||||||
|
- [ ] Developer guide written
|
||||||
|
- [ ] Code examples working
|
||||||
|
- [ ] Interactive docs functional
|
||||||
|
- [ ] Documentation deployed
|
||||||
|
|
||||||
|
## Related Workflow Bundles
|
||||||
|
|
||||||
|
- `documentation` - Documentation
|
||||||
|
- `api-development` - API development
|
||||||
|
- `development` - Development
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,915 @@
|
|||||||
|
---
|
||||||
|
name: api-security-best-practices
|
||||||
|
description: "Implement secure API design patterns including authentication, authorization, input validation, rate limiting, and protection against common API vulnerabilities"
|
||||||
|
risk: unknown
|
||||||
|
source: community
|
||||||
|
date_added: "2026-02-27"
|
||||||
|
---
|
||||||
|
|
||||||
|
# API Security Best Practices
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Guide developers in building secure APIs by implementing authentication, authorization, input validation, rate limiting, and protection against common vulnerabilities. This skill covers security patterns for REST, GraphQL, and WebSocket APIs.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
- Use when designing new API endpoints
|
||||||
|
- Use when securing existing APIs
|
||||||
|
- Use when implementing authentication and authorization
|
||||||
|
- Use when protecting against API attacks (injection, DDoS, etc.)
|
||||||
|
- Use when conducting API security reviews
|
||||||
|
- Use when preparing for security audits
|
||||||
|
- Use when implementing rate limiting and throttling
|
||||||
|
- Use when handling sensitive data in APIs
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Step 1: Authentication & Authorization
|
||||||
|
|
||||||
|
I'll help you implement secure authentication:
|
||||||
|
- Choose authentication method (JWT, OAuth 2.0, API keys)
|
||||||
|
- Implement token-based authentication
|
||||||
|
- Set up role-based access control (RBAC)
|
||||||
|
- Secure session management
|
||||||
|
- Implement multi-factor authentication (MFA)
|
||||||
|
|
||||||
|
### Step 2: Input Validation & Sanitization
|
||||||
|
|
||||||
|
Protect against injection attacks:
|
||||||
|
- Validate all input data
|
||||||
|
- Sanitize user inputs
|
||||||
|
- Use parameterized queries
|
||||||
|
- Implement request schema validation
|
||||||
|
- Prevent SQL injection, XSS, and command injection
|
||||||
|
|
||||||
|
### Step 3: Rate Limiting & Throttling
|
||||||
|
|
||||||
|
Prevent abuse and DDoS attacks:
|
||||||
|
- Implement rate limiting per user/IP
|
||||||
|
- Set up API throttling
|
||||||
|
- Configure request quotas
|
||||||
|
- Handle rate limit errors gracefully
|
||||||
|
- Monitor for suspicious activity
|
||||||
|
|
||||||
|
### Step 4: Data Protection
|
||||||
|
|
||||||
|
Secure sensitive data:
|
||||||
|
- Encrypt data in transit (HTTPS/TLS)
|
||||||
|
- Encrypt sensitive data at rest
|
||||||
|
- Implement proper error handling (no data leaks)
|
||||||
|
- Sanitize error messages
|
||||||
|
- Use secure headers
|
||||||
|
|
||||||
|
### Step 5: API Security Testing
|
||||||
|
|
||||||
|
Verify security implementation:
|
||||||
|
- Test authentication and authorization
|
||||||
|
- Perform penetration testing
|
||||||
|
- Check for common vulnerabilities (OWASP API Top 10)
|
||||||
|
- Validate input handling
|
||||||
|
- Test rate limiting
|
||||||
|
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1: Implementing JWT Authentication
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Secure JWT Authentication Implementation
|
||||||
|
|
||||||
|
### Authentication Flow
|
||||||
|
|
||||||
|
1. User logs in with credentials
|
||||||
|
2. Server validates credentials
|
||||||
|
3. Server generates JWT token
|
||||||
|
4. Client stores token securely
|
||||||
|
5. Client sends token with each request
|
||||||
|
6. Server validates token
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
#### 1. Generate Secure JWT Tokens
|
||||||
|
|
||||||
|
\`\`\`javascript
|
||||||
|
// auth.js
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const bcrypt = require('bcrypt');
|
||||||
|
|
||||||
|
// Login endpoint
|
||||||
|
app.post('/api/auth/login', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if (!email || !password) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Email and password are required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find user
|
||||||
|
const user = await db.user.findUnique({
|
||||||
|
where: { email }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
// Don't reveal if user exists
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Invalid credentials'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify password
|
||||||
|
const validPassword = await bcrypt.compare(
|
||||||
|
password,
|
||||||
|
user.passwordHash
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!validPassword) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Invalid credentials'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate JWT token
|
||||||
|
const token = jwt.sign(
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role
|
||||||
|
},
|
||||||
|
process.env.JWT_SECRET,
|
||||||
|
{
|
||||||
|
expiresIn: '1h',
|
||||||
|
issuer: 'your-app',
|
||||||
|
audience: 'your-app-users'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate refresh token
|
||||||
|
const refreshToken = jwt.sign(
|
||||||
|
{ userId: user.id },
|
||||||
|
process.env.JWT_REFRESH_SECRET,
|
||||||
|
{ expiresIn: '7d' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store refresh token in database
|
||||||
|
await db.refreshToken.create({
|
||||||
|
data: {
|
||||||
|
token: refreshToken,
|
||||||
|
userId: user.id,
|
||||||
|
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
token,
|
||||||
|
refreshToken,
|
||||||
|
expiresIn: 3600
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'An error occurred during login'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
#### 2. Verify JWT Tokens (Middleware)
|
||||||
|
|
||||||
|
\`\`\`javascript
|
||||||
|
// middleware/auth.js
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
|
function authenticateToken(req, res, next) {
|
||||||
|
// Get token from header
|
||||||
|
const authHeader = req.headers['authorization'];
|
||||||
|
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Access token required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify token
|
||||||
|
jwt.verify(
|
||||||
|
token,
|
||||||
|
process.env.JWT_SECRET,
|
||||||
|
{
|
||||||
|
issuer: 'your-app',
|
||||||
|
audience: 'your-app-users'
|
||||||
|
},
|
||||||
|
(err, user) => {
|
||||||
|
if (err) {
|
||||||
|
if (err.name === 'TokenExpiredError') {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Token expired'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.status(403).json({
|
||||||
|
error: 'Invalid token'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach user to request
|
||||||
|
req.user = user;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { authenticateToken };
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
#### 3. Protect Routes
|
||||||
|
|
||||||
|
\`\`\`javascript
|
||||||
|
const { authenticateToken } = require('./middleware/auth');
|
||||||
|
|
||||||
|
// Protected route
|
||||||
|
app.get('/api/user/profile', authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await db.user.findUnique({
|
||||||
|
where: { id: req.user.userId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
// Don't return passwordHash
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(user);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
#### 4. Implement Token Refresh
|
||||||
|
|
||||||
|
\`\`\`javascript
|
||||||
|
app.post('/api/auth/refresh', async (req, res) => {
|
||||||
|
const { refreshToken } = req.body;
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Refresh token required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify refresh token
|
||||||
|
const decoded = jwt.verify(
|
||||||
|
refreshToken,
|
||||||
|
process.env.JWT_REFRESH_SECRET
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if refresh token exists in database
|
||||||
|
const storedToken = await db.refreshToken.findFirst({
|
||||||
|
where: {
|
||||||
|
token: refreshToken,
|
||||||
|
userId: decoded.userId,
|
||||||
|
expiresAt: { gt: new Date() }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!storedToken) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: 'Invalid refresh token'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new access token
|
||||||
|
const user = await db.user.findUnique({
|
||||||
|
where: { id: decoded.userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
const newToken = jwt.sign(
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role
|
||||||
|
},
|
||||||
|
process.env.JWT_SECRET,
|
||||||
|
{ expiresIn: '1h' }
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
token: newToken,
|
||||||
|
expiresIn: 3600
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
res.status(403).json({
|
||||||
|
error: 'Invalid refresh token'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Security Best Practices
|
||||||
|
|
||||||
|
- ✅ Use strong JWT secrets (256-bit minimum)
|
||||||
|
- ✅ Set short expiration times (1 hour for access tokens)
|
||||||
|
- ✅ Implement refresh tokens for long-lived sessions
|
||||||
|
- ✅ Store refresh tokens in database (can be revoked)
|
||||||
|
- ✅ Use HTTPS only
|
||||||
|
- ✅ Don't store sensitive data in JWT payload
|
||||||
|
- ✅ Validate token issuer and audience
|
||||||
|
- ✅ Implement token blacklisting for logout
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Example 2: Input Validation and SQL Injection Prevention
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Preventing SQL Injection and Input Validation
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
|
||||||
|
**❌ Vulnerable Code:**
|
||||||
|
\`\`\`javascript
|
||||||
|
// NEVER DO THIS - SQL Injection vulnerability
|
||||||
|
app.get('/api/users/:id', async (req, res) => {
|
||||||
|
const userId = req.params.id;
|
||||||
|
|
||||||
|
// Dangerous: User input directly in query
|
||||||
|
const query = \`SELECT * FROM users WHERE id = '\${userId}'\`;
|
||||||
|
const user = await db.query(query);
|
||||||
|
|
||||||
|
res.json(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attack example:
|
||||||
|
// GET /api/users/1' OR '1'='1
|
||||||
|
// Returns all users!
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### The Solution
|
||||||
|
|
||||||
|
#### 1. Use Parameterized Queries
|
||||||
|
|
||||||
|
\`\`\`javascript
|
||||||
|
// ✅ Safe: Parameterized query
|
||||||
|
app.get('/api/users/:id', async (req, res) => {
|
||||||
|
const userId = req.params.id;
|
||||||
|
|
||||||
|
// Validate input first
|
||||||
|
if (!userId || !/^\d+$/.test(userId)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid user ID'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use parameterized query
|
||||||
|
const user = await db.query(
|
||||||
|
'SELECT id, email, name FROM users WHERE id = $1',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: 'User not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(user);
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
#### 2. Use ORM with Proper Escaping
|
||||||
|
|
||||||
|
\`\`\`javascript
|
||||||
|
// ✅ Safe: Using Prisma ORM
|
||||||
|
app.get('/api/users/:id', async (req, res) => {
|
||||||
|
const userId = parseInt(req.params.id);
|
||||||
|
|
||||||
|
if (isNaN(userId)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid user ID'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
// Don't select sensitive fields
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: 'User not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(user);
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
#### 3. Implement Request Validation with Zod
|
||||||
|
|
||||||
|
\`\`\`javascript
|
||||||
|
const { z } = require('zod');
|
||||||
|
|
||||||
|
// Define validation schema
|
||||||
|
const createUserSchema = z.object({
|
||||||
|
email: z.string().email('Invalid email format'),
|
||||||
|
password: z.string()
|
||||||
|
.min(8, 'Password must be at least 8 characters')
|
||||||
|
.regex(/[A-Z]/, 'Password must contain uppercase letter')
|
||||||
|
.regex(/[a-z]/, 'Password must contain lowercase letter')
|
||||||
|
.regex(/[0-9]/, 'Password must contain number'),
|
||||||
|
name: z.string()
|
||||||
|
.min(2, 'Name must be at least 2 characters')
|
||||||
|
.max(100, 'Name too long'),
|
||||||
|
age: z.number()
|
||||||
|
.int('Age must be an integer')
|
||||||
|
.min(18, 'Must be 18 or older')
|
||||||
|
.max(120, 'Invalid age')
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validation middleware
|
||||||
|
function validateRequest(schema) {
|
||||||
|
return (req, res, next) => {
|
||||||
|
try {
|
||||||
|
schema.parse(req.body);
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Validation failed',
|
||||||
|
details: error.errors
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use validation
|
||||||
|
app.post('/api/users',
|
||||||
|
validateRequest(createUserSchema),
|
||||||
|
async (req, res) => {
|
||||||
|
// Input is validated at this point
|
||||||
|
const { email, password, name, age } = req.body;
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
const passwordHash = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email,
|
||||||
|
passwordHash,
|
||||||
|
name,
|
||||||
|
age
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't return password hash
|
||||||
|
const { passwordHash: _, ...userWithoutPassword } = user;
|
||||||
|
res.status(201).json(userWithoutPassword);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
#### 4. Sanitize Output to Prevent XSS
|
||||||
|
|
||||||
|
\`\`\`javascript
|
||||||
|
const DOMPurify = require('isomorphic-dompurify');
|
||||||
|
|
||||||
|
app.post('/api/comments', authenticateToken, async (req, res) => {
|
||||||
|
const { content } = req.body;
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
if (!content || content.length > 1000) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid comment content'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize HTML to prevent XSS
|
||||||
|
const sanitizedContent = DOMPurify.sanitize(content, {
|
||||||
|
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
|
||||||
|
ALLOWED_ATTR: ['href']
|
||||||
|
});
|
||||||
|
|
||||||
|
const comment = await prisma.comment.create({
|
||||||
|
data: {
|
||||||
|
content: sanitizedContent,
|
||||||
|
userId: req.user.userId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json(comment);
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Validation Checklist
|
||||||
|
|
||||||
|
- [ ] Validate all user inputs
|
||||||
|
- [ ] Use parameterized queries or ORM
|
||||||
|
- [ ] Validate data types (string, number, email, etc.)
|
||||||
|
- [ ] Validate data ranges (min/max length, value ranges)
|
||||||
|
- [ ] Sanitize HTML content
|
||||||
|
- [ ] Escape special characters
|
||||||
|
- [ ] Validate file uploads (type, size, content)
|
||||||
|
- [ ] Use allowlists, not blocklists
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Example 3: Rate Limiting and DDoS Protection
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Implementing Rate Limiting
|
||||||
|
|
||||||
|
### Why Rate Limiting?
|
||||||
|
|
||||||
|
- Prevent brute force attacks
|
||||||
|
- Protect against DDoS
|
||||||
|
- Prevent API abuse
|
||||||
|
- Ensure fair usage
|
||||||
|
- Reduce server costs
|
||||||
|
|
||||||
|
### Implementation with Express Rate Limit
|
||||||
|
|
||||||
|
\`\`\`javascript
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const RedisStore = require('rate-limit-redis');
|
||||||
|
const Redis = require('ioredis');
|
||||||
|
|
||||||
|
// Create Redis client
|
||||||
|
const redis = new Redis({
|
||||||
|
host: process.env.REDIS_HOST,
|
||||||
|
port: process.env.REDIS_PORT
|
||||||
|
});
|
||||||
|
|
||||||
|
// General API rate limit
|
||||||
|
const apiLimiter = rateLimit({
|
||||||
|
store: new RedisStore({
|
||||||
|
client: redis,
|
||||||
|
prefix: 'rl:api:'
|
||||||
|
}),
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 100, // 100 requests per window
|
||||||
|
message: {
|
||||||
|
error: 'Too many requests, please try again later',
|
||||||
|
retryAfter: 900 // seconds
|
||||||
|
},
|
||||||
|
standardHeaders: true, // Return rate limit info in headers
|
||||||
|
legacyHeaders: false,
|
||||||
|
// Custom key generator (by user ID or IP)
|
||||||
|
keyGenerator: (req) => {
|
||||||
|
return req.user?.userId || req.ip;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Strict rate limit for authentication endpoints
|
||||||
|
const authLimiter = rateLimit({
|
||||||
|
store: new RedisStore({
|
||||||
|
client: redis,
|
||||||
|
prefix: 'rl:auth:'
|
||||||
|
}),
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 5, // Only 5 login attempts per 15 minutes
|
||||||
|
skipSuccessfulRequests: true, // Don't count successful logins
|
||||||
|
message: {
|
||||||
|
error: 'Too many login attempts, please try again later',
|
||||||
|
retryAfter: 900
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply rate limiters
|
||||||
|
app.use('/api/', apiLimiter);
|
||||||
|
app.use('/api/auth/login', authLimiter);
|
||||||
|
app.use('/api/auth/register', authLimiter);
|
||||||
|
|
||||||
|
// Custom rate limiter for expensive operations
|
||||||
|
const expensiveLimiter = rateLimit({
|
||||||
|
windowMs: 60 * 60 * 1000, // 1 hour
|
||||||
|
max: 10, // 10 requests per hour
|
||||||
|
message: {
|
||||||
|
error: 'Rate limit exceeded for this operation'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/reports/generate',
|
||||||
|
authenticateToken,
|
||||||
|
expensiveLimiter,
|
||||||
|
async (req, res) => {
|
||||||
|
// Expensive operation
|
||||||
|
}
|
||||||
|
);
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Advanced: Per-User Rate Limiting
|
||||||
|
|
||||||
|
\`\`\`javascript
|
||||||
|
// Different limits based on user tier
|
||||||
|
function createTieredRateLimiter() {
|
||||||
|
const limits = {
|
||||||
|
free: { windowMs: 60 * 60 * 1000, max: 100 },
|
||||||
|
pro: { windowMs: 60 * 60 * 1000, max: 1000 },
|
||||||
|
enterprise: { windowMs: 60 * 60 * 1000, max: 10000 }
|
||||||
|
};
|
||||||
|
|
||||||
|
return async (req, res, next) => {
|
||||||
|
const user = req.user;
|
||||||
|
const tier = user?.tier || 'free';
|
||||||
|
const limit = limits[tier];
|
||||||
|
|
||||||
|
const key = \`rl:user:\${user.userId}\`;
|
||||||
|
const current = await redis.incr(key);
|
||||||
|
|
||||||
|
if (current === 1) {
|
||||||
|
await redis.expire(key, limit.windowMs / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current > limit.max) {
|
||||||
|
return res.status(429).json({
|
||||||
|
error: 'Rate limit exceeded',
|
||||||
|
limit: limit.max,
|
||||||
|
remaining: 0,
|
||||||
|
reset: await redis.ttl(key)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set rate limit headers
|
||||||
|
res.set({
|
||||||
|
'X-RateLimit-Limit': limit.max,
|
||||||
|
'X-RateLimit-Remaining': limit.max - current,
|
||||||
|
'X-RateLimit-Reset': await redis.ttl(key)
|
||||||
|
});
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use('/api/', authenticateToken, createTieredRateLimiter());
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### DDoS Protection with Helmet
|
||||||
|
|
||||||
|
\`\`\`javascript
|
||||||
|
const helmet = require('helmet');
|
||||||
|
|
||||||
|
app.use(helmet({
|
||||||
|
// Content Security Policy
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||||
|
scriptSrc: ["'self'"],
|
||||||
|
imgSrc: ["'self'", 'data:', 'https:']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Prevent clickjacking
|
||||||
|
frameguard: { action: 'deny' },
|
||||||
|
// Hide X-Powered-By header
|
||||||
|
hidePoweredBy: true,
|
||||||
|
// Prevent MIME type sniffing
|
||||||
|
noSniff: true,
|
||||||
|
// Enable HSTS
|
||||||
|
hsts: {
|
||||||
|
maxAge: 31536000,
|
||||||
|
includeSubDomains: true,
|
||||||
|
preload: true
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Rate Limit Response Headers
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
X-RateLimit-Limit: 100
|
||||||
|
X-RateLimit-Remaining: 87
|
||||||
|
X-RateLimit-Reset: 1640000000
|
||||||
|
Retry-After: 900
|
||||||
|
\`\`\`
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### ✅ Do This
|
||||||
|
|
||||||
|
- **Use HTTPS Everywhere** - Never send sensitive data over HTTP
|
||||||
|
- **Implement Authentication** - Require authentication for protected endpoints
|
||||||
|
- **Validate All Inputs** - Never trust user input
|
||||||
|
- **Use Parameterized Queries** - Prevent SQL injection
|
||||||
|
- **Implement Rate Limiting** - Protect against brute force and DDoS
|
||||||
|
- **Hash Passwords** - Use bcrypt with salt rounds >= 10
|
||||||
|
- **Use Short-Lived Tokens** - JWT access tokens should expire quickly
|
||||||
|
- **Implement CORS Properly** - Only allow trusted origins
|
||||||
|
- **Log Security Events** - Monitor for suspicious activity
|
||||||
|
- **Keep Dependencies Updated** - Regularly update packages
|
||||||
|
- **Use Security Headers** - Implement Helmet.js
|
||||||
|
- **Sanitize Error Messages** - Don't leak sensitive information
|
||||||
|
|
||||||
|
### ❌ Don't Do This
|
||||||
|
|
||||||
|
- **Don't Store Passwords in Plain Text** - Always hash passwords
|
||||||
|
- **Don't Use Weak Secrets** - Use strong, random JWT secrets
|
||||||
|
- **Don't Trust User Input** - Always validate and sanitize
|
||||||
|
- **Don't Expose Stack Traces** - Hide error details in production
|
||||||
|
- **Don't Use String Concatenation for SQL** - Use parameterized queries
|
||||||
|
- **Don't Store Sensitive Data in JWT** - JWTs are not encrypted
|
||||||
|
- **Don't Ignore Security Updates** - Update dependencies regularly
|
||||||
|
- **Don't Use Default Credentials** - Change all default passwords
|
||||||
|
- **Don't Disable CORS Completely** - Configure it properly instead
|
||||||
|
- **Don't Log Sensitive Data** - Sanitize logs
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### Problem: JWT Secret Exposed in Code
|
||||||
|
**Symptoms:** JWT secret hardcoded or committed to Git
|
||||||
|
**Solution:**
|
||||||
|
\`\`\`javascript
|
||||||
|
// ❌ Bad
|
||||||
|
const JWT_SECRET = 'my-secret-key';
|
||||||
|
|
||||||
|
// ✅ Good
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET;
|
||||||
|
if (!JWT_SECRET) {
|
||||||
|
throw new Error('JWT_SECRET environment variable is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate strong secret
|
||||||
|
// node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Problem: Weak Password Requirements
|
||||||
|
**Symptoms:** Users can set weak passwords like "password123"
|
||||||
|
**Solution:**
|
||||||
|
\`\`\`javascript
|
||||||
|
const passwordSchema = z.string()
|
||||||
|
.min(12, 'Password must be at least 12 characters')
|
||||||
|
.regex(/[A-Z]/, 'Must contain uppercase letter')
|
||||||
|
.regex(/[a-z]/, 'Must contain lowercase letter')
|
||||||
|
.regex(/[0-9]/, 'Must contain number')
|
||||||
|
.regex(/[^A-Za-z0-9]/, 'Must contain special character');
|
||||||
|
|
||||||
|
// Or use a password strength library
|
||||||
|
const zxcvbn = require('zxcvbn');
|
||||||
|
const result = zxcvbn(password);
|
||||||
|
if (result.score < 3) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Password too weak',
|
||||||
|
suggestions: result.feedback.suggestions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Problem: Missing Authorization Checks
|
||||||
|
**Symptoms:** Users can access resources they shouldn't
|
||||||
|
**Solution:**
|
||||||
|
\`\`\`javascript
|
||||||
|
// ❌ Bad: Only checks authentication
|
||||||
|
app.delete('/api/posts/:id', authenticateToken, async (req, res) => {
|
||||||
|
await prisma.post.delete({ where: { id: req.params.id } });
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Good: Checks both authentication and authorization
|
||||||
|
app.delete('/api/posts/:id', authenticateToken, async (req, res) => {
|
||||||
|
const post = await prisma.post.findUnique({
|
||||||
|
where: { id: req.params.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
return res.status(404).json({ error: 'Post not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user owns the post or is admin
|
||||||
|
if (post.userId !== req.user.userId && req.user.role !== 'admin') {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: 'Not authorized to delete this post'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.post.delete({ where: { id: req.params.id } });
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Problem: Verbose Error Messages
|
||||||
|
**Symptoms:** Error messages reveal system details
|
||||||
|
**Solution:**
|
||||||
|
\`\`\`javascript
|
||||||
|
// ❌ Bad: Exposes database details
|
||||||
|
app.post('/api/users', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await prisma.user.create({ data: req.body });
|
||||||
|
res.json(user);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
// Error: "Unique constraint failed on the fields: (`email`)"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Good: Generic error message
|
||||||
|
app.post('/api/users', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await prisma.user.create({ data: req.body });
|
||||||
|
res.json(user);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('User creation error:', error); // Log full error
|
||||||
|
|
||||||
|
if (error.code === 'P2002') {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Email already exists'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'An error occurred while creating user'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Security Checklist
|
||||||
|
|
||||||
|
### Authentication & Authorization
|
||||||
|
- [ ] Implement strong authentication (JWT, OAuth 2.0)
|
||||||
|
- [ ] Use HTTPS for all endpoints
|
||||||
|
- [ ] Hash passwords with bcrypt (salt rounds >= 10)
|
||||||
|
- [ ] Implement token expiration
|
||||||
|
- [ ] Add refresh token mechanism
|
||||||
|
- [ ] Verify user authorization for each request
|
||||||
|
- [ ] Implement role-based access control (RBAC)
|
||||||
|
|
||||||
|
### Input Validation
|
||||||
|
- [ ] Validate all user inputs
|
||||||
|
- [ ] Use parameterized queries or ORM
|
||||||
|
- [ ] Sanitize HTML content
|
||||||
|
- [ ] Validate file uploads
|
||||||
|
- [ ] Implement request schema validation
|
||||||
|
- [ ] Use allowlists, not blocklists
|
||||||
|
|
||||||
|
### Rate Limiting & DDoS Protection
|
||||||
|
- [ ] Implement rate limiting per user/IP
|
||||||
|
- [ ] Add stricter limits for auth endpoints
|
||||||
|
- [ ] Use Redis for distributed rate limiting
|
||||||
|
- [ ] Return proper rate limit headers
|
||||||
|
- [ ] Implement request throttling
|
||||||
|
|
||||||
|
### Data Protection
|
||||||
|
- [ ] Use HTTPS/TLS for all traffic
|
||||||
|
- [ ] Encrypt sensitive data at rest
|
||||||
|
- [ ] Don't store sensitive data in JWT
|
||||||
|
- [ ] Sanitize error messages
|
||||||
|
- [ ] Implement proper CORS configuration
|
||||||
|
- [ ] Use security headers (Helmet.js)
|
||||||
|
|
||||||
|
### Monitoring & Logging
|
||||||
|
- [ ] Log security events
|
||||||
|
- [ ] Monitor for suspicious activity
|
||||||
|
- [ ] Set up alerts for failed auth attempts
|
||||||
|
- [ ] Track API usage patterns
|
||||||
|
- [ ] Don't log sensitive data
|
||||||
|
|
||||||
|
## OWASP API Security Top 10
|
||||||
|
|
||||||
|
1. **Broken Object Level Authorization** - Always verify user can access resource
|
||||||
|
2. **Broken Authentication** - Implement strong authentication mechanisms
|
||||||
|
3. **Broken Object Property Level Authorization** - Validate which properties user can access
|
||||||
|
4. **Unrestricted Resource Consumption** - Implement rate limiting and quotas
|
||||||
|
5. **Broken Function Level Authorization** - Verify user role for each function
|
||||||
|
6. **Unrestricted Access to Sensitive Business Flows** - Protect critical workflows
|
||||||
|
7. **Server Side Request Forgery (SSRF)** - Validate and sanitize URLs
|
||||||
|
8. **Security Misconfiguration** - Use security best practices and headers
|
||||||
|
9. **Improper Inventory Management** - Document and secure all API endpoints
|
||||||
|
10. **Unsafe Consumption of APIs** - Validate data from third-party APIs
|
||||||
|
|
||||||
|
## Related Skills
|
||||||
|
|
||||||
|
- `@ethical-hacking-methodology` - Security testing perspective
|
||||||
|
- `@sql-injection-testing` - Testing for SQL injection
|
||||||
|
- `@xss-html-injection` - Testing for XSS vulnerabilities
|
||||||
|
- `@broken-authentication` - Authentication vulnerabilities
|
||||||
|
- `@backend-dev-guidelines` - Backend development standards
|
||||||
|
- `@systematic-debugging` - Debug security issues
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [OWASP API Security Top 10](https://owasp.org/www-project-api-security/)
|
||||||
|
- [JWT Best Practices](https://tools.ietf.org/html/rfc8725)
|
||||||
|
- [Express Security Best Practices](https://expressjs.com/en/advanced/best-practice-security.html)
|
||||||
|
- [Node.js Security Checklist](https://blog.risingstack.com/node-js-security-checklist/)
|
||||||
|
- [API Security Checklist](https://github.com/shieldfy/API-Security-Checklist)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Pro Tip:** Security is not a one-time task - regularly audit your APIs, keep dependencies updated, and stay informed about new vulnerabilities!
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
name: app-store-changelog
|
||||||
|
description: Generate user-facing App Store release notes from git history since the last tag.
|
||||||
|
risk: safe
|
||||||
|
source: "Dimillian/Skills (MIT)"
|
||||||
|
date_added: "2026-03-25"
|
||||||
|
---
|
||||||
|
|
||||||
|
# App Store Changelog
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Generate a comprehensive, user-facing changelog from git history since the last tag, then translate commits into clear App Store release notes.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
- When the user asks for App Store "What's New" text or release notes from git history.
|
||||||
|
- When you need to turn raw commits into concise, user-facing release bullets.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 1) Collect changes
|
||||||
|
- Run `scripts/collect_release_changes.sh` from the repo root to gather commits and touched files.
|
||||||
|
- If needed, pass a specific tag or ref: `scripts/collect_release_changes.sh v1.2.3 HEAD`.
|
||||||
|
- If no tags exist, the script falls back to full history.
|
||||||
|
|
||||||
|
### 2) Triage for user impact
|
||||||
|
- Scan commits and files to identify user-visible changes.
|
||||||
|
- Group changes by theme (New, Improved, Fixed) and deduplicate overlaps.
|
||||||
|
- Drop internal-only work (build scripts, refactors, dependency bumps, CI).
|
||||||
|
|
||||||
|
### 3) Draft App Store notes
|
||||||
|
- Write short, benefit-focused bullets for each user-facing change.
|
||||||
|
- Use clear verbs and plain language; avoid internal jargon.
|
||||||
|
- Prefer 5 to 10 bullets unless the user requests a different length.
|
||||||
|
|
||||||
|
### 4) Validate
|
||||||
|
- Ensure every bullet maps back to a real change in the range.
|
||||||
|
- Check for duplicates and overly technical wording.
|
||||||
|
- Ask for clarification if any change is ambiguous or possibly internal-only.
|
||||||
|
|
||||||
|
## Commit-to-Bullet Examples
|
||||||
|
|
||||||
|
The following shows how raw commits are translated into App Store bullets:
|
||||||
|
|
||||||
|
| Raw commit message | App Store bullet |
|
||||||
|
|---|---|
|
||||||
|
| `fix(auth): resolve token refresh race condition on iOS 17` | • Fixed a login issue that could leave some users unexpectedly signed out. |
|
||||||
|
| `feat(search): add voice input to search bar` | • Search your library hands-free with the new voice input option. |
|
||||||
|
| `perf(timeline): lazy-load images to reduce scroll jank` | • Scrolling through your timeline is now smoother and faster. |
|
||||||
|
|
||||||
|
Internal-only commits that are **dropped** (no user impact):
|
||||||
|
- `chore: upgrade fastlane to 2.219`
|
||||||
|
- `refactor(network): extract URLSession wrapper into module`
|
||||||
|
- `ci: add nightly build job`
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
|
||||||
|
```
|
||||||
|
What's New in Version 3.4
|
||||||
|
|
||||||
|
• Search your library hands-free with the new voice input option.
|
||||||
|
• Scrolling through your timeline is now smoother and faster.
|
||||||
|
• Fixed a login issue that could leave some users unexpectedly signed out.
|
||||||
|
• Added dark-mode support to the settings screen.
|
||||||
|
• Improved load times when opening large photo albums.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
- Title (optional): "What's New" or product name + version.
|
||||||
|
- Bullet list only; one sentence per bullet.
|
||||||
|
- Stick to storefront limits if the user provides one.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
- `scripts/collect_release_changes.sh`: Collect commits and touched files since last tag.
|
||||||
|
- `references/release-notes-guidelines.md`: Language, filtering, and QA rules for App Store notes.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "App Store Changelog"
|
||||||
|
short_description: "Generate App Store release notes"
|
||||||
|
default_prompt: "Use $app-store-changelog to draft App Store release notes from the changes since the last tag."
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
# App Store Release Notes Guidelines
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
- Produce user-facing release notes that describe visible changes since the last tag.
|
||||||
|
- Include all user-impacting changes; omit purely internal or refactor-only work.
|
||||||
|
- Keep language plain, short, and benefit-focused.
|
||||||
|
|
||||||
|
## Output Shape
|
||||||
|
- Prefer 5 to 10 bullets total for most releases.
|
||||||
|
- Group by theme if needed: New, Improved, Fixed.
|
||||||
|
- Each bullet should be one sentence and start with a verb.
|
||||||
|
- Avoid internal codenames, ticket IDs, or file paths.
|
||||||
|
|
||||||
|
## Filtering Rules
|
||||||
|
- Include: new features, UI changes, behavior changes, bug fixes users would notice, performance improvements with visible impact.
|
||||||
|
- Exclude: refactors, dependency bumps, CI changes, developer tooling, internal logging, analytics changes unless they affect user privacy or behavior.
|
||||||
|
- If a change is ambiguous, ask for clarification or describe it as a small improvement only if it is user-visible.
|
||||||
|
|
||||||
|
## Language Guidance
|
||||||
|
- Translate technical terms into user-facing descriptions.
|
||||||
|
- Avoid versions of "API", "refactor", "nil", "crash log", or "dependency".
|
||||||
|
- Prefer "Improved", "Added", "Fixed", "Updated" or action verbs like "Search", "Upload", "Sync".
|
||||||
|
- Keep tense present or past: "Added", "Improved", "Fixed".
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
- "Added account switching from the profile menu."
|
||||||
|
- "Improved timeline loading speed on slow connections."
|
||||||
|
- "Fixed media attachments not opening in full screen."
|
||||||
|
|
||||||
|
## QA Checklist
|
||||||
|
- Every bullet ties to a real change in the range.
|
||||||
|
- No duplicate bullets that describe the same change.
|
||||||
|
- No internal jargon or file paths.
|
||||||
|
- Final list fits App Store text limits for the target storefront if provided.
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
since_ref="${1:-}"
|
||||||
|
until_ref="${2:-HEAD}"
|
||||||
|
|
||||||
|
if [[ -z "${since_ref}" ]]; then
|
||||||
|
if git describe --tags --abbrev=0 >/dev/null 2>&1; then
|
||||||
|
since_ref="$(git describe --tags --abbrev=0)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
range=""
|
||||||
|
if [[ -n "${since_ref}" ]]; then
|
||||||
|
range="${since_ref}..${until_ref}"
|
||||||
|
else
|
||||||
|
range="${until_ref}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
repo_root="$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
printf "Repo: %s\n" "${repo_root}"
|
||||||
|
if [[ -n "${since_ref}" ]]; then
|
||||||
|
printf "Range: %s..%s\n" "${since_ref}" "${until_ref}"
|
||||||
|
else
|
||||||
|
printf "Range: start..%s (no tags found)\n" "${until_ref}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "\n== Commits ==\n"
|
||||||
|
git log --reverse --date=short --pretty=format:'%h|%ad|%s' ${range}
|
||||||
|
|
||||||
|
printf "\n\n== Files Touched ==\n"
|
||||||
|
git log --reverse --name-only --pretty=format:'--- %h %s' ${range} | sed '/^$/d'
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
# How to Use the App Store Optimization Skill
|
||||||
|
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you help me optimize my app's presence on the App Store and Google Play?
|
||||||
|
|
||||||
|
## Example Invocations
|
||||||
|
|
||||||
|
### Keyword Research
|
||||||
|
|
||||||
|
**Example 1: Basic Keyword Research**
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you research the best keywords for my productivity app? I'm targeting professionals who need task management and team collaboration features.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example 2: Competitive Keyword Analysis**
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you analyze keywords that Todoist, Asana, and Monday.com are using? I want to find gaps and opportunities for my project management app.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metadata Optimization
|
||||||
|
|
||||||
|
**Example 3: Optimize App Title**
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you optimize my app title for the Apple App Store? My app is called "TaskFlow" and I want to rank for "task manager", "productivity", and "team collaboration". The title needs to be under 30 characters.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example 4: Full Metadata Package**
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you create optimized metadata for both Apple App Store and Google Play Store? Here's my app info:
|
||||||
|
- Name: TaskFlow
|
||||||
|
- Category: Productivity
|
||||||
|
- Key features: AI task prioritization, team collaboration, calendar integration
|
||||||
|
- Target keywords: task manager, productivity app, team tasks
|
||||||
|
```
|
||||||
|
|
||||||
|
### Competitor Analysis
|
||||||
|
|
||||||
|
**Example 5: Analyze Top Competitors**
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you analyze the ASO strategies of the top 5 productivity apps in the App Store? I want to understand their title strategies, keyword usage, and visual asset approaches.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example 6: Identify Competitive Gaps**
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you compare my app's ASO performance against competitors and identify what I'm missing? Here's my current metadata: [paste metadata]
|
||||||
|
```
|
||||||
|
|
||||||
|
### ASO Score Calculation
|
||||||
|
|
||||||
|
**Example 7: Calculate Overall ASO Health**
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you calculate my app's ASO health score? Here are my metrics:
|
||||||
|
- Average rating: 4.2 stars
|
||||||
|
- Total ratings: 3,500
|
||||||
|
- Keywords in top 10: 3
|
||||||
|
- Keywords in top 50: 12
|
||||||
|
- Conversion rate: 4.5%
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example 8: Identify Improvement Areas**
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. My ASO score is 62/100. Can you tell me which areas I should focus on first to improve my rankings and downloads?
|
||||||
|
```
|
||||||
|
|
||||||
|
### A/B Testing
|
||||||
|
|
||||||
|
**Example 9: Plan Icon Test**
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. I want to A/B test two different app icons. My current conversion rate is 5%. Can you help me plan the test, calculate required sample size, and determine how long to run it?
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example 10: Analyze Test Results**
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you analyze my A/B test results?
|
||||||
|
- Variant A (control): 2,500 visitors, 125 installs
|
||||||
|
- Variant B (new icon): 2,500 visitors, 150 installs
|
||||||
|
Is this statistically significant? Should I implement variant B?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
|
||||||
|
**Example 11: Plan Localization Strategy**
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. I currently only have English metadata. Which markets should I localize for first? I'm a bootstrapped startup with moderate budget.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example 12: Translate Metadata**
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you help me translate my app metadata to Spanish for the Mexico market? Here's my English metadata: [paste metadata]. Check if it fits within character limits.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Review Analysis
|
||||||
|
|
||||||
|
**Example 13: Analyze User Reviews**
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you analyze my recent reviews and tell me:
|
||||||
|
- Overall sentiment (positive/negative ratio)
|
||||||
|
- Most common complaints
|
||||||
|
- Most requested features
|
||||||
|
- Bugs that need immediate fixing
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example 14: Generate Review Response Templates**
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you create professional response templates for:
|
||||||
|
- Users reporting crashes
|
||||||
|
- Feature requests
|
||||||
|
- Positive 5-star reviews
|
||||||
|
- General complaints
|
||||||
|
```
|
||||||
|
|
||||||
|
### Launch Planning
|
||||||
|
|
||||||
|
**Example 15: Pre-Launch Checklist**
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you generate a comprehensive pre-launch checklist for both Apple App Store and Google Play Store? My launch date is December 1, 2025.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example 16: Optimize Launch Timing**
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. What's the best day and time to launch my fitness app? I want to maximize visibility and downloads in the first week.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example 17: Plan Seasonal Campaign**
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you identify seasonal opportunities for my fitness app? It's currently October—what campaigns should I run for the next 6 months?
|
||||||
|
```
|
||||||
|
|
||||||
|
## What to Provide
|
||||||
|
|
||||||
|
### For Keyword Research
|
||||||
|
- App name and category
|
||||||
|
- Target audience description
|
||||||
|
- Key features and unique value proposition
|
||||||
|
- Competitor apps (optional)
|
||||||
|
- Geographic markets to target
|
||||||
|
|
||||||
|
### For Metadata Optimization
|
||||||
|
- Current app name
|
||||||
|
- Platform (Apple, Google, or both)
|
||||||
|
- Target keywords (prioritized list)
|
||||||
|
- Key features and benefits
|
||||||
|
- Target audience
|
||||||
|
- Current metadata (for optimization)
|
||||||
|
|
||||||
|
### For Competitor Analysis
|
||||||
|
- Your app category
|
||||||
|
- List of competitor app names or IDs
|
||||||
|
- Platform (Apple or Google)
|
||||||
|
- Specific aspects to analyze (keywords, visuals, ratings)
|
||||||
|
|
||||||
|
### For ASO Score Calculation
|
||||||
|
- Metadata quality metrics (title length, description length, keyword density)
|
||||||
|
- Rating data (average rating, total ratings, recent ratings)
|
||||||
|
- Keyword rankings (top 10, top 50, top 100 counts)
|
||||||
|
- Conversion metrics (impression-to-install rate, downloads)
|
||||||
|
|
||||||
|
### For A/B Testing
|
||||||
|
- Test type (icon, screenshot, title, description)
|
||||||
|
- Control variant details
|
||||||
|
- Test variant details
|
||||||
|
- Baseline conversion rate
|
||||||
|
- For results analysis: visitor and conversion counts for both variants
|
||||||
|
|
||||||
|
### For Localization
|
||||||
|
- Current market and language
|
||||||
|
- Budget level (low, medium, high)
|
||||||
|
- Target number of markets
|
||||||
|
- Current metadata text for translation
|
||||||
|
|
||||||
|
### For Review Analysis
|
||||||
|
- Recent reviews (text, rating, date)
|
||||||
|
- Platform (Apple or Google)
|
||||||
|
- Time period to analyze
|
||||||
|
- Specific focus (bugs, features, sentiment)
|
||||||
|
|
||||||
|
### For Launch Planning
|
||||||
|
- Platform (Apple, Google, or both)
|
||||||
|
- Target launch date
|
||||||
|
- App category
|
||||||
|
- App information (name, features, target audience)
|
||||||
|
|
||||||
|
## What You'll Get
|
||||||
|
|
||||||
|
### Keyword Research Output
|
||||||
|
- Prioritized keyword list with search volume estimates
|
||||||
|
- Competition level analysis
|
||||||
|
- Relevance scores
|
||||||
|
- Long-tail keyword opportunities
|
||||||
|
- Strategic recommendations
|
||||||
|
|
||||||
|
### Metadata Optimization Output
|
||||||
|
- Optimized titles (multiple options)
|
||||||
|
- Optimized descriptions (short and full)
|
||||||
|
- Keyword field optimization (Apple)
|
||||||
|
- Character count validation
|
||||||
|
- Keyword density analysis
|
||||||
|
- Before/after comparison
|
||||||
|
|
||||||
|
### Competitor Analysis Output
|
||||||
|
- Ranked competitors by ASO strength
|
||||||
|
- Common keyword patterns
|
||||||
|
- Keyword gaps and opportunities
|
||||||
|
- Visual asset assessment
|
||||||
|
- Best practices identified
|
||||||
|
- Actionable recommendations
|
||||||
|
|
||||||
|
### ASO Score Output
|
||||||
|
- Overall score (0-100)
|
||||||
|
- Breakdown by category (metadata, ratings, keywords, conversion)
|
||||||
|
- Strengths and weaknesses
|
||||||
|
- Prioritized action items
|
||||||
|
- Expected impact of improvements
|
||||||
|
|
||||||
|
### A/B Test Output
|
||||||
|
- Test design with hypothesis
|
||||||
|
- Required sample size calculation
|
||||||
|
- Duration estimates
|
||||||
|
- Statistical significance analysis
|
||||||
|
- Implementation recommendations
|
||||||
|
- Learnings and insights
|
||||||
|
|
||||||
|
### Localization Output
|
||||||
|
- Prioritized target markets
|
||||||
|
- Estimated translation costs
|
||||||
|
- ROI projections
|
||||||
|
- Character limit validation for each language
|
||||||
|
- Cultural adaptation recommendations
|
||||||
|
- Phased implementation plan
|
||||||
|
|
||||||
|
### Review Analysis Output
|
||||||
|
- Sentiment distribution (positive/neutral/negative)
|
||||||
|
- Common themes and topics
|
||||||
|
- Top issues requiring fixes
|
||||||
|
- Most requested features
|
||||||
|
- Response templates
|
||||||
|
- Trend analysis over time
|
||||||
|
|
||||||
|
### Launch Planning Output
|
||||||
|
- Platform-specific checklists (Apple, Google, Universal)
|
||||||
|
- Timeline with milestones
|
||||||
|
- Compliance validation
|
||||||
|
- Optimal launch timing recommendations
|
||||||
|
- Seasonal campaign opportunities
|
||||||
|
- Update cadence planning
|
||||||
|
|
||||||
|
## Tips for Best Results
|
||||||
|
|
||||||
|
1. **Be Specific**: Provide as much detail about your app as possible
|
||||||
|
2. **Include Context**: Share your goals (increase downloads, improve ranking, boost conversion)
|
||||||
|
3. **Provide Data**: Real metrics enable more accurate analysis
|
||||||
|
4. **Iterate**: Start with keyword research, then optimize metadata, then test
|
||||||
|
5. **Track Results**: Monitor changes after implementing recommendations
|
||||||
|
6. **Stay Compliant**: Always verify recommendations against current App Store/Play Store guidelines
|
||||||
|
7. **Test First**: Use A/B testing before making major metadata changes
|
||||||
|
8. **Localize Strategically**: Start with highest-ROI markets first
|
||||||
|
9. **Respond to Reviews**: Use provided templates to engage with users
|
||||||
|
10. **Plan Ahead**: Use launch checklists and timelines to avoid last-minute rushes
|
||||||
|
|
||||||
|
## Common Workflows
|
||||||
|
|
||||||
|
### New App Launch
|
||||||
|
1. Keyword research → Competitor analysis → Metadata optimization → Pre-launch checklist → Launch timing optimization
|
||||||
|
|
||||||
|
### Improving Existing App
|
||||||
|
1. ASO score calculation → Identify gaps → Metadata optimization → A/B testing → Review analysis → Implement changes
|
||||||
|
|
||||||
|
### International Expansion
|
||||||
|
1. Localization planning → Market prioritization → Metadata translation → ROI analysis → Phased rollout
|
||||||
|
|
||||||
|
### Ongoing Optimization
|
||||||
|
1. Monthly keyword ranking tracking → Quarterly metadata updates → Continuous A/B testing → Review monitoring → Seasonal campaigns
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
If you need clarification on any aspect of ASO or want to combine multiple analyses, just ask! For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you create a complete ASO strategy for my new productivity app? I need keyword research, optimized metadata for both stores, a pre-launch checklist, and launch timing recommendations.
|
||||||
|
```
|
||||||
|
|
||||||
|
The skill can handle comprehensive, multi-phase ASO projects as well as specific tactical optimizations.
|
||||||
@@ -0,0 +1,430 @@
|
|||||||
|
# App Store Optimization (ASO) Skill
|
||||||
|
|
||||||
|
**Version**: 1.0.0
|
||||||
|
**Last Updated**: November 7, 2025
|
||||||
|
**Author**: Claude Skills Factory
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
A comprehensive App Store Optimization (ASO) skill that provides complete capabilities for researching, optimizing, and tracking mobile app performance on the Apple App Store and Google Play Store. This skill empowers app developers and marketers to maximize their app's visibility, downloads, and success in competitive app marketplaces.
|
||||||
|
|
||||||
|
## What This Skill Does
|
||||||
|
|
||||||
|
This skill provides end-to-end ASO capabilities across seven key areas:
|
||||||
|
|
||||||
|
1. **Research & Analysis**: Keyword research, competitor analysis, market trends, review sentiment
|
||||||
|
2. **Metadata Optimization**: Title, description, keywords with platform-specific character limits
|
||||||
|
3. **Conversion Optimization**: A/B testing framework, visual asset optimization
|
||||||
|
4. **Rating & Review Management**: Sentiment analysis, response strategies, issue identification
|
||||||
|
5. **Launch & Update Strategies**: Pre-launch checklists, timing optimization, update planning
|
||||||
|
6. **Analytics & Tracking**: ASO scoring, keyword rankings, performance benchmarking
|
||||||
|
7. **Localization**: Multi-language strategy, translation management, ROI analysis
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### Comprehensive Keyword Research
|
||||||
|
- Search volume and competition analysis
|
||||||
|
- Long-tail keyword discovery
|
||||||
|
- Competitor keyword extraction
|
||||||
|
- Keyword difficulty scoring
|
||||||
|
- Strategic prioritization
|
||||||
|
|
||||||
|
### Platform-Specific Metadata Optimization
|
||||||
|
- **Apple App Store**:
|
||||||
|
- Title (30 chars)
|
||||||
|
- Subtitle (30 chars)
|
||||||
|
- Promotional Text (170 chars)
|
||||||
|
- Description (4000 chars)
|
||||||
|
- Keywords field (100 chars)
|
||||||
|
- **Google Play Store**:
|
||||||
|
- Title (50 chars)
|
||||||
|
- Short Description (80 chars)
|
||||||
|
- Full Description (4000 chars)
|
||||||
|
- Character limit validation
|
||||||
|
- Keyword density analysis
|
||||||
|
- Multiple optimization strategies
|
||||||
|
|
||||||
|
### Competitor Intelligence
|
||||||
|
- Automated competitor discovery
|
||||||
|
- Metadata strategy analysis
|
||||||
|
- Visual asset assessment
|
||||||
|
- Gap identification
|
||||||
|
- Competitive positioning
|
||||||
|
|
||||||
|
### ASO Health Scoring
|
||||||
|
- 0-100 overall score
|
||||||
|
- Four-category breakdown (Metadata, Ratings, Keywords, Conversion)
|
||||||
|
- Strengths and weaknesses identification
|
||||||
|
- Prioritized action recommendations
|
||||||
|
- Expected impact estimates
|
||||||
|
|
||||||
|
### Scientific A/B Testing
|
||||||
|
- Test design and hypothesis formulation
|
||||||
|
- Sample size calculation
|
||||||
|
- Statistical significance analysis
|
||||||
|
- Duration estimation
|
||||||
|
- Implementation recommendations
|
||||||
|
|
||||||
|
### Global Localization
|
||||||
|
- Market prioritization (Tier 1/2/3)
|
||||||
|
- Translation cost estimation
|
||||||
|
- Character limit adaptation by language
|
||||||
|
- Cultural keyword considerations
|
||||||
|
- ROI analysis
|
||||||
|
|
||||||
|
### Review Intelligence
|
||||||
|
- Sentiment analysis
|
||||||
|
- Common theme extraction
|
||||||
|
- Bug and issue identification
|
||||||
|
- Feature request clustering
|
||||||
|
- Professional response templates
|
||||||
|
|
||||||
|
### Launch Planning
|
||||||
|
- Platform-specific checklists
|
||||||
|
- Timeline generation
|
||||||
|
- Compliance validation
|
||||||
|
- Optimal timing recommendations
|
||||||
|
- Seasonal campaign planning
|
||||||
|
|
||||||
|
## Python Modules
|
||||||
|
|
||||||
|
This skill includes 8 powerful Python modules:
|
||||||
|
|
||||||
|
### 1. keyword_analyzer.py
|
||||||
|
**Purpose**: Analyzes keywords for search volume, competition, and relevance
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
- `analyze_keyword()`: Single keyword analysis
|
||||||
|
- `compare_keywords()`: Multi-keyword comparison and ranking
|
||||||
|
- `find_long_tail_opportunities()`: Generate long-tail variations
|
||||||
|
- `calculate_keyword_density()`: Analyze keyword usage in text
|
||||||
|
- `extract_keywords_from_text()`: Extract keywords from reviews/descriptions
|
||||||
|
|
||||||
|
### 2. metadata_optimizer.py
|
||||||
|
**Purpose**: Optimizes titles, descriptions, keywords with character limit validation
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
- `optimize_title()`: Generate optimal title options
|
||||||
|
- `optimize_description()`: Create conversion-focused descriptions
|
||||||
|
- `optimize_keyword_field()`: Maximize Apple's 100-char keyword field
|
||||||
|
- `validate_character_limits()`: Ensure platform compliance
|
||||||
|
- `calculate_keyword_density()`: Analyze keyword integration
|
||||||
|
|
||||||
|
### 3. competitor_analyzer.py
|
||||||
|
**Purpose**: Analyzes competitor ASO strategies
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
- `analyze_competitor()`: Single competitor deep-dive
|
||||||
|
- `compare_competitors()`: Multi-competitor analysis
|
||||||
|
- `identify_gaps()`: Find competitive opportunities
|
||||||
|
- `_calculate_competitive_strength()`: Score competitor ASO quality
|
||||||
|
|
||||||
|
### 4. aso_scorer.py
|
||||||
|
**Purpose**: Calculates comprehensive ASO health score
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
- `calculate_overall_score()`: 0-100 ASO health score
|
||||||
|
- `score_metadata_quality()`: Evaluate metadata optimization
|
||||||
|
- `score_ratings_reviews()`: Assess rating quality and volume
|
||||||
|
- `score_keyword_performance()`: Analyze ranking positions
|
||||||
|
- `score_conversion_metrics()`: Evaluate conversion rates
|
||||||
|
- `generate_recommendations()`: Prioritized improvement actions
|
||||||
|
|
||||||
|
### 5. ab_test_planner.py
|
||||||
|
**Purpose**: Plans and tracks A/B tests for ASO elements
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
- `design_test()`: Create test hypothesis and structure
|
||||||
|
- `calculate_sample_size()`: Determine required visitors
|
||||||
|
- `calculate_significance()`: Assess statistical validity
|
||||||
|
- `track_test_results()`: Monitor ongoing tests
|
||||||
|
- `generate_test_report()`: Create comprehensive test reports
|
||||||
|
|
||||||
|
### 6. localization_helper.py
|
||||||
|
**Purpose**: Manages multi-language ASO optimization
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
- `identify_target_markets()`: Prioritize localization markets
|
||||||
|
- `translate_metadata()`: Adapt metadata for languages
|
||||||
|
- `adapt_keywords()`: Cultural keyword adaptation
|
||||||
|
- `validate_translations()`: Character limit validation
|
||||||
|
- `calculate_localization_roi()`: Estimate investment returns
|
||||||
|
|
||||||
|
### 7. review_analyzer.py
|
||||||
|
**Purpose**: Analyzes user reviews for actionable insights
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
- `analyze_sentiment()`: Calculate sentiment distribution
|
||||||
|
- `extract_common_themes()`: Identify frequent topics
|
||||||
|
- `identify_issues()`: Surface bugs and problems
|
||||||
|
- `find_feature_requests()`: Extract desired features
|
||||||
|
- `track_sentiment_trends()`: Monitor changes over time
|
||||||
|
- `generate_response_templates()`: Create review responses
|
||||||
|
|
||||||
|
### 8. launch_checklist.py
|
||||||
|
**Purpose**: Generates comprehensive launch and update checklists
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
- `generate_prelaunch_checklist()`: Complete submission validation
|
||||||
|
- `validate_app_store_compliance()`: Check guidelines compliance
|
||||||
|
- `create_update_plan()`: Plan update cadence
|
||||||
|
- `optimize_launch_timing()`: Recommend launch dates
|
||||||
|
- `plan_seasonal_campaigns()`: Identify seasonal opportunities
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### For Claude Code (Desktop/CLI)
|
||||||
|
|
||||||
|
#### Project-Level Installation
|
||||||
|
```bash
|
||||||
|
# Copy skill folder to project
|
||||||
|
cp -r app-store-optimization /path/to/your/project/.claude/skills/
|
||||||
|
|
||||||
|
# Claude will auto-load the skill when working in this project
|
||||||
|
```
|
||||||
|
|
||||||
|
#### User-Level Installation (Available in All Projects)
|
||||||
|
```bash
|
||||||
|
# Copy skill folder to user-level skills
|
||||||
|
cp -r app-store-optimization ~/.claude/skills/
|
||||||
|
|
||||||
|
# Claude will load this skill in all your projects
|
||||||
|
```
|
||||||
|
|
||||||
|
### For Claude Apps (Browser)
|
||||||
|
|
||||||
|
1. Use the `skill-creator` skill to import the skill
|
||||||
|
2. Or manually import via Claude Apps interface
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
To verify installation:
|
||||||
|
```bash
|
||||||
|
# Check if skill folder exists
|
||||||
|
ls ~/.claude/skills/app-store-optimization/
|
||||||
|
|
||||||
|
# You should see:
|
||||||
|
# SKILL.md
|
||||||
|
# keyword_analyzer.py
|
||||||
|
# metadata_optimizer.py
|
||||||
|
# competitor_analyzer.py
|
||||||
|
# aso_scorer.py
|
||||||
|
# ab_test_planner.py
|
||||||
|
# localization_helper.py
|
||||||
|
# review_analyzer.py
|
||||||
|
# launch_checklist.py
|
||||||
|
# sample_input.json
|
||||||
|
# expected_output.json
|
||||||
|
# HOW_TO_USE.md
|
||||||
|
# README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Example 1: Complete Keyword Research
|
||||||
|
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you research keywords for my fitness app? I'm targeting people who want home workouts, yoga, and meal planning. Analyze top competitors like Nike Training Club and Peloton.
|
||||||
|
```
|
||||||
|
|
||||||
|
**What Claude will do**:
|
||||||
|
- Use `keyword_analyzer.py` to research keywords
|
||||||
|
- Use `competitor_analyzer.py` to analyze Nike Training Club and Peloton
|
||||||
|
- Provide prioritized keyword list with search volumes, competition levels
|
||||||
|
- Identify gaps and long-tail opportunities
|
||||||
|
- Recommend primary keywords for title and secondary keywords for description
|
||||||
|
|
||||||
|
### Example 2: Optimize App Store Metadata
|
||||||
|
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Optimize my app's metadata for both Apple App Store and Google Play Store:
|
||||||
|
- App: FitFlow
|
||||||
|
- Category: Health & Fitness
|
||||||
|
- Features: AI workout plans, nutrition tracking, progress photos
|
||||||
|
- Keywords: fitness app, workout planner, home fitness
|
||||||
|
```
|
||||||
|
|
||||||
|
**What Claude will do**:
|
||||||
|
- Use `metadata_optimizer.py` to create optimized titles (multiple options)
|
||||||
|
- Generate platform-specific descriptions (short and full)
|
||||||
|
- Optimize Apple's 100-character keyword field
|
||||||
|
- Validate all character limits
|
||||||
|
- Calculate keyword density
|
||||||
|
- Provide before/after comparison
|
||||||
|
|
||||||
|
### Example 3: Calculate ASO Health Score
|
||||||
|
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Calculate my app's ASO score:
|
||||||
|
- Average rating: 4.3 stars (8,200 ratings)
|
||||||
|
- Keywords in top 10: 4
|
||||||
|
- Keywords in top 50: 15
|
||||||
|
- Conversion rate: 3.8%
|
||||||
|
- Title: "FitFlow - Home Workouts"
|
||||||
|
- Description: 1,500 characters with 3 keyword mentions
|
||||||
|
```
|
||||||
|
|
||||||
|
**What Claude will do**:
|
||||||
|
- Use `aso_scorer.py` to calculate overall score (0-100)
|
||||||
|
- Break down by category (Metadata: X/25, Ratings: X/25, Keywords: X/25, Conversion: X/25)
|
||||||
|
- Identify strengths and weaknesses
|
||||||
|
- Generate prioritized recommendations
|
||||||
|
- Estimate impact of improvements
|
||||||
|
|
||||||
|
### Example 4: A/B Test Planning
|
||||||
|
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. I want to A/B test my app icon. My current conversion rate is 4.2%. How many visitors do I need and how long should I run the test?
|
||||||
|
```
|
||||||
|
|
||||||
|
**What Claude will do**:
|
||||||
|
- Use `ab_test_planner.py` to design test
|
||||||
|
- Calculate required sample size (based on minimum detectable effect)
|
||||||
|
- Estimate test duration for low/medium/high traffic scenarios
|
||||||
|
- Provide test structure and success metrics
|
||||||
|
- Explain how to analyze results
|
||||||
|
|
||||||
|
### Example 5: Review Sentiment Analysis
|
||||||
|
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Analyze my last 500 reviews and tell me:
|
||||||
|
- Overall sentiment
|
||||||
|
- Most common complaints
|
||||||
|
- Top feature requests
|
||||||
|
- Bugs needing immediate fixes
|
||||||
|
```
|
||||||
|
|
||||||
|
**What Claude will do**:
|
||||||
|
- Use `review_analyzer.py` to process reviews
|
||||||
|
- Calculate sentiment distribution
|
||||||
|
- Extract common themes
|
||||||
|
- Identify and prioritize issues
|
||||||
|
- Cluster feature requests
|
||||||
|
- Generate response templates
|
||||||
|
|
||||||
|
### Example 6: Pre-Launch Checklist
|
||||||
|
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Generate a complete pre-launch checklist for both app stores. My launch date is March 15, 2026.
|
||||||
|
```
|
||||||
|
|
||||||
|
**What Claude will do**:
|
||||||
|
- Use `launch_checklist.py` to generate checklists
|
||||||
|
- Create Apple App Store checklist (metadata, assets, technical, legal)
|
||||||
|
- Create Google Play Store checklist (metadata, assets, technical, legal)
|
||||||
|
- Add universal checklist (marketing, QA, support)
|
||||||
|
- Generate timeline with milestones
|
||||||
|
- Calculate completion percentage
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Keyword Research
|
||||||
|
1. Start with 20-30 seed keywords
|
||||||
|
2. Analyze top 5 competitors in your category
|
||||||
|
3. Balance high-volume and long-tail keywords
|
||||||
|
4. Prioritize relevance over search volume
|
||||||
|
5. Update keyword research quarterly
|
||||||
|
|
||||||
|
### Metadata Optimization
|
||||||
|
1. Front-load keywords in title (first 15 characters most important)
|
||||||
|
2. Use every available character (don't waste space)
|
||||||
|
3. Write for humans first, search engines second
|
||||||
|
4. A/B test major changes before committing
|
||||||
|
5. Update descriptions with each major release
|
||||||
|
|
||||||
|
### A/B Testing
|
||||||
|
1. Test one element at a time (icon vs. screenshots vs. title)
|
||||||
|
2. Run tests to statistical significance (90%+ confidence)
|
||||||
|
3. Test high-impact elements first (icon has biggest impact)
|
||||||
|
4. Allow sufficient duration (at least 1 week, preferably 2-3)
|
||||||
|
5. Document learnings for future tests
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
1. Start with top 5 revenue markets (US, China, Japan, Germany, UK)
|
||||||
|
2. Use professional translators, not machine translation
|
||||||
|
3. Test translations with native speakers
|
||||||
|
4. Adapt keywords for cultural context
|
||||||
|
5. Monitor ROI by market
|
||||||
|
|
||||||
|
### Review Management
|
||||||
|
1. Respond to reviews within 24-48 hours
|
||||||
|
2. Always be professional, even with negative reviews
|
||||||
|
3. Address specific issues raised
|
||||||
|
4. Thank users for positive feedback
|
||||||
|
5. Use insights to prioritize product improvements
|
||||||
|
|
||||||
|
## Technical Requirements
|
||||||
|
|
||||||
|
- **Python**: 3.7+ (for Python modules)
|
||||||
|
- **Platform Support**: Apple App Store, Google Play Store
|
||||||
|
- **Data Formats**: JSON input/output
|
||||||
|
- **Dependencies**: Standard library only (no external packages required)
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
### Data Dependencies
|
||||||
|
- Keyword search volumes are estimates (no official Apple/Google data)
|
||||||
|
- Competitor data limited to publicly available information
|
||||||
|
- Review analysis requires access to public reviews
|
||||||
|
- Historical data may not be available for new apps
|
||||||
|
|
||||||
|
### Platform Constraints
|
||||||
|
- Apple: Metadata changes require app submission (except Promotional Text)
|
||||||
|
- Google: Metadata changes take 1-2 hours to index
|
||||||
|
- A/B testing requires significant traffic for statistical significance
|
||||||
|
- Store algorithms are proprietary and change without notice
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Does not include paid user acquisition (Apple Search Ads, Google Ads)
|
||||||
|
- Does not cover in-app analytics implementation
|
||||||
|
- Does not handle technical app development
|
||||||
|
- Focuses on organic discovery and conversion optimization
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Issue: Python modules not found
|
||||||
|
**Solution**: Ensure all .py files are in the same directory as SKILL.md
|
||||||
|
|
||||||
|
### Issue: Character limit validation failing
|
||||||
|
**Solution**: Check that you're using the correct platform ('apple' or 'google')
|
||||||
|
|
||||||
|
### Issue: Keyword research returning limited results
|
||||||
|
**Solution**: Provide more context about your app, features, and target audience
|
||||||
|
|
||||||
|
### Issue: ASO score seems inaccurate
|
||||||
|
**Solution**: Ensure you're providing accurate metrics (ratings, keyword rankings, conversion rate)
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
### Version 1.0.0 (November 7, 2025)
|
||||||
|
- Initial release
|
||||||
|
- 8 Python modules with comprehensive ASO capabilities
|
||||||
|
- Support for both Apple App Store and Google Play Store
|
||||||
|
- Keyword research, metadata optimization, competitor analysis
|
||||||
|
- ASO scoring, A/B testing, localization, review analysis
|
||||||
|
- Launch planning and seasonal campaign tools
|
||||||
|
|
||||||
|
## Support & Feedback
|
||||||
|
|
||||||
|
This skill is designed to help app developers and marketers succeed in competitive app marketplaces. For the best results:
|
||||||
|
|
||||||
|
1. Provide detailed context about your app
|
||||||
|
2. Include specific metrics when available
|
||||||
|
3. Ask follow-up questions for clarification
|
||||||
|
4. Iterate based on results
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
Developed by Claude Skills Factory
|
||||||
|
Based on industry-standard ASO best practices
|
||||||
|
Platform requirements current as of November 2025
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This skill is provided as-is for use with Claude Code and Claude Apps. Customize and extend as needed for your specific use cases.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ready to optimize your app?** Start with keyword research, then move to metadata optimization, and finally implement A/B testing for continuous improvement. The skill handles everything from pre-launch planning to ongoing optimization.
|
||||||
|
|
||||||
|
For detailed usage examples, see [HOW_TO_USE.md](HOW_TO_USE.md).
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
---
|
||||||
|
name: app-store-optimization
|
||||||
|
description: "Complete App Store Optimization (ASO) toolkit for researching, optimizing, and tracking mobile app performance on Apple App Store and Google Play Store"
|
||||||
|
risk: unknown
|
||||||
|
source: community
|
||||||
|
date_added: "2026-02-27"
|
||||||
|
---
|
||||||
|
|
||||||
|
# App Store Optimization (ASO) Skill
|
||||||
|
|
||||||
|
This comprehensive skill provides complete ASO capabilities for successfully launching and optimizing mobile applications on the Apple App Store and Google Play Store.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### Research & Analysis
|
||||||
|
- **Keyword Research**: Analyze keyword volume, competition, and relevance for app discovery
|
||||||
|
- **Competitor Analysis**: Deep-dive into top-performing apps in your category
|
||||||
|
- **Market Trend Analysis**: Identify emerging trends and opportunities in your app category
|
||||||
|
- **Review Sentiment Analysis**: Extract insights from user reviews to identify strengths and issues
|
||||||
|
- **Category Analysis**: Evaluate optimal category and subcategory placement strategies
|
||||||
|
|
||||||
|
### Metadata Optimization
|
||||||
|
- **Title Optimization**: Create compelling titles with optimal keyword placement (platform-specific character limits)
|
||||||
|
- **Description Optimization**: Craft both short and full descriptions that convert and rank
|
||||||
|
- **Subtitle/Promotional Text**: Optimize Apple-specific subtitle (30 chars) and promotional text (170 chars)
|
||||||
|
- **Keyword Field**: Maximize Apple's 100-character keyword field with strategic selection
|
||||||
|
- **Category Selection**: Data-driven recommendations for primary and secondary categories
|
||||||
|
- **Icon Best Practices**: Guidelines for designing high-converting app icons
|
||||||
|
- **Screenshot Optimization**: Strategies for creating screenshots that drive installs
|
||||||
|
- **Preview Video**: Best practices for app preview videos
|
||||||
|
- **Localization**: Multi-language optimization strategies for global reach
|
||||||
|
|
||||||
|
### Conversion Optimization
|
||||||
|
- **A/B Testing Framework**: Plan and track metadata experiments for continuous improvement
|
||||||
|
- **Visual Asset Testing**: Test icons, screenshots, and videos for maximum conversion
|
||||||
|
- **Store Listing Optimization**: Comprehensive page optimization for impression-to-install conversion
|
||||||
|
- **Call-to-Action**: Optimize CTAs in descriptions and promotional materials
|
||||||
|
|
||||||
|
### Rating & Review Management
|
||||||
|
- **Review Monitoring**: Track and analyze user reviews for actionable insights
|
||||||
|
- **Response Strategies**: Templates and best practices for responding to reviews
|
||||||
|
- **Rating Improvement**: Tactical approaches to improve app ratings organically
|
||||||
|
- **Issue Identification**: Surface common problems and feature requests from reviews
|
||||||
|
|
||||||
|
### Launch & Update Strategies
|
||||||
|
- **Pre-Launch Checklist**: Complete validation before submitting to stores
|
||||||
|
- **Launch Timing**: Optimize release timing for maximum visibility and downloads
|
||||||
|
- **Update Cadence**: Plan optimal update frequency and feature rollouts
|
||||||
|
- **Feature Announcements**: Craft "What's New" sections that re-engage users
|
||||||
|
- **Seasonal Optimization**: Leverage seasonal trends and events
|
||||||
|
|
||||||
|
### Analytics & Tracking
|
||||||
|
- **ASO Score**: Calculate overall ASO health score across multiple factors
|
||||||
|
- **Keyword Rankings**: Track keyword position changes over time
|
||||||
|
- **Conversion Metrics**: Monitor impression-to-install conversion rates
|
||||||
|
- **Download Velocity**: Track download trends and momentum
|
||||||
|
- **Performance Benchmarking**: Compare against category averages and competitors
|
||||||
|
|
||||||
|
### Platform-Specific Requirements
|
||||||
|
- **Apple App Store**:
|
||||||
|
- Title: 30 characters
|
||||||
|
- Subtitle: 30 characters
|
||||||
|
- Promotional Text: 170 characters (editable without app update)
|
||||||
|
- Description: 4,000 characters
|
||||||
|
- Keywords: 100 characters (comma-separated, no spaces)
|
||||||
|
- What's New: 4,000 characters
|
||||||
|
- **Google Play Store**:
|
||||||
|
- Title: 50 characters (formerly 30, increased in 2021)
|
||||||
|
- Short Description: 80 characters
|
||||||
|
- Full Description: 4,000 characters
|
||||||
|
- No separate keyword field (keywords extracted from title and description)
|
||||||
|
|
||||||
|
## Input Requirements
|
||||||
|
|
||||||
|
### Keyword Research
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"app_name": "MyApp",
|
||||||
|
"category": "Productivity",
|
||||||
|
"target_keywords": ["task manager", "productivity", "todo list"],
|
||||||
|
"competitors": ["Todoist", "Any.do", "Microsoft To Do"],
|
||||||
|
"language": "en-US"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metadata Optimization
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"platform": "apple" | "google",
|
||||||
|
"app_info": {
|
||||||
|
"name": "MyApp",
|
||||||
|
"category": "Productivity",
|
||||||
|
"target_audience": "Professionals aged 25-45",
|
||||||
|
"key_features": ["Task management", "Team collaboration", "AI assistance"],
|
||||||
|
"unique_value": "AI-powered task prioritization"
|
||||||
|
},
|
||||||
|
"current_metadata": {
|
||||||
|
"title": "Current Title",
|
||||||
|
"subtitle": "Current Subtitle",
|
||||||
|
"description": "Current description..."
|
||||||
|
},
|
||||||
|
"target_keywords": ["productivity", "task manager", "todo"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Review Analysis
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"app_id": "com.myapp.app",
|
||||||
|
"platform": "apple" | "google",
|
||||||
|
"date_range": "last_30_days" | "last_90_days" | "all_time",
|
||||||
|
"rating_filter": [1, 2, 3, 4, 5],
|
||||||
|
"language": "en"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ASO Score Calculation
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title_quality": 0.8,
|
||||||
|
"description_quality": 0.7,
|
||||||
|
"keyword_density": 0.6
|
||||||
|
},
|
||||||
|
"ratings": {
|
||||||
|
"average_rating": 4.5,
|
||||||
|
"total_ratings": 15000
|
||||||
|
},
|
||||||
|
"conversion": {
|
||||||
|
"impression_to_install": 0.05
|
||||||
|
},
|
||||||
|
"keyword_rankings": {
|
||||||
|
"top_10": 5,
|
||||||
|
"top_50": 12,
|
||||||
|
"top_100": 18
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Formats
|
||||||
|
|
||||||
|
### Keyword Research Report
|
||||||
|
- List of recommended keywords with search volume estimates
|
||||||
|
- Competition level analysis (low/medium/high)
|
||||||
|
- Relevance scores for each keyword
|
||||||
|
- Strategic recommendations for primary vs. secondary keywords
|
||||||
|
- Long-tail keyword opportunities
|
||||||
|
|
||||||
|
### Optimized Metadata Package
|
||||||
|
- Platform-specific title (with character count validation)
|
||||||
|
- Subtitle/promotional text (Apple)
|
||||||
|
- Short description (Google)
|
||||||
|
- Full description (both platforms)
|
||||||
|
- Keyword field (Apple - 100 chars)
|
||||||
|
- Character count validation for all fields
|
||||||
|
- Keyword density analysis
|
||||||
|
- Before/after comparison
|
||||||
|
|
||||||
|
### Competitor Analysis Report
|
||||||
|
- Top 10 competitors in category
|
||||||
|
- Their metadata strategies
|
||||||
|
- Keyword overlap analysis
|
||||||
|
- Visual asset assessment
|
||||||
|
- Rating and review volume comparison
|
||||||
|
- Identified gaps and opportunities
|
||||||
|
|
||||||
|
### ASO Health Score
|
||||||
|
- Overall score (0-100)
|
||||||
|
- Category breakdown:
|
||||||
|
- Metadata Quality (0-25)
|
||||||
|
- Ratings & Reviews (0-25)
|
||||||
|
- Keyword Performance (0-25)
|
||||||
|
- Conversion Metrics (0-25)
|
||||||
|
- Specific improvement recommendations
|
||||||
|
- Priority action items
|
||||||
|
|
||||||
|
### A/B Test Plan
|
||||||
|
- Hypothesis and test variables
|
||||||
|
- Test duration recommendations
|
||||||
|
- Success metrics definition
|
||||||
|
- Sample size calculations
|
||||||
|
- Statistical significance thresholds
|
||||||
|
|
||||||
|
### Launch Checklist
|
||||||
|
- Pre-submission validation (all required assets, metadata)
|
||||||
|
- Store compliance verification
|
||||||
|
- Testing checklist (devices, OS versions)
|
||||||
|
- Marketing preparation items
|
||||||
|
- Post-launch monitoring plan
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
### Keyword Research
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you research the best keywords for a productivity app targeting professionals? Focus on keywords with good search volume but lower competition.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optimize App Store Listing
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you optimize my app's metadata for the Apple App Store? Here's my current listing: [provide current metadata]. I want to rank for "task management" and "productivity tools".
|
||||||
|
```
|
||||||
|
|
||||||
|
### Analyze Competitor Strategy
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you analyze the ASO strategies of Todoist, Any.do, and Microsoft To Do? I want to understand what they're doing well and where there are opportunities.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Review Sentiment Analysis
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you analyze recent reviews for my app (com.myapp.ios) and identify the most common user complaints and feature requests?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Calculate ASO Score
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you calculate my app's overall ASO health score and provide specific recommendations for improvement?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plan A/B Test
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. I want to A/B test my app icon and first screenshot. Can you help me design the test and determine how long to run it?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pre-Launch Checklist
|
||||||
|
```
|
||||||
|
Hey Claude—I just added the "app-store-optimization" skill. Can you generate a comprehensive pre-launch checklist for submitting my app to both Apple App Store and Google Play Store?
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
### keyword_analyzer.py
|
||||||
|
Analyzes keywords for search volume, competition, and relevance. Provides strategic recommendations for primary and secondary keywords.
|
||||||
|
|
||||||
|
**Key Functions:**
|
||||||
|
- `analyze_keyword()`: Analyze single keyword metrics
|
||||||
|
- `compare_keywords()`: Compare multiple keywords
|
||||||
|
- `find_long_tail()`: Discover long-tail keyword opportunities
|
||||||
|
- `calculate_keyword_difficulty()`: Assess competition level
|
||||||
|
|
||||||
|
### metadata_optimizer.py
|
||||||
|
Optimizes titles, descriptions, and keyword fields with platform-specific character limit validation.
|
||||||
|
|
||||||
|
**Key Functions:**
|
||||||
|
- `optimize_title()`: Create compelling, keyword-rich titles
|
||||||
|
- `optimize_description()`: Generate conversion-focused descriptions
|
||||||
|
- `optimize_keyword_field()`: Maximize Apple's 100-char keyword field
|
||||||
|
- `validate_character_limits()`: Ensure compliance with platform limits
|
||||||
|
- `calculate_keyword_density()`: Analyze keyword usage in metadata
|
||||||
|
|
||||||
|
### competitor_analyzer.py
|
||||||
|
Analyzes top competitors' ASO strategies and identifies opportunities.
|
||||||
|
|
||||||
|
**Key Functions:**
|
||||||
|
- `get_top_competitors()`: Identify category leaders
|
||||||
|
- `analyze_competitor_metadata()`: Extract and analyze competitor keywords
|
||||||
|
- `compare_visual_assets()`: Evaluate icons and screenshots
|
||||||
|
- `identify_gaps()`: Find competitive opportunities
|
||||||
|
|
||||||
|
### aso_scorer.py
|
||||||
|
Calculates comprehensive ASO health score across multiple dimensions.
|
||||||
|
|
||||||
|
**Key Functions:**
|
||||||
|
- `calculate_overall_score()`: Compute 0-100 ASO score
|
||||||
|
- `score_metadata_quality()`: Evaluate title, description, keywords
|
||||||
|
- `score_ratings_reviews()`: Assess rating quality and volume
|
||||||
|
- `score_keyword_performance()`: Analyze ranking positions
|
||||||
|
- `score_conversion_metrics()`: Evaluate impression-to-install rates
|
||||||
|
- `generate_recommendations()`: Provide prioritized action items
|
||||||
|
|
||||||
|
### ab_test_planner.py
|
||||||
|
Plans and tracks A/B tests for metadata and visual assets.
|
||||||
|
|
||||||
|
**Key Functions:**
|
||||||
|
- `design_test()`: Create test hypothesis and variables
|
||||||
|
- `calculate_sample_size()`: Determine required test duration
|
||||||
|
- `calculate_significance()`: Assess statistical significance
|
||||||
|
- `track_results()`: Monitor test performance
|
||||||
|
- `generate_report()`: Summarize test outcomes
|
||||||
|
|
||||||
|
### localization_helper.py
|
||||||
|
Manages multi-language ASO optimization strategies.
|
||||||
|
|
||||||
|
**Key Functions:**
|
||||||
|
- `identify_target_markets()`: Recommend localization priorities
|
||||||
|
- `translate_metadata()`: Generate localized metadata
|
||||||
|
- `adapt_keywords()`: Research locale-specific keywords
|
||||||
|
- `validate_translations()`: Check character limits per language
|
||||||
|
- `calculate_localization_roi()`: Estimate impact of localization
|
||||||
|
|
||||||
|
### review_analyzer.py
|
||||||
|
Analyzes user reviews for sentiment, issues, and feature requests.
|
||||||
|
|
||||||
|
**Key Functions:**
|
||||||
|
- `analyze_sentiment()`: Calculate positive/negative/neutral ratios
|
||||||
|
- `extract_common_themes()`: Identify frequently mentioned topics
|
||||||
|
- `identify_issues()`: Surface bugs and user complaints
|
||||||
|
- `find_feature_requests()`: Extract desired features
|
||||||
|
- `track_sentiment_trends()`: Monitor sentiment over time
|
||||||
|
- `generate_response_templates()`: Create review response drafts
|
||||||
|
|
||||||
|
### launch_checklist.py
|
||||||
|
Generates comprehensive pre-launch and update checklists.
|
||||||
|
|
||||||
|
**Key Functions:**
|
||||||
|
- `generate_prelaunch_checklist()`: Complete submission validation
|
||||||
|
- `validate_app_store_compliance()`: Check Apple guidelines
|
||||||
|
- `validate_play_store_compliance()`: Check Google policies
|
||||||
|
- `create_update_plan()`: Plan update cadence and features
|
||||||
|
- `optimize_launch_timing()`: Recommend release dates
|
||||||
|
- `plan_seasonal_campaigns()`: Identify seasonal opportunities
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Keyword Research
|
||||||
|
1. **Volume vs. Competition**: Balance high-volume keywords with achievable rankings
|
||||||
|
2. **Relevance First**: Only target keywords genuinely relevant to your app
|
||||||
|
3. **Long-Tail Strategy**: Include 3-4 word phrases with lower competition
|
||||||
|
4. **Continuous Research**: Keyword trends change—research quarterly
|
||||||
|
5. **Competitor Keywords**: Don't copy blindly; ensure relevance to your features
|
||||||
|
|
||||||
|
### Metadata Optimization
|
||||||
|
1. **Front-Load Keywords**: Place most important keywords early in title/description
|
||||||
|
2. **Natural Language**: Write for humans first, SEO second
|
||||||
|
3. **Feature Benefits**: Focus on user benefits, not just features
|
||||||
|
4. **A/B Test Everything**: Test titles, descriptions, screenshots systematically
|
||||||
|
5. **Update Regularly**: Refresh metadata every major update
|
||||||
|
6. **Character Limits**: Use every character—don't waste valuable space
|
||||||
|
7. **Apple Keyword Field**: No plurals, duplicates, or spaces between commas
|
||||||
|
|
||||||
|
### Visual Assets
|
||||||
|
1. **Icon**: Must be recognizable at small sizes (60x60px)
|
||||||
|
2. **Screenshots**: First 2-3 are critical—most users don't scroll
|
||||||
|
3. **Captions**: Use screenshot captions to tell your value story
|
||||||
|
4. **Consistency**: Match visual style to app design
|
||||||
|
5. **A/B Test Icons**: Icon is the single most important visual element
|
||||||
|
|
||||||
|
### Reviews & Ratings
|
||||||
|
1. **Respond Quickly**: Reply to reviews within 24-48 hours
|
||||||
|
2. **Professional Tone**: Always courteous, even with negative reviews
|
||||||
|
3. **Address Issues**: Show you're actively fixing reported problems
|
||||||
|
4. **Thank Supporters**: Acknowledge positive reviews
|
||||||
|
5. **Prompt Strategically**: Ask for ratings after positive experiences
|
||||||
|
|
||||||
|
### Launch Strategy
|
||||||
|
1. **Soft Launch**: Consider launching in smaller markets first
|
||||||
|
2. **PR Timing**: Coordinate press coverage with launch
|
||||||
|
3. **Update Frequently**: Initial updates signal active development
|
||||||
|
4. **Monitor Closely**: Track metrics daily for first 2 weeks
|
||||||
|
5. **Iterate Quickly**: Fix critical issues immediately
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
1. **Prioritize Markets**: Start with English, Spanish, Chinese, French, German
|
||||||
|
2. **Native Speakers**: Use professional translators, not machine translation
|
||||||
|
3. **Cultural Adaptation**: Some features resonate differently by culture
|
||||||
|
4. **Test Locally**: Have native speakers review before publishing
|
||||||
|
5. **Measure ROI**: Track downloads by locale to assess impact
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
### Data Dependencies
|
||||||
|
- Keyword search volume estimates are approximate (no official data from Apple/Google)
|
||||||
|
- Competitor data may be incomplete for private apps
|
||||||
|
- Review analysis limited to public reviews (can't access private feedback)
|
||||||
|
- Historical data may not be available for new apps
|
||||||
|
|
||||||
|
### Platform Constraints
|
||||||
|
- Apple App Store keyword changes require app submission (except Promotional Text)
|
||||||
|
- Google Play Store metadata changes take 1-2 hours to index
|
||||||
|
- A/B testing requires significant traffic for statistical significance
|
||||||
|
- Store algorithms are proprietary and change without notice
|
||||||
|
|
||||||
|
### Industry Variability
|
||||||
|
- ASO benchmarks vary significantly by category (games vs. utilities)
|
||||||
|
- Seasonality affects different categories differently
|
||||||
|
- Geographic markets have different competitive landscapes
|
||||||
|
- Cultural preferences impact what works in different countries
|
||||||
|
|
||||||
|
### Scope Boundaries
|
||||||
|
- Does not include paid user acquisition strategies (Apple Search Ads, Google Ads)
|
||||||
|
- Does not cover app development or UI/UX optimization
|
||||||
|
- Does not include app analytics implementation (use Firebase, Mixpanel, etc.)
|
||||||
|
- Does not handle app submission technical issues (provisioning profiles, certificates)
|
||||||
|
|
||||||
|
### When NOT to Use This Skill
|
||||||
|
- For web apps (different SEO strategies apply)
|
||||||
|
- For enterprise apps not in public stores
|
||||||
|
- For apps in beta/TestFlight only
|
||||||
|
- If you need paid advertising strategies (use marketing skills instead)
|
||||||
|
|
||||||
|
## Integration with Other Skills
|
||||||
|
|
||||||
|
This skill works well with:
|
||||||
|
- **Content Strategy Skills**: For creating app descriptions and marketing copy
|
||||||
|
- **Analytics Skills**: For analyzing download and engagement data
|
||||||
|
- **Localization Skills**: For managing multi-language content
|
||||||
|
- **Design Skills**: For creating optimized visual assets
|
||||||
|
- **Marketing Skills**: For coordinating broader launch campaigns
|
||||||
|
|
||||||
|
## Version & Updates
|
||||||
|
|
||||||
|
This skill is based on current Apple App Store and Google Play Store requirements as of November 2025. Store policies and best practices evolve—verify current requirements before major launches.
|
||||||
|
|
||||||
|
**Key Updates to Monitor:**
|
||||||
|
- Apple App Store Connect updates (apple.com/app-store/review/guidelines)
|
||||||
|
- Google Play Console updates (play.google.com/console/about/guides/releasewithconfidence)
|
||||||
|
- iOS/Android version adoption rates (affects device testing)
|
||||||
|
- Store algorithm changes (follow ASO blogs and communities)
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
This skill is applicable to execute the workflow or actions described in the overview.
|
||||||
@@ -0,0 +1,662 @@
|
|||||||
|
"""
|
||||||
|
A/B testing module for App Store Optimization.
|
||||||
|
Plans and tracks A/B tests for metadata and visual assets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
class ABTestPlanner:
|
||||||
|
"""Plans and tracks A/B tests for ASO elements."""
|
||||||
|
|
||||||
|
# Minimum detectable effect sizes (conservative estimates)
|
||||||
|
MIN_EFFECT_SIZES = {
|
||||||
|
'icon': 0.10, # 10% conversion improvement
|
||||||
|
'screenshot': 0.08, # 8% conversion improvement
|
||||||
|
'title': 0.05, # 5% conversion improvement
|
||||||
|
'description': 0.03 # 3% conversion improvement
|
||||||
|
}
|
||||||
|
|
||||||
|
# Statistical confidence levels
|
||||||
|
CONFIDENCE_LEVELS = {
|
||||||
|
'high': 0.95, # 95% confidence
|
||||||
|
'standard': 0.90, # 90% confidence
|
||||||
|
'exploratory': 0.80 # 80% confidence
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize A/B test planner."""
|
||||||
|
self.active_tests = []
|
||||||
|
|
||||||
|
def design_test(
|
||||||
|
self,
|
||||||
|
test_type: str,
|
||||||
|
variant_a: Dict[str, Any],
|
||||||
|
variant_b: Dict[str, Any],
|
||||||
|
hypothesis: str,
|
||||||
|
success_metric: str = 'conversion_rate'
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Design an A/B test with hypothesis and variables.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
test_type: Type of test ('icon', 'screenshot', 'title', 'description')
|
||||||
|
variant_a: Control variant details
|
||||||
|
variant_b: Test variant details
|
||||||
|
hypothesis: Expected outcome hypothesis
|
||||||
|
success_metric: Metric to optimize
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Test design with configuration
|
||||||
|
"""
|
||||||
|
test_design = {
|
||||||
|
'test_id': self._generate_test_id(test_type),
|
||||||
|
'test_type': test_type,
|
||||||
|
'hypothesis': hypothesis,
|
||||||
|
'variants': {
|
||||||
|
'a': {
|
||||||
|
'name': 'Control',
|
||||||
|
'details': variant_a,
|
||||||
|
'traffic_split': 0.5
|
||||||
|
},
|
||||||
|
'b': {
|
||||||
|
'name': 'Variation',
|
||||||
|
'details': variant_b,
|
||||||
|
'traffic_split': 0.5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'success_metric': success_metric,
|
||||||
|
'secondary_metrics': self._get_secondary_metrics(test_type),
|
||||||
|
'minimum_effect_size': self.MIN_EFFECT_SIZES.get(test_type, 0.05),
|
||||||
|
'recommended_confidence': 'standard',
|
||||||
|
'best_practices': self._get_test_best_practices(test_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.active_tests.append(test_design)
|
||||||
|
return test_design
|
||||||
|
|
||||||
|
def calculate_sample_size(
|
||||||
|
self,
|
||||||
|
baseline_conversion: float,
|
||||||
|
minimum_detectable_effect: float,
|
||||||
|
confidence_level: str = 'standard',
|
||||||
|
power: float = 0.80
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Calculate required sample size for statistical significance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
baseline_conversion: Current conversion rate (0-1)
|
||||||
|
minimum_detectable_effect: Minimum effect size to detect (0-1)
|
||||||
|
confidence_level: 'high', 'standard', or 'exploratory'
|
||||||
|
power: Statistical power (typically 0.80 or 0.90)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Sample size calculation with duration estimates
|
||||||
|
"""
|
||||||
|
alpha = 1 - self.CONFIDENCE_LEVELS[confidence_level]
|
||||||
|
beta = 1 - power
|
||||||
|
|
||||||
|
# Expected conversion for variant B
|
||||||
|
expected_conversion_b = baseline_conversion * (1 + minimum_detectable_effect)
|
||||||
|
|
||||||
|
# Z-scores for alpha and beta
|
||||||
|
z_alpha = self._get_z_score(1 - alpha / 2) # Two-tailed test
|
||||||
|
z_beta = self._get_z_score(power)
|
||||||
|
|
||||||
|
# Pooled standard deviation
|
||||||
|
p_pooled = (baseline_conversion + expected_conversion_b) / 2
|
||||||
|
sd_pooled = math.sqrt(2 * p_pooled * (1 - p_pooled))
|
||||||
|
|
||||||
|
# Sample size per variant
|
||||||
|
n_per_variant = math.ceil(
|
||||||
|
((z_alpha + z_beta) ** 2 * sd_pooled ** 2) /
|
||||||
|
((expected_conversion_b - baseline_conversion) ** 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
total_sample_size = n_per_variant * 2
|
||||||
|
|
||||||
|
# Estimate duration based on typical traffic
|
||||||
|
duration_estimates = self._estimate_test_duration(
|
||||||
|
total_sample_size,
|
||||||
|
baseline_conversion
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'sample_size_per_variant': n_per_variant,
|
||||||
|
'total_sample_size': total_sample_size,
|
||||||
|
'baseline_conversion': baseline_conversion,
|
||||||
|
'expected_conversion_improvement': minimum_detectable_effect,
|
||||||
|
'expected_conversion_b': expected_conversion_b,
|
||||||
|
'confidence_level': confidence_level,
|
||||||
|
'statistical_power': power,
|
||||||
|
'duration_estimates': duration_estimates,
|
||||||
|
'recommendations': self._generate_sample_size_recommendations(
|
||||||
|
n_per_variant,
|
||||||
|
duration_estimates
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def calculate_significance(
|
||||||
|
self,
|
||||||
|
variant_a_conversions: int,
|
||||||
|
variant_a_visitors: int,
|
||||||
|
variant_b_conversions: int,
|
||||||
|
variant_b_visitors: int
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Calculate statistical significance of test results.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
variant_a_conversions: Conversions for control
|
||||||
|
variant_a_visitors: Visitors for control
|
||||||
|
variant_b_conversions: Conversions for variation
|
||||||
|
variant_b_visitors: Visitors for variation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Significance analysis with decision recommendation
|
||||||
|
"""
|
||||||
|
# Calculate conversion rates
|
||||||
|
rate_a = variant_a_conversions / variant_a_visitors if variant_a_visitors > 0 else 0
|
||||||
|
rate_b = variant_b_conversions / variant_b_visitors if variant_b_visitors > 0 else 0
|
||||||
|
|
||||||
|
# Calculate improvement
|
||||||
|
if rate_a > 0:
|
||||||
|
relative_improvement = (rate_b - rate_a) / rate_a
|
||||||
|
else:
|
||||||
|
relative_improvement = 0
|
||||||
|
|
||||||
|
absolute_improvement = rate_b - rate_a
|
||||||
|
|
||||||
|
# Calculate standard error
|
||||||
|
se_a = math.sqrt(rate_a * (1 - rate_a) / variant_a_visitors) if variant_a_visitors > 0 else 0
|
||||||
|
se_b = math.sqrt(rate_b * (1 - rate_b) / variant_b_visitors) if variant_b_visitors > 0 else 0
|
||||||
|
se_diff = math.sqrt(se_a**2 + se_b**2)
|
||||||
|
|
||||||
|
# Calculate z-score
|
||||||
|
z_score = absolute_improvement / se_diff if se_diff > 0 else 0
|
||||||
|
|
||||||
|
# Calculate p-value (two-tailed)
|
||||||
|
p_value = 2 * (1 - self._standard_normal_cdf(abs(z_score)))
|
||||||
|
|
||||||
|
# Determine significance
|
||||||
|
is_significant_95 = p_value < 0.05
|
||||||
|
is_significant_90 = p_value < 0.10
|
||||||
|
|
||||||
|
# Generate decision
|
||||||
|
decision = self._generate_test_decision(
|
||||||
|
relative_improvement,
|
||||||
|
is_significant_95,
|
||||||
|
is_significant_90,
|
||||||
|
variant_a_visitors + variant_b_visitors
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'variant_a': {
|
||||||
|
'conversions': variant_a_conversions,
|
||||||
|
'visitors': variant_a_visitors,
|
||||||
|
'conversion_rate': round(rate_a, 4)
|
||||||
|
},
|
||||||
|
'variant_b': {
|
||||||
|
'conversions': variant_b_conversions,
|
||||||
|
'visitors': variant_b_visitors,
|
||||||
|
'conversion_rate': round(rate_b, 4)
|
||||||
|
},
|
||||||
|
'improvement': {
|
||||||
|
'absolute': round(absolute_improvement, 4),
|
||||||
|
'relative_percentage': round(relative_improvement * 100, 2)
|
||||||
|
},
|
||||||
|
'statistical_analysis': {
|
||||||
|
'z_score': round(z_score, 3),
|
||||||
|
'p_value': round(p_value, 4),
|
||||||
|
'is_significant_95': is_significant_95,
|
||||||
|
'is_significant_90': is_significant_90,
|
||||||
|
'confidence_level': '95%' if is_significant_95 else ('90%' if is_significant_90 else 'Not significant')
|
||||||
|
},
|
||||||
|
'decision': decision
|
||||||
|
}
|
||||||
|
|
||||||
|
def track_test_results(
|
||||||
|
self,
|
||||||
|
test_id: str,
|
||||||
|
results_data: Dict[str, Any]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Track ongoing test results and provide recommendations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
test_id: Test identifier
|
||||||
|
results_data: Current test results
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Test tracking report with next steps
|
||||||
|
"""
|
||||||
|
# Find test
|
||||||
|
test = next((t for t in self.active_tests if t['test_id'] == test_id), None)
|
||||||
|
if not test:
|
||||||
|
return {'error': f'Test {test_id} not found'}
|
||||||
|
|
||||||
|
# Calculate significance
|
||||||
|
significance = self.calculate_significance(
|
||||||
|
results_data['variant_a_conversions'],
|
||||||
|
results_data['variant_a_visitors'],
|
||||||
|
results_data['variant_b_conversions'],
|
||||||
|
results_data['variant_b_visitors']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate test progress
|
||||||
|
total_visitors = results_data['variant_a_visitors'] + results_data['variant_b_visitors']
|
||||||
|
required_sample = results_data.get('required_sample_size', 10000)
|
||||||
|
progress_percentage = min((total_visitors / required_sample) * 100, 100)
|
||||||
|
|
||||||
|
# Generate recommendations
|
||||||
|
recommendations = self._generate_tracking_recommendations(
|
||||||
|
significance,
|
||||||
|
progress_percentage,
|
||||||
|
test['test_type']
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'test_id': test_id,
|
||||||
|
'test_type': test['test_type'],
|
||||||
|
'progress': {
|
||||||
|
'total_visitors': total_visitors,
|
||||||
|
'required_sample_size': required_sample,
|
||||||
|
'progress_percentage': round(progress_percentage, 1),
|
||||||
|
'is_complete': progress_percentage >= 100
|
||||||
|
},
|
||||||
|
'current_results': significance,
|
||||||
|
'recommendations': recommendations,
|
||||||
|
'next_steps': self._determine_next_steps(
|
||||||
|
significance,
|
||||||
|
progress_percentage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def generate_test_report(
|
||||||
|
self,
|
||||||
|
test_id: str,
|
||||||
|
final_results: Dict[str, Any]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Generate final test report with insights and recommendations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
test_id: Test identifier
|
||||||
|
final_results: Final test results
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Comprehensive test report
|
||||||
|
"""
|
||||||
|
test = next((t for t in self.active_tests if t['test_id'] == test_id), None)
|
||||||
|
if not test:
|
||||||
|
return {'error': f'Test {test_id} not found'}
|
||||||
|
|
||||||
|
significance = self.calculate_significance(
|
||||||
|
final_results['variant_a_conversions'],
|
||||||
|
final_results['variant_a_visitors'],
|
||||||
|
final_results['variant_b_conversions'],
|
||||||
|
final_results['variant_b_visitors']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate insights
|
||||||
|
insights = self._generate_test_insights(
|
||||||
|
test,
|
||||||
|
significance,
|
||||||
|
final_results
|
||||||
|
)
|
||||||
|
|
||||||
|
# Implementation plan
|
||||||
|
implementation_plan = self._create_implementation_plan(
|
||||||
|
test,
|
||||||
|
significance
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'test_summary': {
|
||||||
|
'test_id': test_id,
|
||||||
|
'test_type': test['test_type'],
|
||||||
|
'hypothesis': test['hypothesis'],
|
||||||
|
'duration_days': final_results.get('duration_days', 'N/A')
|
||||||
|
},
|
||||||
|
'results': significance,
|
||||||
|
'insights': insights,
|
||||||
|
'implementation_plan': implementation_plan,
|
||||||
|
'learnings': self._extract_learnings(test, significance)
|
||||||
|
}
|
||||||
|
|
||||||
|
def _generate_test_id(self, test_type: str) -> str:
|
||||||
|
"""Generate unique test ID."""
|
||||||
|
import time
|
||||||
|
timestamp = int(time.time())
|
||||||
|
return f"{test_type}_{timestamp}"
|
||||||
|
|
||||||
|
def _get_secondary_metrics(self, test_type: str) -> List[str]:
|
||||||
|
"""Get secondary metrics to track for test type."""
|
||||||
|
metrics_map = {
|
||||||
|
'icon': ['tap_through_rate', 'impression_count', 'brand_recall'],
|
||||||
|
'screenshot': ['tap_through_rate', 'time_on_page', 'scroll_depth'],
|
||||||
|
'title': ['impression_count', 'tap_through_rate', 'search_visibility'],
|
||||||
|
'description': ['time_on_page', 'scroll_depth', 'tap_through_rate']
|
||||||
|
}
|
||||||
|
return metrics_map.get(test_type, ['tap_through_rate'])
|
||||||
|
|
||||||
|
def _get_test_best_practices(self, test_type: str) -> List[str]:
|
||||||
|
"""Get best practices for specific test type."""
|
||||||
|
practices_map = {
|
||||||
|
'icon': [
|
||||||
|
'Test only one element at a time (color vs. style vs. symbolism)',
|
||||||
|
'Ensure icon is recognizable at small sizes (60x60px)',
|
||||||
|
'Consider cultural context for global audience',
|
||||||
|
'Test against top competitor icons'
|
||||||
|
],
|
||||||
|
'screenshot': [
|
||||||
|
'Test order of screenshots (users see first 2-3)',
|
||||||
|
'Use captions to tell story',
|
||||||
|
'Show key features and benefits',
|
||||||
|
'Test with and without device frames'
|
||||||
|
],
|
||||||
|
'title': [
|
||||||
|
'Test keyword variations, not major rebrand',
|
||||||
|
'Keep brand name consistent',
|
||||||
|
'Ensure title fits within character limits',
|
||||||
|
'Test on both search and browse contexts'
|
||||||
|
],
|
||||||
|
'description': [
|
||||||
|
'Test structure (bullet points vs. paragraphs)',
|
||||||
|
'Test call-to-action placement',
|
||||||
|
'Test feature vs. benefit focus',
|
||||||
|
'Maintain keyword density'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return practices_map.get(test_type, ['Test one variable at a time'])
|
||||||
|
|
||||||
|
def _estimate_test_duration(
|
||||||
|
self,
|
||||||
|
required_sample_size: int,
|
||||||
|
baseline_conversion: float
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Estimate test duration based on typical traffic levels."""
|
||||||
|
# Assume different daily traffic scenarios
|
||||||
|
traffic_scenarios = {
|
||||||
|
'low': 100, # 100 page views/day
|
||||||
|
'medium': 1000, # 1000 page views/day
|
||||||
|
'high': 10000 # 10000 page views/day
|
||||||
|
}
|
||||||
|
|
||||||
|
estimates = {}
|
||||||
|
for scenario, daily_views in traffic_scenarios.items():
|
||||||
|
days = math.ceil(required_sample_size / daily_views)
|
||||||
|
estimates[scenario] = {
|
||||||
|
'daily_page_views': daily_views,
|
||||||
|
'estimated_days': days,
|
||||||
|
'estimated_weeks': round(days / 7, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return estimates
|
||||||
|
|
||||||
|
def _generate_sample_size_recommendations(
|
||||||
|
self,
|
||||||
|
sample_size: int,
|
||||||
|
duration_estimates: Dict[str, Any]
|
||||||
|
) -> List[str]:
|
||||||
|
"""Generate recommendations based on sample size."""
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
if sample_size > 50000:
|
||||||
|
recommendations.append(
|
||||||
|
"Large sample size required - consider testing smaller effect size or increasing traffic"
|
||||||
|
)
|
||||||
|
|
||||||
|
if duration_estimates['medium']['estimated_days'] > 30:
|
||||||
|
recommendations.append(
|
||||||
|
"Long test duration - consider higher minimum detectable effect or focus on high-impact changes"
|
||||||
|
)
|
||||||
|
|
||||||
|
if duration_estimates['low']['estimated_days'] > 60:
|
||||||
|
recommendations.append(
|
||||||
|
"Insufficient traffic for reliable testing - consider user acquisition or broader targeting"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not recommendations:
|
||||||
|
recommendations.append("Sample size and duration are reasonable for this test")
|
||||||
|
|
||||||
|
return recommendations
|
||||||
|
|
||||||
|
def _get_z_score(self, percentile: float) -> float:
|
||||||
|
"""Get z-score for given percentile (approximation)."""
|
||||||
|
# Common z-scores
|
||||||
|
z_scores = {
|
||||||
|
0.80: 0.84,
|
||||||
|
0.85: 1.04,
|
||||||
|
0.90: 1.28,
|
||||||
|
0.95: 1.645,
|
||||||
|
0.975: 1.96,
|
||||||
|
0.99: 2.33
|
||||||
|
}
|
||||||
|
return z_scores.get(percentile, 1.96)
|
||||||
|
|
||||||
|
def _standard_normal_cdf(self, z: float) -> float:
|
||||||
|
"""Approximate standard normal cumulative distribution function."""
|
||||||
|
# Using error function approximation
|
||||||
|
t = 1.0 / (1.0 + 0.2316419 * abs(z))
|
||||||
|
d = 0.3989423 * math.exp(-z * z / 2.0)
|
||||||
|
p = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))))
|
||||||
|
|
||||||
|
if z > 0:
|
||||||
|
return 1.0 - p
|
||||||
|
else:
|
||||||
|
return p
|
||||||
|
|
||||||
|
def _generate_test_decision(
|
||||||
|
self,
|
||||||
|
improvement: float,
|
||||||
|
is_significant_95: bool,
|
||||||
|
is_significant_90: bool,
|
||||||
|
total_visitors: int
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Generate test decision and recommendation."""
|
||||||
|
if total_visitors < 1000:
|
||||||
|
return {
|
||||||
|
'decision': 'continue',
|
||||||
|
'rationale': 'Insufficient data - continue test to reach minimum sample size',
|
||||||
|
'action': 'Keep test running'
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_significant_95:
|
||||||
|
if improvement > 0:
|
||||||
|
return {
|
||||||
|
'decision': 'implement_b',
|
||||||
|
'rationale': f'Variant B shows {improvement*100:.1f}% improvement with 95% confidence',
|
||||||
|
'action': 'Implement Variant B'
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'decision': 'keep_a',
|
||||||
|
'rationale': 'Variant A performs better with 95% confidence',
|
||||||
|
'action': 'Keep current version (A)'
|
||||||
|
}
|
||||||
|
|
||||||
|
elif is_significant_90:
|
||||||
|
if improvement > 0:
|
||||||
|
return {
|
||||||
|
'decision': 'implement_b_cautiously',
|
||||||
|
'rationale': f'Variant B shows {improvement*100:.1f}% improvement with 90% confidence',
|
||||||
|
'action': 'Consider implementing B, monitor closely'
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'decision': 'keep_a',
|
||||||
|
'rationale': 'Variant A performs better with 90% confidence',
|
||||||
|
'action': 'Keep current version (A)'
|
||||||
|
}
|
||||||
|
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'decision': 'inconclusive',
|
||||||
|
'rationale': 'No statistically significant difference detected',
|
||||||
|
'action': 'Either keep A or test different hypothesis'
|
||||||
|
}
|
||||||
|
|
||||||
|
def _generate_tracking_recommendations(
|
||||||
|
self,
|
||||||
|
significance: Dict[str, Any],
|
||||||
|
progress: float,
|
||||||
|
test_type: str
|
||||||
|
) -> List[str]:
|
||||||
|
"""Generate recommendations for ongoing test."""
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
if progress < 50:
|
||||||
|
recommendations.append(
|
||||||
|
f"Test is {progress:.0f}% complete - continue collecting data"
|
||||||
|
)
|
||||||
|
|
||||||
|
if progress >= 100:
|
||||||
|
if significance['statistical_analysis']['is_significant_95']:
|
||||||
|
recommendations.append(
|
||||||
|
"Sufficient data collected with significant results - ready to conclude test"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
recommendations.append(
|
||||||
|
"Sample size reached but no significant difference - consider extending test or concluding"
|
||||||
|
)
|
||||||
|
|
||||||
|
return recommendations
|
||||||
|
|
||||||
|
def _determine_next_steps(
|
||||||
|
self,
|
||||||
|
significance: Dict[str, Any],
|
||||||
|
progress: float
|
||||||
|
) -> str:
|
||||||
|
"""Determine next steps for test."""
|
||||||
|
if progress < 100:
|
||||||
|
return f"Continue test until reaching 100% sample size (currently {progress:.0f}%)"
|
||||||
|
|
||||||
|
decision = significance.get('decision', {}).get('decision', 'inconclusive')
|
||||||
|
|
||||||
|
if decision == 'implement_b':
|
||||||
|
return "Implement Variant B and monitor metrics for 2 weeks"
|
||||||
|
elif decision == 'keep_a':
|
||||||
|
return "Keep Variant A and design new test with different hypothesis"
|
||||||
|
else:
|
||||||
|
return "Test inconclusive - either keep A or design new test"
|
||||||
|
|
||||||
|
def _generate_test_insights(
|
||||||
|
self,
|
||||||
|
test: Dict[str, Any],
|
||||||
|
significance: Dict[str, Any],
|
||||||
|
results: Dict[str, Any]
|
||||||
|
) -> List[str]:
|
||||||
|
"""Generate insights from test results."""
|
||||||
|
insights = []
|
||||||
|
|
||||||
|
improvement = significance['improvement']['relative_percentage']
|
||||||
|
|
||||||
|
if significance['statistical_analysis']['is_significant_95']:
|
||||||
|
insights.append(
|
||||||
|
f"Strong evidence: Variant B {'improved' if improvement > 0 else 'decreased'} "
|
||||||
|
f"conversion by {abs(improvement):.1f}% with 95% confidence"
|
||||||
|
)
|
||||||
|
|
||||||
|
insights.append(
|
||||||
|
f"Tested {test['test_type']} changes: {test['hypothesis']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add context-specific insights
|
||||||
|
if test['test_type'] == 'icon' and improvement > 5:
|
||||||
|
insights.append(
|
||||||
|
"Icon change had substantial impact - visual first impression is critical"
|
||||||
|
)
|
||||||
|
|
||||||
|
return insights
|
||||||
|
|
||||||
|
def _create_implementation_plan(
|
||||||
|
self,
|
||||||
|
test: Dict[str, Any],
|
||||||
|
significance: Dict[str, Any]
|
||||||
|
) -> List[Dict[str, str]]:
|
||||||
|
"""Create implementation plan for winning variant."""
|
||||||
|
plan = []
|
||||||
|
|
||||||
|
if significance.get('decision', {}).get('decision') == 'implement_b':
|
||||||
|
plan.append({
|
||||||
|
'step': '1. Update store listing',
|
||||||
|
'details': f"Replace {test['test_type']} with Variant B across all platforms"
|
||||||
|
})
|
||||||
|
plan.append({
|
||||||
|
'step': '2. Monitor metrics',
|
||||||
|
'details': 'Track conversion rate for 2 weeks to confirm sustained improvement'
|
||||||
|
})
|
||||||
|
plan.append({
|
||||||
|
'step': '3. Document learnings',
|
||||||
|
'details': 'Record insights for future optimization'
|
||||||
|
})
|
||||||
|
|
||||||
|
return plan
|
||||||
|
|
||||||
|
def _extract_learnings(
|
||||||
|
self,
|
||||||
|
test: Dict[str, Any],
|
||||||
|
significance: Dict[str, Any]
|
||||||
|
) -> List[str]:
|
||||||
|
"""Extract key learnings from test."""
|
||||||
|
learnings = []
|
||||||
|
|
||||||
|
improvement = significance['improvement']['relative_percentage']
|
||||||
|
|
||||||
|
learnings.append(
|
||||||
|
f"Testing {test['test_type']} can yield {abs(improvement):.1f}% conversion change"
|
||||||
|
)
|
||||||
|
|
||||||
|
if test['test_type'] == 'title':
|
||||||
|
learnings.append(
|
||||||
|
"Title changes affect search visibility and user perception"
|
||||||
|
)
|
||||||
|
elif test['test_type'] == 'screenshot':
|
||||||
|
learnings.append(
|
||||||
|
"First 2-3 screenshots are critical for conversion"
|
||||||
|
)
|
||||||
|
|
||||||
|
return learnings
|
||||||
|
|
||||||
|
|
||||||
|
def plan_ab_test(
|
||||||
|
test_type: str,
|
||||||
|
variant_a: Dict[str, Any],
|
||||||
|
variant_b: Dict[str, Any],
|
||||||
|
hypothesis: str,
|
||||||
|
baseline_conversion: float
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Convenience function to plan an A/B test.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
test_type: Type of test
|
||||||
|
variant_a: Control variant
|
||||||
|
variant_b: Test variant
|
||||||
|
hypothesis: Test hypothesis
|
||||||
|
baseline_conversion: Current conversion rate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complete test plan
|
||||||
|
"""
|
||||||
|
planner = ABTestPlanner()
|
||||||
|
|
||||||
|
test_design = planner.design_test(
|
||||||
|
test_type,
|
||||||
|
variant_a,
|
||||||
|
variant_b,
|
||||||
|
hypothesis
|
||||||
|
)
|
||||||
|
|
||||||
|
sample_size = planner.calculate_sample_size(
|
||||||
|
baseline_conversion,
|
||||||
|
planner.MIN_EFFECT_SIZES.get(test_type, 0.05)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'test_design': test_design,
|
||||||
|
'sample_size_requirements': sample_size
|
||||||
|
}
|
||||||
@@ -0,0 +1,482 @@
|
|||||||
|
"""
|
||||||
|
ASO scoring module for App Store Optimization.
|
||||||
|
Calculates comprehensive ASO health score across multiple dimensions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class ASOScorer:
|
||||||
|
"""Calculates overall ASO health score and provides recommendations."""
|
||||||
|
|
||||||
|
# Score weights for different components (total = 100)
|
||||||
|
WEIGHTS = {
|
||||||
|
'metadata_quality': 25,
|
||||||
|
'ratings_reviews': 25,
|
||||||
|
'keyword_performance': 25,
|
||||||
|
'conversion_metrics': 25
|
||||||
|
}
|
||||||
|
|
||||||
|
# Benchmarks for scoring
|
||||||
|
BENCHMARKS = {
|
||||||
|
'title_keyword_usage': {'min': 1, 'target': 2},
|
||||||
|
'description_length': {'min': 500, 'target': 2000},
|
||||||
|
'keyword_density': {'min': 2, 'optimal': 5, 'max': 8},
|
||||||
|
'average_rating': {'min': 3.5, 'target': 4.5},
|
||||||
|
'ratings_count': {'min': 100, 'target': 5000},
|
||||||
|
'keywords_top_10': {'min': 2, 'target': 10},
|
||||||
|
'keywords_top_50': {'min': 5, 'target': 20},
|
||||||
|
'conversion_rate': {'min': 0.02, 'target': 0.10}
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize ASO scorer."""
|
||||||
|
self.score_breakdown = {}
|
||||||
|
|
||||||
|
def calculate_overall_score(
|
||||||
|
self,
|
||||||
|
metadata: Dict[str, Any],
|
||||||
|
ratings: Dict[str, Any],
|
||||||
|
keyword_performance: Dict[str, Any],
|
||||||
|
conversion: Dict[str, Any]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Calculate comprehensive ASO score (0-100).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
metadata: Title, description quality metrics
|
||||||
|
ratings: Rating average and count
|
||||||
|
keyword_performance: Keyword ranking data
|
||||||
|
conversion: Impression-to-install metrics
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Overall score with detailed breakdown
|
||||||
|
"""
|
||||||
|
# Calculate component scores
|
||||||
|
metadata_score = self.score_metadata_quality(metadata)
|
||||||
|
ratings_score = self.score_ratings_reviews(ratings)
|
||||||
|
keyword_score = self.score_keyword_performance(keyword_performance)
|
||||||
|
conversion_score = self.score_conversion_metrics(conversion)
|
||||||
|
|
||||||
|
# Calculate weighted overall score
|
||||||
|
overall_score = (
|
||||||
|
metadata_score * (self.WEIGHTS['metadata_quality'] / 100) +
|
||||||
|
ratings_score * (self.WEIGHTS['ratings_reviews'] / 100) +
|
||||||
|
keyword_score * (self.WEIGHTS['keyword_performance'] / 100) +
|
||||||
|
conversion_score * (self.WEIGHTS['conversion_metrics'] / 100)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store breakdown
|
||||||
|
self.score_breakdown = {
|
||||||
|
'metadata_quality': {
|
||||||
|
'score': metadata_score,
|
||||||
|
'weight': self.WEIGHTS['metadata_quality'],
|
||||||
|
'weighted_contribution': round(metadata_score * (self.WEIGHTS['metadata_quality'] / 100), 1)
|
||||||
|
},
|
||||||
|
'ratings_reviews': {
|
||||||
|
'score': ratings_score,
|
||||||
|
'weight': self.WEIGHTS['ratings_reviews'],
|
||||||
|
'weighted_contribution': round(ratings_score * (self.WEIGHTS['ratings_reviews'] / 100), 1)
|
||||||
|
},
|
||||||
|
'keyword_performance': {
|
||||||
|
'score': keyword_score,
|
||||||
|
'weight': self.WEIGHTS['keyword_performance'],
|
||||||
|
'weighted_contribution': round(keyword_score * (self.WEIGHTS['keyword_performance'] / 100), 1)
|
||||||
|
},
|
||||||
|
'conversion_metrics': {
|
||||||
|
'score': conversion_score,
|
||||||
|
'weight': self.WEIGHTS['conversion_metrics'],
|
||||||
|
'weighted_contribution': round(conversion_score * (self.WEIGHTS['conversion_metrics'] / 100), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate recommendations
|
||||||
|
recommendations = self.generate_recommendations(
|
||||||
|
metadata_score,
|
||||||
|
ratings_score,
|
||||||
|
keyword_score,
|
||||||
|
conversion_score
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assess overall health
|
||||||
|
health_status = self._assess_health_status(overall_score)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'overall_score': round(overall_score, 1),
|
||||||
|
'health_status': health_status,
|
||||||
|
'score_breakdown': self.score_breakdown,
|
||||||
|
'recommendations': recommendations,
|
||||||
|
'priority_actions': self._prioritize_actions(recommendations),
|
||||||
|
'strengths': self._identify_strengths(self.score_breakdown),
|
||||||
|
'weaknesses': self._identify_weaknesses(self.score_breakdown)
|
||||||
|
}
|
||||||
|
|
||||||
|
def score_metadata_quality(self, metadata: Dict[str, Any]) -> float:
|
||||||
|
"""
|
||||||
|
Score metadata quality (0-100).
|
||||||
|
|
||||||
|
Evaluates:
|
||||||
|
- Title optimization
|
||||||
|
- Description quality
|
||||||
|
- Keyword usage
|
||||||
|
"""
|
||||||
|
scores = []
|
||||||
|
|
||||||
|
# Title score (0-35 points)
|
||||||
|
title_keywords = metadata.get('title_keyword_count', 0)
|
||||||
|
title_length = metadata.get('title_length', 0)
|
||||||
|
|
||||||
|
title_score = 0
|
||||||
|
if title_keywords >= self.BENCHMARKS['title_keyword_usage']['target']:
|
||||||
|
title_score = 35
|
||||||
|
elif title_keywords >= self.BENCHMARKS['title_keyword_usage']['min']:
|
||||||
|
title_score = 25
|
||||||
|
else:
|
||||||
|
title_score = 10
|
||||||
|
|
||||||
|
# Adjust for title length usage
|
||||||
|
if title_length > 25: # Using most of available space
|
||||||
|
title_score += 0
|
||||||
|
else:
|
||||||
|
title_score -= 5
|
||||||
|
|
||||||
|
scores.append(min(title_score, 35))
|
||||||
|
|
||||||
|
# Description score (0-35 points)
|
||||||
|
desc_length = metadata.get('description_length', 0)
|
||||||
|
desc_quality = metadata.get('description_quality', 0.0) # 0-1 scale
|
||||||
|
|
||||||
|
desc_score = 0
|
||||||
|
if desc_length >= self.BENCHMARKS['description_length']['target']:
|
||||||
|
desc_score = 25
|
||||||
|
elif desc_length >= self.BENCHMARKS['description_length']['min']:
|
||||||
|
desc_score = 15
|
||||||
|
else:
|
||||||
|
desc_score = 5
|
||||||
|
|
||||||
|
# Add quality bonus
|
||||||
|
desc_score += desc_quality * 10
|
||||||
|
scores.append(min(desc_score, 35))
|
||||||
|
|
||||||
|
# Keyword density score (0-30 points)
|
||||||
|
keyword_density = metadata.get('keyword_density', 0.0)
|
||||||
|
|
||||||
|
if self.BENCHMARKS['keyword_density']['min'] <= keyword_density <= self.BENCHMARKS['keyword_density']['optimal']:
|
||||||
|
density_score = 30
|
||||||
|
elif keyword_density < self.BENCHMARKS['keyword_density']['min']:
|
||||||
|
# Too low - proportional scoring
|
||||||
|
density_score = (keyword_density / self.BENCHMARKS['keyword_density']['min']) * 20
|
||||||
|
else:
|
||||||
|
# Too high (keyword stuffing) - penalty
|
||||||
|
excess = keyword_density - self.BENCHMARKS['keyword_density']['optimal']
|
||||||
|
density_score = max(30 - (excess * 5), 0)
|
||||||
|
|
||||||
|
scores.append(density_score)
|
||||||
|
|
||||||
|
return round(sum(scores), 1)
|
||||||
|
|
||||||
|
def score_ratings_reviews(self, ratings: Dict[str, Any]) -> float:
|
||||||
|
"""
|
||||||
|
Score ratings and reviews (0-100).
|
||||||
|
|
||||||
|
Evaluates:
|
||||||
|
- Average rating
|
||||||
|
- Total ratings count
|
||||||
|
- Review velocity
|
||||||
|
"""
|
||||||
|
average_rating = ratings.get('average_rating', 0.0)
|
||||||
|
total_ratings = ratings.get('total_ratings', 0)
|
||||||
|
recent_ratings = ratings.get('recent_ratings_30d', 0)
|
||||||
|
|
||||||
|
# Rating quality score (0-50 points)
|
||||||
|
if average_rating >= self.BENCHMARKS['average_rating']['target']:
|
||||||
|
rating_quality_score = 50
|
||||||
|
elif average_rating >= self.BENCHMARKS['average_rating']['min']:
|
||||||
|
# Proportional scoring between min and target
|
||||||
|
proportion = (average_rating - self.BENCHMARKS['average_rating']['min']) / \
|
||||||
|
(self.BENCHMARKS['average_rating']['target'] - self.BENCHMARKS['average_rating']['min'])
|
||||||
|
rating_quality_score = 30 + (proportion * 20)
|
||||||
|
elif average_rating >= 3.0:
|
||||||
|
rating_quality_score = 20
|
||||||
|
else:
|
||||||
|
rating_quality_score = 10
|
||||||
|
|
||||||
|
# Rating volume score (0-30 points)
|
||||||
|
if total_ratings >= self.BENCHMARKS['ratings_count']['target']:
|
||||||
|
rating_volume_score = 30
|
||||||
|
elif total_ratings >= self.BENCHMARKS['ratings_count']['min']:
|
||||||
|
# Proportional scoring
|
||||||
|
proportion = (total_ratings - self.BENCHMARKS['ratings_count']['min']) / \
|
||||||
|
(self.BENCHMARKS['ratings_count']['target'] - self.BENCHMARKS['ratings_count']['min'])
|
||||||
|
rating_volume_score = 15 + (proportion * 15)
|
||||||
|
else:
|
||||||
|
# Very low volume
|
||||||
|
rating_volume_score = (total_ratings / self.BENCHMARKS['ratings_count']['min']) * 15
|
||||||
|
|
||||||
|
# Rating velocity score (0-20 points)
|
||||||
|
if recent_ratings > 100:
|
||||||
|
velocity_score = 20
|
||||||
|
elif recent_ratings > 50:
|
||||||
|
velocity_score = 15
|
||||||
|
elif recent_ratings > 10:
|
||||||
|
velocity_score = 10
|
||||||
|
else:
|
||||||
|
velocity_score = 5
|
||||||
|
|
||||||
|
total_score = rating_quality_score + rating_volume_score + velocity_score
|
||||||
|
|
||||||
|
return round(min(total_score, 100), 1)
|
||||||
|
|
||||||
|
def score_keyword_performance(self, keyword_performance: Dict[str, Any]) -> float:
|
||||||
|
"""
|
||||||
|
Score keyword ranking performance (0-100).
|
||||||
|
|
||||||
|
Evaluates:
|
||||||
|
- Top 10 rankings
|
||||||
|
- Top 50 rankings
|
||||||
|
- Ranking trends
|
||||||
|
"""
|
||||||
|
top_10_count = keyword_performance.get('top_10', 0)
|
||||||
|
top_50_count = keyword_performance.get('top_50', 0)
|
||||||
|
top_100_count = keyword_performance.get('top_100', 0)
|
||||||
|
improving_keywords = keyword_performance.get('improving_keywords', 0)
|
||||||
|
|
||||||
|
# Top 10 score (0-50 points) - most valuable rankings
|
||||||
|
if top_10_count >= self.BENCHMARKS['keywords_top_10']['target']:
|
||||||
|
top_10_score = 50
|
||||||
|
elif top_10_count >= self.BENCHMARKS['keywords_top_10']['min']:
|
||||||
|
proportion = (top_10_count - self.BENCHMARKS['keywords_top_10']['min']) / \
|
||||||
|
(self.BENCHMARKS['keywords_top_10']['target'] - self.BENCHMARKS['keywords_top_10']['min'])
|
||||||
|
top_10_score = 25 + (proportion * 25)
|
||||||
|
else:
|
||||||
|
top_10_score = (top_10_count / self.BENCHMARKS['keywords_top_10']['min']) * 25
|
||||||
|
|
||||||
|
# Top 50 score (0-30 points)
|
||||||
|
if top_50_count >= self.BENCHMARKS['keywords_top_50']['target']:
|
||||||
|
top_50_score = 30
|
||||||
|
elif top_50_count >= self.BENCHMARKS['keywords_top_50']['min']:
|
||||||
|
proportion = (top_50_count - self.BENCHMARKS['keywords_top_50']['min']) / \
|
||||||
|
(self.BENCHMARKS['keywords_top_50']['target'] - self.BENCHMARKS['keywords_top_50']['min'])
|
||||||
|
top_50_score = 15 + (proportion * 15)
|
||||||
|
else:
|
||||||
|
top_50_score = (top_50_count / self.BENCHMARKS['keywords_top_50']['min']) * 15
|
||||||
|
|
||||||
|
# Coverage score (0-10 points) - based on top 100
|
||||||
|
coverage_score = min((top_100_count / 30) * 10, 10)
|
||||||
|
|
||||||
|
# Trend score (0-10 points) - are rankings improving?
|
||||||
|
if improving_keywords > 5:
|
||||||
|
trend_score = 10
|
||||||
|
elif improving_keywords > 0:
|
||||||
|
trend_score = 5
|
||||||
|
else:
|
||||||
|
trend_score = 0
|
||||||
|
|
||||||
|
total_score = top_10_score + top_50_score + coverage_score + trend_score
|
||||||
|
|
||||||
|
return round(min(total_score, 100), 1)
|
||||||
|
|
||||||
|
def score_conversion_metrics(self, conversion: Dict[str, Any]) -> float:
|
||||||
|
"""
|
||||||
|
Score conversion performance (0-100).
|
||||||
|
|
||||||
|
Evaluates:
|
||||||
|
- Impression-to-install conversion rate
|
||||||
|
- Download velocity
|
||||||
|
"""
|
||||||
|
conversion_rate = conversion.get('impression_to_install', 0.0)
|
||||||
|
downloads_30d = conversion.get('downloads_last_30_days', 0)
|
||||||
|
downloads_trend = conversion.get('downloads_trend', 'stable') # 'up', 'stable', 'down'
|
||||||
|
|
||||||
|
# Conversion rate score (0-70 points)
|
||||||
|
if conversion_rate >= self.BENCHMARKS['conversion_rate']['target']:
|
||||||
|
conversion_score = 70
|
||||||
|
elif conversion_rate >= self.BENCHMARKS['conversion_rate']['min']:
|
||||||
|
proportion = (conversion_rate - self.BENCHMARKS['conversion_rate']['min']) / \
|
||||||
|
(self.BENCHMARKS['conversion_rate']['target'] - self.BENCHMARKS['conversion_rate']['min'])
|
||||||
|
conversion_score = 35 + (proportion * 35)
|
||||||
|
else:
|
||||||
|
conversion_score = (conversion_rate / self.BENCHMARKS['conversion_rate']['min']) * 35
|
||||||
|
|
||||||
|
# Download velocity score (0-20 points)
|
||||||
|
if downloads_30d > 10000:
|
||||||
|
velocity_score = 20
|
||||||
|
elif downloads_30d > 1000:
|
||||||
|
velocity_score = 15
|
||||||
|
elif downloads_30d > 100:
|
||||||
|
velocity_score = 10
|
||||||
|
else:
|
||||||
|
velocity_score = 5
|
||||||
|
|
||||||
|
# Trend bonus (0-10 points)
|
||||||
|
if downloads_trend == 'up':
|
||||||
|
trend_score = 10
|
||||||
|
elif downloads_trend == 'stable':
|
||||||
|
trend_score = 5
|
||||||
|
else:
|
||||||
|
trend_score = 0
|
||||||
|
|
||||||
|
total_score = conversion_score + velocity_score + trend_score
|
||||||
|
|
||||||
|
return round(min(total_score, 100), 1)
|
||||||
|
|
||||||
|
def generate_recommendations(
|
||||||
|
self,
|
||||||
|
metadata_score: float,
|
||||||
|
ratings_score: float,
|
||||||
|
keyword_score: float,
|
||||||
|
conversion_score: float
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Generate prioritized recommendations based on scores."""
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
# Metadata recommendations
|
||||||
|
if metadata_score < 60:
|
||||||
|
recommendations.append({
|
||||||
|
'category': 'metadata_quality',
|
||||||
|
'priority': 'high',
|
||||||
|
'action': 'Optimize app title and description',
|
||||||
|
'details': 'Add more keywords to title, expand description to 1500-2000 characters, improve keyword density to 3-5%',
|
||||||
|
'expected_impact': 'Improve discoverability and ranking potential'
|
||||||
|
})
|
||||||
|
elif metadata_score < 80:
|
||||||
|
recommendations.append({
|
||||||
|
'category': 'metadata_quality',
|
||||||
|
'priority': 'medium',
|
||||||
|
'action': 'Refine metadata for better keyword targeting',
|
||||||
|
'details': 'Test variations of title/subtitle, optimize keyword field for Apple',
|
||||||
|
'expected_impact': 'Incremental ranking improvements'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Ratings recommendations
|
||||||
|
if ratings_score < 60:
|
||||||
|
recommendations.append({
|
||||||
|
'category': 'ratings_reviews',
|
||||||
|
'priority': 'high',
|
||||||
|
'action': 'Improve rating quality and volume',
|
||||||
|
'details': 'Address top user complaints, implement in-app rating prompts, respond to negative reviews',
|
||||||
|
'expected_impact': 'Better conversion rates and trust signals'
|
||||||
|
})
|
||||||
|
elif ratings_score < 80:
|
||||||
|
recommendations.append({
|
||||||
|
'category': 'ratings_reviews',
|
||||||
|
'priority': 'medium',
|
||||||
|
'action': 'Increase rating velocity',
|
||||||
|
'details': 'Optimize timing of rating requests, encourage satisfied users to rate',
|
||||||
|
'expected_impact': 'Sustained rating quality'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Keyword performance recommendations
|
||||||
|
if keyword_score < 60:
|
||||||
|
recommendations.append({
|
||||||
|
'category': 'keyword_performance',
|
||||||
|
'priority': 'high',
|
||||||
|
'action': 'Improve keyword rankings',
|
||||||
|
'details': 'Target long-tail keywords with lower competition, update metadata with high-potential keywords, build backlinks',
|
||||||
|
'expected_impact': 'Significant improvement in organic visibility'
|
||||||
|
})
|
||||||
|
elif keyword_score < 80:
|
||||||
|
recommendations.append({
|
||||||
|
'category': 'keyword_performance',
|
||||||
|
'priority': 'medium',
|
||||||
|
'action': 'Expand keyword coverage',
|
||||||
|
'details': 'Target additional related keywords, test seasonal keywords, localize for new markets',
|
||||||
|
'expected_impact': 'Broader reach and more discovery opportunities'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Conversion recommendations
|
||||||
|
if conversion_score < 60:
|
||||||
|
recommendations.append({
|
||||||
|
'category': 'conversion_metrics',
|
||||||
|
'priority': 'high',
|
||||||
|
'action': 'Optimize store listing for conversions',
|
||||||
|
'details': 'Improve screenshots and icon, strengthen value proposition in description, add video preview',
|
||||||
|
'expected_impact': 'Higher impression-to-install conversion'
|
||||||
|
})
|
||||||
|
elif conversion_score < 80:
|
||||||
|
recommendations.append({
|
||||||
|
'category': 'conversion_metrics',
|
||||||
|
'priority': 'medium',
|
||||||
|
'action': 'Test visual asset variations',
|
||||||
|
'details': 'A/B test different icon designs and screenshot sequences',
|
||||||
|
'expected_impact': 'Incremental conversion improvements'
|
||||||
|
})
|
||||||
|
|
||||||
|
return recommendations
|
||||||
|
|
||||||
|
def _assess_health_status(self, overall_score: float) -> str:
|
||||||
|
"""Assess overall ASO health status."""
|
||||||
|
if overall_score >= 80:
|
||||||
|
return "Excellent - Top-tier ASO performance"
|
||||||
|
elif overall_score >= 65:
|
||||||
|
return "Good - Competitive ASO with room for improvement"
|
||||||
|
elif overall_score >= 50:
|
||||||
|
return "Fair - Needs strategic improvements"
|
||||||
|
else:
|
||||||
|
return "Poor - Requires immediate ASO overhaul"
|
||||||
|
|
||||||
|
def _prioritize_actions(
|
||||||
|
self,
|
||||||
|
recommendations: List[Dict[str, Any]]
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Prioritize actions by impact and urgency."""
|
||||||
|
# Sort by priority (high first) and expected impact
|
||||||
|
priority_order = {'high': 0, 'medium': 1, 'low': 2}
|
||||||
|
|
||||||
|
sorted_recommendations = sorted(
|
||||||
|
recommendations,
|
||||||
|
key=lambda x: priority_order[x['priority']]
|
||||||
|
)
|
||||||
|
|
||||||
|
return sorted_recommendations[:3] # Top 3 priority actions
|
||||||
|
|
||||||
|
def _identify_strengths(self, score_breakdown: Dict[str, Any]) -> List[str]:
|
||||||
|
"""Identify areas of strength (scores >= 75)."""
|
||||||
|
strengths = []
|
||||||
|
|
||||||
|
for category, data in score_breakdown.items():
|
||||||
|
if data['score'] >= 75:
|
||||||
|
strengths.append(
|
||||||
|
f"{category.replace('_', ' ').title()}: {data['score']}/100"
|
||||||
|
)
|
||||||
|
|
||||||
|
return strengths if strengths else ["Focus on building strengths across all areas"]
|
||||||
|
|
||||||
|
def _identify_weaknesses(self, score_breakdown: Dict[str, Any]) -> List[str]:
|
||||||
|
"""Identify areas needing improvement (scores < 60)."""
|
||||||
|
weaknesses = []
|
||||||
|
|
||||||
|
for category, data in score_breakdown.items():
|
||||||
|
if data['score'] < 60:
|
||||||
|
weaknesses.append(
|
||||||
|
f"{category.replace('_', ' ').title()}: {data['score']}/100 - needs improvement"
|
||||||
|
)
|
||||||
|
|
||||||
|
return weaknesses if weaknesses else ["All areas performing adequately"]
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_aso_score(
|
||||||
|
metadata: Dict[str, Any],
|
||||||
|
ratings: Dict[str, Any],
|
||||||
|
keyword_performance: Dict[str, Any],
|
||||||
|
conversion: Dict[str, Any]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Convenience function to calculate ASO score.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
metadata: Metadata quality metrics
|
||||||
|
ratings: Ratings data
|
||||||
|
keyword_performance: Keyword ranking data
|
||||||
|
conversion: Conversion metrics
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complete ASO score report
|
||||||
|
"""
|
||||||
|
scorer = ASOScorer()
|
||||||
|
return scorer.calculate_overall_score(
|
||||||
|
metadata,
|
||||||
|
ratings,
|
||||||
|
keyword_performance,
|
||||||
|
conversion
|
||||||
|
)
|
||||||
+577
@@ -0,0 +1,577 @@
|
|||||||
|
"""
|
||||||
|
Competitor analysis module for App Store Optimization.
|
||||||
|
Analyzes top competitors' ASO strategies and identifies opportunities.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
from collections import Counter
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class CompetitorAnalyzer:
|
||||||
|
"""Analyzes competitor apps to identify ASO opportunities."""
|
||||||
|
|
||||||
|
def __init__(self, category: str, platform: str = 'apple'):
|
||||||
|
"""
|
||||||
|
Initialize competitor analyzer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
category: App category (e.g., "Productivity", "Games")
|
||||||
|
platform: 'apple' or 'google'
|
||||||
|
"""
|
||||||
|
self.category = category
|
||||||
|
self.platform = platform
|
||||||
|
self.competitors = []
|
||||||
|
|
||||||
|
def analyze_competitor(
|
||||||
|
self,
|
||||||
|
app_data: Dict[str, Any]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Analyze a single competitor's ASO strategy.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_data: Dictionary with app_name, title, description, rating, ratings_count, keywords
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Comprehensive competitor analysis
|
||||||
|
"""
|
||||||
|
app_name = app_data.get('app_name', '')
|
||||||
|
title = app_data.get('title', '')
|
||||||
|
description = app_data.get('description', '')
|
||||||
|
rating = app_data.get('rating', 0.0)
|
||||||
|
ratings_count = app_data.get('ratings_count', 0)
|
||||||
|
keywords = app_data.get('keywords', [])
|
||||||
|
|
||||||
|
analysis = {
|
||||||
|
'app_name': app_name,
|
||||||
|
'title_analysis': self._analyze_title(title),
|
||||||
|
'description_analysis': self._analyze_description(description),
|
||||||
|
'keyword_strategy': self._extract_keyword_strategy(title, description, keywords),
|
||||||
|
'rating_metrics': {
|
||||||
|
'rating': rating,
|
||||||
|
'ratings_count': ratings_count,
|
||||||
|
'rating_quality': self._assess_rating_quality(rating, ratings_count)
|
||||||
|
},
|
||||||
|
'competitive_strength': self._calculate_competitive_strength(
|
||||||
|
rating,
|
||||||
|
ratings_count,
|
||||||
|
len(description)
|
||||||
|
),
|
||||||
|
'key_differentiators': self._identify_differentiators(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.competitors.append(analysis)
|
||||||
|
return analysis
|
||||||
|
|
||||||
|
def compare_competitors(
|
||||||
|
self,
|
||||||
|
competitors_data: List[Dict[str, Any]]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Compare multiple competitors and identify patterns.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
competitors_data: List of competitor data dictionaries
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Comparative analysis with insights
|
||||||
|
"""
|
||||||
|
# Analyze each competitor
|
||||||
|
analyses = []
|
||||||
|
for comp_data in competitors_data:
|
||||||
|
analysis = self.analyze_competitor(comp_data)
|
||||||
|
analyses.append(analysis)
|
||||||
|
|
||||||
|
# Extract common keywords across competitors
|
||||||
|
all_keywords = []
|
||||||
|
for analysis in analyses:
|
||||||
|
all_keywords.extend(analysis['keyword_strategy']['primary_keywords'])
|
||||||
|
|
||||||
|
common_keywords = self._find_common_keywords(all_keywords)
|
||||||
|
|
||||||
|
# Identify keyword gaps (used by some but not all)
|
||||||
|
keyword_gaps = self._identify_keyword_gaps(analyses)
|
||||||
|
|
||||||
|
# Rank competitors by strength
|
||||||
|
ranked_competitors = sorted(
|
||||||
|
analyses,
|
||||||
|
key=lambda x: x['competitive_strength'],
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Analyze rating distribution
|
||||||
|
rating_analysis = self._analyze_rating_distribution(analyses)
|
||||||
|
|
||||||
|
# Identify best practices
|
||||||
|
best_practices = self._identify_best_practices(ranked_competitors)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'category': self.category,
|
||||||
|
'platform': self.platform,
|
||||||
|
'competitors_analyzed': len(analyses),
|
||||||
|
'ranked_competitors': ranked_competitors,
|
||||||
|
'common_keywords': common_keywords,
|
||||||
|
'keyword_gaps': keyword_gaps,
|
||||||
|
'rating_analysis': rating_analysis,
|
||||||
|
'best_practices': best_practices,
|
||||||
|
'opportunities': self._identify_opportunities(
|
||||||
|
analyses,
|
||||||
|
common_keywords,
|
||||||
|
keyword_gaps
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def identify_gaps(
|
||||||
|
self,
|
||||||
|
your_app_data: Dict[str, Any],
|
||||||
|
competitors_data: List[Dict[str, Any]]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Identify gaps between your app and competitors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
your_app_data: Your app's data
|
||||||
|
competitors_data: List of competitor data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Gap analysis with actionable recommendations
|
||||||
|
"""
|
||||||
|
# Analyze your app
|
||||||
|
your_analysis = self.analyze_competitor(your_app_data)
|
||||||
|
|
||||||
|
# Analyze competitors
|
||||||
|
competitor_comparison = self.compare_competitors(competitors_data)
|
||||||
|
|
||||||
|
# Identify keyword gaps
|
||||||
|
your_keywords = set(your_analysis['keyword_strategy']['primary_keywords'])
|
||||||
|
competitor_keywords = set(competitor_comparison['common_keywords'])
|
||||||
|
missing_keywords = competitor_keywords - your_keywords
|
||||||
|
|
||||||
|
# Identify rating gap
|
||||||
|
avg_competitor_rating = competitor_comparison['rating_analysis']['average_rating']
|
||||||
|
rating_gap = avg_competitor_rating - your_analysis['rating_metrics']['rating']
|
||||||
|
|
||||||
|
# Identify description length gap
|
||||||
|
avg_competitor_desc_length = sum(
|
||||||
|
len(comp['description_analysis']['text'])
|
||||||
|
for comp in competitor_comparison['ranked_competitors']
|
||||||
|
) / len(competitor_comparison['ranked_competitors'])
|
||||||
|
your_desc_length = len(your_analysis['description_analysis']['text'])
|
||||||
|
desc_length_gap = avg_competitor_desc_length - your_desc_length
|
||||||
|
|
||||||
|
return {
|
||||||
|
'your_app': your_analysis,
|
||||||
|
'keyword_gaps': {
|
||||||
|
'missing_keywords': list(missing_keywords)[:10],
|
||||||
|
'recommendations': self._generate_keyword_recommendations(missing_keywords)
|
||||||
|
},
|
||||||
|
'rating_gap': {
|
||||||
|
'your_rating': your_analysis['rating_metrics']['rating'],
|
||||||
|
'average_competitor_rating': avg_competitor_rating,
|
||||||
|
'gap': round(rating_gap, 2),
|
||||||
|
'action_items': self._generate_rating_improvement_actions(rating_gap)
|
||||||
|
},
|
||||||
|
'content_gap': {
|
||||||
|
'your_description_length': your_desc_length,
|
||||||
|
'average_competitor_length': int(avg_competitor_desc_length),
|
||||||
|
'gap': int(desc_length_gap),
|
||||||
|
'recommendations': self._generate_content_recommendations(desc_length_gap)
|
||||||
|
},
|
||||||
|
'competitive_positioning': self._assess_competitive_position(
|
||||||
|
your_analysis,
|
||||||
|
competitor_comparison
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def _analyze_title(self, title: str) -> Dict[str, Any]:
|
||||||
|
"""Analyze title structure and keyword usage."""
|
||||||
|
parts = re.split(r'[-' + r':|]', title)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'title': title,
|
||||||
|
'length': len(title),
|
||||||
|
'has_brand': len(parts) > 0,
|
||||||
|
'has_keywords': len(parts) > 1,
|
||||||
|
'components': [part.strip() for part in parts],
|
||||||
|
'word_count': len(title.split()),
|
||||||
|
'strategy': 'brand_plus_keywords' if len(parts) > 1 else 'brand_only'
|
||||||
|
}
|
||||||
|
|
||||||
|
def _analyze_description(self, description: str) -> Dict[str, Any]:
|
||||||
|
"""Analyze description structure and content."""
|
||||||
|
lines = description.split('\n')
|
||||||
|
word_count = len(description.split())
|
||||||
|
|
||||||
|
# Check for structural elements
|
||||||
|
has_bullet_points = '•' in description or '*' in description
|
||||||
|
has_sections = any(line.isupper() for line in lines if len(line) > 0)
|
||||||
|
has_call_to_action = any(
|
||||||
|
cta in description.lower()
|
||||||
|
for cta in ['download', 'try', 'get', 'start', 'join']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract features mentioned
|
||||||
|
features = self._extract_features(description)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'text': description,
|
||||||
|
'length': len(description),
|
||||||
|
'word_count': word_count,
|
||||||
|
'structure': {
|
||||||
|
'has_bullet_points': has_bullet_points,
|
||||||
|
'has_sections': has_sections,
|
||||||
|
'has_call_to_action': has_call_to_action
|
||||||
|
},
|
||||||
|
'features_mentioned': features,
|
||||||
|
'readability': 'good' if 50 <= word_count <= 300 else 'needs_improvement'
|
||||||
|
}
|
||||||
|
|
||||||
|
def _extract_keyword_strategy(
|
||||||
|
self,
|
||||||
|
title: str,
|
||||||
|
description: str,
|
||||||
|
explicit_keywords: List[str]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Extract keyword strategy from metadata."""
|
||||||
|
# Extract keywords from title
|
||||||
|
title_keywords = [word.lower() for word in title.split() if len(word) > 3]
|
||||||
|
|
||||||
|
# Extract frequently used words from description
|
||||||
|
desc_words = re.findall(r'\b\w{4,}\b', description.lower())
|
||||||
|
word_freq = Counter(desc_words)
|
||||||
|
frequent_words = [word for word, count in word_freq.most_common(15) if count > 2]
|
||||||
|
|
||||||
|
# Combine with explicit keywords
|
||||||
|
all_keywords = list(set(title_keywords + frequent_words + explicit_keywords))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'primary_keywords': title_keywords,
|
||||||
|
'description_keywords': frequent_words[:10],
|
||||||
|
'explicit_keywords': explicit_keywords,
|
||||||
|
'total_unique_keywords': len(all_keywords),
|
||||||
|
'keyword_focus': self._assess_keyword_focus(title_keywords, frequent_words)
|
||||||
|
}
|
||||||
|
|
||||||
|
def _assess_rating_quality(self, rating: float, ratings_count: int) -> str:
|
||||||
|
"""Assess the quality of ratings."""
|
||||||
|
if ratings_count < 100:
|
||||||
|
return 'insufficient_data'
|
||||||
|
elif rating >= 4.5 and ratings_count > 1000:
|
||||||
|
return 'excellent'
|
||||||
|
elif rating >= 4.0 and ratings_count > 500:
|
||||||
|
return 'good'
|
||||||
|
elif rating >= 3.5:
|
||||||
|
return 'average'
|
||||||
|
else:
|
||||||
|
return 'poor'
|
||||||
|
|
||||||
|
def _calculate_competitive_strength(
|
||||||
|
self,
|
||||||
|
rating: float,
|
||||||
|
ratings_count: int,
|
||||||
|
description_length: int
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Calculate overall competitive strength (0-100).
|
||||||
|
|
||||||
|
Factors:
|
||||||
|
- Rating quality (40%)
|
||||||
|
- Rating volume (30%)
|
||||||
|
- Metadata quality (30%)
|
||||||
|
"""
|
||||||
|
# Rating quality score (0-40)
|
||||||
|
rating_score = (rating / 5.0) * 40
|
||||||
|
|
||||||
|
# Rating volume score (0-30)
|
||||||
|
volume_score = min((ratings_count / 10000) * 30, 30)
|
||||||
|
|
||||||
|
# Metadata quality score (0-30)
|
||||||
|
metadata_score = min((description_length / 2000) * 30, 30)
|
||||||
|
|
||||||
|
total_score = rating_score + volume_score + metadata_score
|
||||||
|
|
||||||
|
return round(total_score, 1)
|
||||||
|
|
||||||
|
def _identify_differentiators(self, description: str) -> List[str]:
|
||||||
|
"""Identify key differentiators from description."""
|
||||||
|
differentiator_keywords = [
|
||||||
|
'unique', 'only', 'first', 'best', 'leading', 'exclusive',
|
||||||
|
'revolutionary', 'innovative', 'patent', 'award'
|
||||||
|
]
|
||||||
|
|
||||||
|
differentiators = []
|
||||||
|
sentences = description.split('.')
|
||||||
|
|
||||||
|
for sentence in sentences:
|
||||||
|
sentence_lower = sentence.lower()
|
||||||
|
if any(keyword in sentence_lower for keyword in differentiator_keywords):
|
||||||
|
differentiators.append(sentence.strip())
|
||||||
|
|
||||||
|
return differentiators[:5]
|
||||||
|
|
||||||
|
def _find_common_keywords(self, all_keywords: List[str]) -> List[str]:
|
||||||
|
"""Find keywords used by multiple competitors."""
|
||||||
|
keyword_counts = Counter(all_keywords)
|
||||||
|
# Return keywords used by at least 2 competitors
|
||||||
|
common = [kw for kw, count in keyword_counts.items() if count >= 2]
|
||||||
|
return sorted(common, key=lambda x: keyword_counts[x], reverse=True)[:20]
|
||||||
|
|
||||||
|
def _identify_keyword_gaps(self, analyses: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
|
"""Identify keywords used by some competitors but not others."""
|
||||||
|
all_keywords_by_app = {}
|
||||||
|
|
||||||
|
for analysis in analyses:
|
||||||
|
app_name = analysis['app_name']
|
||||||
|
keywords = analysis['keyword_strategy']['primary_keywords']
|
||||||
|
all_keywords_by_app[app_name] = set(keywords)
|
||||||
|
|
||||||
|
# Find keywords used by some but not all
|
||||||
|
all_keywords_set = set()
|
||||||
|
for keywords in all_keywords_by_app.values():
|
||||||
|
all_keywords_set.update(keywords)
|
||||||
|
|
||||||
|
gaps = []
|
||||||
|
for keyword in all_keywords_set:
|
||||||
|
using_apps = [
|
||||||
|
app for app, keywords in all_keywords_by_app.items()
|
||||||
|
if keyword in keywords
|
||||||
|
]
|
||||||
|
if 1 < len(using_apps) < len(analyses):
|
||||||
|
gaps.append({
|
||||||
|
'keyword': keyword,
|
||||||
|
'used_by': using_apps,
|
||||||
|
'usage_percentage': round(len(using_apps) / len(analyses) * 100, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
return sorted(gaps, key=lambda x: x['usage_percentage'], reverse=True)[:15]
|
||||||
|
|
||||||
|
def _analyze_rating_distribution(self, analyses: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
|
"""Analyze rating distribution across competitors."""
|
||||||
|
ratings = [a['rating_metrics']['rating'] for a in analyses]
|
||||||
|
ratings_counts = [a['rating_metrics']['ratings_count'] for a in analyses]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'average_rating': round(sum(ratings) / len(ratings), 2),
|
||||||
|
'highest_rating': max(ratings),
|
||||||
|
'lowest_rating': min(ratings),
|
||||||
|
'average_ratings_count': int(sum(ratings_counts) / len(ratings_counts)),
|
||||||
|
'total_ratings_in_category': sum(ratings_counts)
|
||||||
|
}
|
||||||
|
|
||||||
|
def _identify_best_practices(self, ranked_competitors: List[Dict[str, Any]]) -> List[str]:
|
||||||
|
"""Identify best practices from top competitors."""
|
||||||
|
if not ranked_competitors:
|
||||||
|
return []
|
||||||
|
|
||||||
|
top_competitor = ranked_competitors[0]
|
||||||
|
practices = []
|
||||||
|
|
||||||
|
# Title strategy
|
||||||
|
title_analysis = top_competitor['title_analysis']
|
||||||
|
if title_analysis['has_keywords']:
|
||||||
|
practices.append(
|
||||||
|
f"Title Strategy: Include primary keyword in title (e.g., '{title_analysis['title']}')"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Description structure
|
||||||
|
desc_analysis = top_competitor['description_analysis']
|
||||||
|
if desc_analysis['structure']['has_bullet_points']:
|
||||||
|
practices.append("Description: Use bullet points to highlight key features")
|
||||||
|
|
||||||
|
if desc_analysis['structure']['has_sections']:
|
||||||
|
practices.append("Description: Organize content with clear section headers")
|
||||||
|
|
||||||
|
# Rating strategy
|
||||||
|
rating_quality = top_competitor['rating_metrics']['rating_quality']
|
||||||
|
if rating_quality in ['excellent', 'good']:
|
||||||
|
practices.append(
|
||||||
|
f"Ratings: Maintain high rating quality ({top_competitor['rating_metrics']['rating']}★) "
|
||||||
|
f"with significant volume ({top_competitor['rating_metrics']['ratings_count']} ratings)"
|
||||||
|
)
|
||||||
|
|
||||||
|
return practices[:5]
|
||||||
|
|
||||||
|
def _identify_opportunities(
|
||||||
|
self,
|
||||||
|
analyses: List[Dict[str, Any]],
|
||||||
|
common_keywords: List[str],
|
||||||
|
keyword_gaps: List[Dict[str, Any]]
|
||||||
|
) -> List[str]:
|
||||||
|
"""Identify ASO opportunities based on competitive analysis."""
|
||||||
|
opportunities = []
|
||||||
|
|
||||||
|
# Keyword opportunities from gaps
|
||||||
|
if keyword_gaps:
|
||||||
|
underutilized_keywords = [
|
||||||
|
gap['keyword'] for gap in keyword_gaps
|
||||||
|
if gap['usage_percentage'] < 50
|
||||||
|
]
|
||||||
|
if underutilized_keywords:
|
||||||
|
opportunities.append(
|
||||||
|
f"Target underutilized keywords: {', '.join(underutilized_keywords[:5])}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Rating opportunity
|
||||||
|
avg_rating = sum(a['rating_metrics']['rating'] for a in analyses) / len(analyses)
|
||||||
|
if avg_rating < 4.5:
|
||||||
|
opportunities.append(
|
||||||
|
f"Category average rating is {avg_rating:.1f} - opportunity to differentiate with higher ratings"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Content depth opportunity
|
||||||
|
avg_desc_length = sum(
|
||||||
|
a['description_analysis']['length'] for a in analyses
|
||||||
|
) / len(analyses)
|
||||||
|
if avg_desc_length < 1500:
|
||||||
|
opportunities.append(
|
||||||
|
"Competitors have relatively short descriptions - opportunity to provide more comprehensive information"
|
||||||
|
)
|
||||||
|
|
||||||
|
return opportunities[:5]
|
||||||
|
|
||||||
|
def _extract_features(self, description: str) -> List[str]:
|
||||||
|
"""Extract feature mentions from description."""
|
||||||
|
# Look for bullet points or numbered lists
|
||||||
|
lines = description.split('\n')
|
||||||
|
features = []
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
# Check if line starts with bullet or number
|
||||||
|
if line and (line[0] in ['•', '*', '-', '✓'] or line[0].isdigit()):
|
||||||
|
# Clean the line
|
||||||
|
cleaned = re.sub(r'^[•*\-✓\d.)\s]+', '', line)
|
||||||
|
if cleaned:
|
||||||
|
features.append(cleaned)
|
||||||
|
|
||||||
|
return features[:10]
|
||||||
|
|
||||||
|
def _assess_keyword_focus(
|
||||||
|
self,
|
||||||
|
title_keywords: List[str],
|
||||||
|
description_keywords: List[str]
|
||||||
|
) -> str:
|
||||||
|
"""Assess keyword focus strategy."""
|
||||||
|
overlap = set(title_keywords) & set(description_keywords)
|
||||||
|
|
||||||
|
if len(overlap) >= 3:
|
||||||
|
return 'consistent_focus'
|
||||||
|
elif len(overlap) >= 1:
|
||||||
|
return 'moderate_focus'
|
||||||
|
else:
|
||||||
|
return 'broad_focus'
|
||||||
|
|
||||||
|
def _generate_keyword_recommendations(self, missing_keywords: set) -> List[str]:
|
||||||
|
"""Generate recommendations for missing keywords."""
|
||||||
|
if not missing_keywords:
|
||||||
|
return ["Your keyword coverage is comprehensive"]
|
||||||
|
|
||||||
|
recommendations = []
|
||||||
|
missing_list = list(missing_keywords)[:5]
|
||||||
|
|
||||||
|
recommendations.append(
|
||||||
|
f"Consider adding these competitor keywords: {', '.join(missing_list)}"
|
||||||
|
)
|
||||||
|
recommendations.append(
|
||||||
|
"Test keyword variations in subtitle/promotional text first"
|
||||||
|
)
|
||||||
|
recommendations.append(
|
||||||
|
"Monitor competitor keyword changes monthly"
|
||||||
|
)
|
||||||
|
|
||||||
|
return recommendations
|
||||||
|
|
||||||
|
def _generate_rating_improvement_actions(self, rating_gap: float) -> List[str]:
|
||||||
|
"""Generate actions to improve ratings."""
|
||||||
|
actions = []
|
||||||
|
|
||||||
|
if rating_gap > 0.5:
|
||||||
|
actions.append("CRITICAL: Significant rating gap - prioritize user satisfaction improvements")
|
||||||
|
actions.append("Analyze negative reviews to identify top issues")
|
||||||
|
actions.append("Implement in-app rating prompts after positive experiences")
|
||||||
|
actions.append("Respond to all negative reviews professionally")
|
||||||
|
elif rating_gap > 0.2:
|
||||||
|
actions.append("Focus on incremental improvements to close rating gap")
|
||||||
|
actions.append("Optimize timing of rating requests")
|
||||||
|
else:
|
||||||
|
actions.append("Ratings are competitive - maintain quality and continue improvements")
|
||||||
|
|
||||||
|
return actions
|
||||||
|
|
||||||
|
def _generate_content_recommendations(self, desc_length_gap: int) -> List[str]:
|
||||||
|
"""Generate content recommendations based on length gap."""
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
if desc_length_gap > 500:
|
||||||
|
recommendations.append(
|
||||||
|
"Expand description to match competitor detail level"
|
||||||
|
)
|
||||||
|
recommendations.append(
|
||||||
|
"Add use case examples and success stories"
|
||||||
|
)
|
||||||
|
recommendations.append(
|
||||||
|
"Include more feature explanations and benefits"
|
||||||
|
)
|
||||||
|
elif desc_length_gap < -500:
|
||||||
|
recommendations.append(
|
||||||
|
"Consider condensing description for better readability"
|
||||||
|
)
|
||||||
|
recommendations.append(
|
||||||
|
"Focus on most important features first"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
recommendations.append(
|
||||||
|
"Description length is competitive"
|
||||||
|
)
|
||||||
|
|
||||||
|
return recommendations
|
||||||
|
|
||||||
|
def _assess_competitive_position(
|
||||||
|
self,
|
||||||
|
your_analysis: Dict[str, Any],
|
||||||
|
competitor_comparison: Dict[str, Any]
|
||||||
|
) -> str:
|
||||||
|
"""Assess your competitive position."""
|
||||||
|
your_strength = your_analysis['competitive_strength']
|
||||||
|
competitors = competitor_comparison['ranked_competitors']
|
||||||
|
|
||||||
|
if not competitors:
|
||||||
|
return "No comparison data available"
|
||||||
|
|
||||||
|
# Find where you'd rank
|
||||||
|
better_than_count = sum(
|
||||||
|
1 for comp in competitors
|
||||||
|
if your_strength > comp['competitive_strength']
|
||||||
|
)
|
||||||
|
|
||||||
|
position_percentage = (better_than_count / len(competitors)) * 100
|
||||||
|
|
||||||
|
if position_percentage >= 75:
|
||||||
|
return "Strong Position: Top quartile in competitive strength"
|
||||||
|
elif position_percentage >= 50:
|
||||||
|
return "Competitive Position: Above average, opportunities for improvement"
|
||||||
|
elif position_percentage >= 25:
|
||||||
|
return "Challenging Position: Below average, requires strategic improvements"
|
||||||
|
else:
|
||||||
|
return "Weak Position: Bottom quartile, major ASO overhaul needed"
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_competitor_set(
|
||||||
|
category: str,
|
||||||
|
competitors_data: List[Dict[str, Any]],
|
||||||
|
platform: str = 'apple'
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Convenience function to analyze a set of competitors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
category: App category
|
||||||
|
competitors_data: List of competitor data
|
||||||
|
platform: 'apple' or 'google'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complete competitive analysis
|
||||||
|
"""
|
||||||
|
analyzer = CompetitorAnalyzer(category, platform)
|
||||||
|
return analyzer.compare_competitors(competitors_data)
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
{
|
||||||
|
"request_type": "keyword_research",
|
||||||
|
"app_name": "TaskFlow Pro",
|
||||||
|
"keyword_analysis": {
|
||||||
|
"total_keywords_analyzed": 25,
|
||||||
|
"primary_keywords": [
|
||||||
|
{
|
||||||
|
"keyword": "task manager",
|
||||||
|
"search_volume": 45000,
|
||||||
|
"competition_level": "high",
|
||||||
|
"relevance_score": 0.95,
|
||||||
|
"difficulty_score": 72.5,
|
||||||
|
"potential_score": 78.3,
|
||||||
|
"recommendation": "High priority - target immediately"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keyword": "productivity app",
|
||||||
|
"search_volume": 38000,
|
||||||
|
"competition_level": "high",
|
||||||
|
"relevance_score": 0.90,
|
||||||
|
"difficulty_score": 68.2,
|
||||||
|
"potential_score": 75.1,
|
||||||
|
"recommendation": "High priority - target immediately"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keyword": "todo list",
|
||||||
|
"search_volume": 52000,
|
||||||
|
"competition_level": "very_high",
|
||||||
|
"relevance_score": 0.85,
|
||||||
|
"difficulty_score": 78.9,
|
||||||
|
"potential_score": 71.4,
|
||||||
|
"recommendation": "High priority - target immediately"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"secondary_keywords": [
|
||||||
|
{
|
||||||
|
"keyword": "team task manager",
|
||||||
|
"search_volume": 8500,
|
||||||
|
"competition_level": "medium",
|
||||||
|
"relevance_score": 0.88,
|
||||||
|
"difficulty_score": 42.3,
|
||||||
|
"potential_score": 68.7,
|
||||||
|
"recommendation": "Good opportunity - include in metadata"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keyword": "project planning app",
|
||||||
|
"search_volume": 12000,
|
||||||
|
"competition_level": "medium",
|
||||||
|
"relevance_score": 0.75,
|
||||||
|
"difficulty_score": 48.1,
|
||||||
|
"potential_score": 64.2,
|
||||||
|
"recommendation": "Good opportunity - include in metadata"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"long_tail_keywords": [
|
||||||
|
{
|
||||||
|
"keyword": "ai task prioritization",
|
||||||
|
"search_volume": 2800,
|
||||||
|
"competition_level": "low",
|
||||||
|
"relevance_score": 0.95,
|
||||||
|
"difficulty_score": 25.4,
|
||||||
|
"potential_score": 82.6,
|
||||||
|
"recommendation": "Excellent long-tail opportunity"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keyword": "team productivity tool",
|
||||||
|
"search_volume": 3500,
|
||||||
|
"competition_level": "low",
|
||||||
|
"relevance_score": 0.85,
|
||||||
|
"difficulty_score": 28.7,
|
||||||
|
"potential_score": 79.3,
|
||||||
|
"recommendation": "Excellent long-tail opportunity"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"competitor_insights": {
|
||||||
|
"competitors_analyzed": 4,
|
||||||
|
"common_keywords": [
|
||||||
|
"task",
|
||||||
|
"todo",
|
||||||
|
"list",
|
||||||
|
"productivity",
|
||||||
|
"organize",
|
||||||
|
"manage"
|
||||||
|
],
|
||||||
|
"keyword_gaps": [
|
||||||
|
{
|
||||||
|
"keyword": "ai prioritization",
|
||||||
|
"used_by": ["None of the major competitors"],
|
||||||
|
"opportunity": "Unique positioning opportunity"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keyword": "smart task manager",
|
||||||
|
"used_by": ["Things 3"],
|
||||||
|
"opportunity": "Underutilized by most competitors"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata_recommendations": {
|
||||||
|
"apple_app_store": {
|
||||||
|
"title_options": [
|
||||||
|
{
|
||||||
|
"title": "TaskFlow - AI Task Manager",
|
||||||
|
"length": 26,
|
||||||
|
"keywords_included": ["task manager", "ai"],
|
||||||
|
"strategy": "brand_plus_primary"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "TaskFlow: Smart Todo & Tasks",
|
||||||
|
"length": 29,
|
||||||
|
"keywords_included": ["todo", "tasks"],
|
||||||
|
"strategy": "brand_plus_multiple"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subtitle_recommendation": "AI-Powered Team Productivity",
|
||||||
|
"keyword_field": "productivity,organize,planner,schedule,workflow,reminders,collaboration,calendar,sync,priorities",
|
||||||
|
"description_focus": "Lead with AI differentiation, emphasize team features"
|
||||||
|
},
|
||||||
|
"google_play_store": {
|
||||||
|
"title_options": [
|
||||||
|
{
|
||||||
|
"title": "TaskFlow - AI Task Manager & Team Productivity",
|
||||||
|
"length": 48,
|
||||||
|
"keywords_included": ["task manager", "ai", "team", "productivity"],
|
||||||
|
"strategy": "keyword_rich"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"short_description_recommendation": "AI task manager - Organize, prioritize, and collaborate with your team",
|
||||||
|
"description_focus": "Keywords naturally integrated throughout 4000 character description"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strategic_recommendations": [
|
||||||
|
"Focus on 'AI prioritization' as unique differentiator - low competition, high relevance",
|
||||||
|
"Target 'team task manager' and 'team productivity' keywords - good search volume, lower competition than generic terms",
|
||||||
|
"Include long-tail keywords in description for additional discovery opportunities",
|
||||||
|
"Test title variations with A/B testing after launch",
|
||||||
|
"Monitor competitor keyword changes quarterly"
|
||||||
|
],
|
||||||
|
"priority_actions": [
|
||||||
|
{
|
||||||
|
"action": "Optimize app title with primary keyword",
|
||||||
|
"priority": "high",
|
||||||
|
"expected_impact": "15-25% improvement in search visibility"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "Create description highlighting AI features with natural keyword integration",
|
||||||
|
"priority": "high",
|
||||||
|
"expected_impact": "10-15% improvement in conversion rate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "Plan A/B tests for icon and screenshots post-launch",
|
||||||
|
"priority": "medium",
|
||||||
|
"expected_impact": "5-10% improvement in conversion rate"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aso_health_estimate": {
|
||||||
|
"current_score": "N/A (pre-launch)",
|
||||||
|
"potential_score_with_optimizations": "75-80/100",
|
||||||
|
"key_strengths": [
|
||||||
|
"Unique AI differentiation",
|
||||||
|
"Clear target audience",
|
||||||
|
"Strong feature set"
|
||||||
|
],
|
||||||
|
"areas_to_develop": [
|
||||||
|
"Build rating volume post-launch",
|
||||||
|
"Monitor and respond to reviews",
|
||||||
|
"Continuous keyword optimization"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,406 @@
|
|||||||
|
"""
|
||||||
|
Keyword analysis module for App Store Optimization.
|
||||||
|
Analyzes keyword search volume, competition, and relevance for app discovery.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, List, Any, Optional, Tuple
|
||||||
|
import re
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
|
||||||
|
class KeywordAnalyzer:
|
||||||
|
"""Analyzes keywords for ASO effectiveness."""
|
||||||
|
|
||||||
|
# Competition level thresholds (based on number of competing apps)
|
||||||
|
COMPETITION_THRESHOLDS = {
|
||||||
|
'low': 1000,
|
||||||
|
'medium': 5000,
|
||||||
|
'high': 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
# Search volume categories (monthly searches estimate)
|
||||||
|
VOLUME_CATEGORIES = {
|
||||||
|
'very_low': 1000,
|
||||||
|
'low': 5000,
|
||||||
|
'medium': 20000,
|
||||||
|
'high': 100000,
|
||||||
|
'very_high': 500000
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize keyword analyzer."""
|
||||||
|
self.analyzed_keywords = {}
|
||||||
|
|
||||||
|
def analyze_keyword(
|
||||||
|
self,
|
||||||
|
keyword: str,
|
||||||
|
search_volume: int = 0,
|
||||||
|
competing_apps: int = 0,
|
||||||
|
relevance_score: float = 0.0
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Analyze a single keyword for ASO potential.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keyword: The keyword to analyze
|
||||||
|
search_volume: Estimated monthly search volume
|
||||||
|
competing_apps: Number of apps competing for this keyword
|
||||||
|
relevance_score: Relevance to your app (0.0-1.0)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with keyword analysis
|
||||||
|
"""
|
||||||
|
competition_level = self._calculate_competition_level(competing_apps)
|
||||||
|
volume_category = self._categorize_search_volume(search_volume)
|
||||||
|
difficulty_score = self._calculate_keyword_difficulty(
|
||||||
|
search_volume,
|
||||||
|
competing_apps
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate potential score (0-100)
|
||||||
|
potential_score = self._calculate_potential_score(
|
||||||
|
search_volume,
|
||||||
|
competing_apps,
|
||||||
|
relevance_score
|
||||||
|
)
|
||||||
|
|
||||||
|
analysis = {
|
||||||
|
'keyword': keyword,
|
||||||
|
'search_volume': search_volume,
|
||||||
|
'volume_category': volume_category,
|
||||||
|
'competing_apps': competing_apps,
|
||||||
|
'competition_level': competition_level,
|
||||||
|
'relevance_score': relevance_score,
|
||||||
|
'difficulty_score': difficulty_score,
|
||||||
|
'potential_score': potential_score,
|
||||||
|
'recommendation': self._generate_recommendation(
|
||||||
|
potential_score,
|
||||||
|
difficulty_score,
|
||||||
|
relevance_score
|
||||||
|
),
|
||||||
|
'keyword_length': len(keyword.split()),
|
||||||
|
'is_long_tail': len(keyword.split()) >= 3
|
||||||
|
}
|
||||||
|
|
||||||
|
self.analyzed_keywords[keyword] = analysis
|
||||||
|
return analysis
|
||||||
|
|
||||||
|
def compare_keywords(self, keywords_data: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Compare multiple keywords and rank by potential.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keywords_data: List of dicts with keyword, search_volume, competing_apps, relevance_score
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Comparison report with ranked keywords
|
||||||
|
"""
|
||||||
|
analyses = []
|
||||||
|
for kw_data in keywords_data:
|
||||||
|
analysis = self.analyze_keyword(
|
||||||
|
keyword=kw_data['keyword'],
|
||||||
|
search_volume=kw_data.get('search_volume', 0),
|
||||||
|
competing_apps=kw_data.get('competing_apps', 0),
|
||||||
|
relevance_score=kw_data.get('relevance_score', 0.0)
|
||||||
|
)
|
||||||
|
analyses.append(analysis)
|
||||||
|
|
||||||
|
# Sort by potential score (descending)
|
||||||
|
ranked_keywords = sorted(
|
||||||
|
analyses,
|
||||||
|
key=lambda x: x['potential_score'],
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Categorize keywords
|
||||||
|
primary_keywords = [
|
||||||
|
kw for kw in ranked_keywords
|
||||||
|
if kw['potential_score'] >= 70 and kw['relevance_score'] >= 0.8
|
||||||
|
]
|
||||||
|
|
||||||
|
secondary_keywords = [
|
||||||
|
kw for kw in ranked_keywords
|
||||||
|
if 50 <= kw['potential_score'] < 70 and kw['relevance_score'] >= 0.6
|
||||||
|
]
|
||||||
|
|
||||||
|
long_tail_keywords = [
|
||||||
|
kw for kw in ranked_keywords
|
||||||
|
if kw['is_long_tail'] and kw['relevance_score'] >= 0.7
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total_keywords_analyzed': len(analyses),
|
||||||
|
'ranked_keywords': ranked_keywords,
|
||||||
|
'primary_keywords': primary_keywords[:5], # Top 5
|
||||||
|
'secondary_keywords': secondary_keywords[:10], # Top 10
|
||||||
|
'long_tail_keywords': long_tail_keywords[:10], # Top 10
|
||||||
|
'summary': self._generate_comparison_summary(
|
||||||
|
primary_keywords,
|
||||||
|
secondary_keywords,
|
||||||
|
long_tail_keywords
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def find_long_tail_opportunities(
|
||||||
|
self,
|
||||||
|
base_keyword: str,
|
||||||
|
modifiers: List[str]
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Generate long-tail keyword variations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_keyword: Core keyword (e.g., "task manager")
|
||||||
|
modifiers: List of modifiers (e.g., ["free", "simple", "team"])
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of long-tail keyword suggestions
|
||||||
|
"""
|
||||||
|
long_tail_keywords = []
|
||||||
|
|
||||||
|
# Generate combinations
|
||||||
|
for modifier in modifiers:
|
||||||
|
# Modifier + base
|
||||||
|
variation1 = f"{modifier} {base_keyword}"
|
||||||
|
long_tail_keywords.append({
|
||||||
|
'keyword': variation1,
|
||||||
|
'pattern': 'modifier_base',
|
||||||
|
'estimated_competition': 'low',
|
||||||
|
'rationale': f"Less competitive variation of '{base_keyword}'"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Base + modifier
|
||||||
|
variation2 = f"{base_keyword} {modifier}"
|
||||||
|
long_tail_keywords.append({
|
||||||
|
'keyword': variation2,
|
||||||
|
'pattern': 'base_modifier',
|
||||||
|
'estimated_competition': 'low',
|
||||||
|
'rationale': f"Specific use-case variation of '{base_keyword}'"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add question-based long-tail
|
||||||
|
question_words = ['how', 'what', 'best', 'top']
|
||||||
|
for q_word in question_words:
|
||||||
|
question_keyword = f"{q_word} {base_keyword}"
|
||||||
|
long_tail_keywords.append({
|
||||||
|
'keyword': question_keyword,
|
||||||
|
'pattern': 'question_based',
|
||||||
|
'estimated_competition': 'very_low',
|
||||||
|
'rationale': f"Informational search query"
|
||||||
|
})
|
||||||
|
|
||||||
|
return long_tail_keywords
|
||||||
|
|
||||||
|
def extract_keywords_from_text(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
min_word_length: int = 3
|
||||||
|
) -> List[Tuple[str, int]]:
|
||||||
|
"""
|
||||||
|
Extract potential keywords from text (descriptions, reviews).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Text to analyze
|
||||||
|
min_word_length: Minimum word length to consider
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of (keyword, frequency) tuples
|
||||||
|
"""
|
||||||
|
# Clean and normalize text
|
||||||
|
text = text.lower()
|
||||||
|
text = re.sub(r'[^\w\s]', ' ', text)
|
||||||
|
|
||||||
|
# Extract words
|
||||||
|
words = text.split()
|
||||||
|
|
||||||
|
# Filter by length
|
||||||
|
words = [w for w in words if len(w) >= min_word_length]
|
||||||
|
|
||||||
|
# Remove common stop words
|
||||||
|
stop_words = {
|
||||||
|
'the', 'and', 'for', 'with', 'this', 'that', 'from', 'have',
|
||||||
|
'but', 'not', 'you', 'all', 'can', 'are', 'was', 'were', 'been'
|
||||||
|
}
|
||||||
|
words = [w for w in words if w not in stop_words]
|
||||||
|
|
||||||
|
# Count frequency
|
||||||
|
word_counts = Counter(words)
|
||||||
|
|
||||||
|
# Extract 2-word phrases
|
||||||
|
phrases = []
|
||||||
|
for i in range(len(words) - 1):
|
||||||
|
phrase = f"{words[i]} {words[i+1]}"
|
||||||
|
phrases.append(phrase)
|
||||||
|
|
||||||
|
phrase_counts = Counter(phrases)
|
||||||
|
|
||||||
|
# Combine and sort
|
||||||
|
all_keywords = list(word_counts.items()) + list(phrase_counts.items())
|
||||||
|
all_keywords.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
|
||||||
|
return all_keywords[:50] # Top 50
|
||||||
|
|
||||||
|
def calculate_keyword_density(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
target_keywords: List[str]
|
||||||
|
) -> Dict[str, float]:
|
||||||
|
"""
|
||||||
|
Calculate keyword density in text.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Text to analyze (title, description)
|
||||||
|
target_keywords: Keywords to check density for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of keyword: density (percentage)
|
||||||
|
"""
|
||||||
|
text_lower = text.lower()
|
||||||
|
total_words = len(text_lower.split())
|
||||||
|
|
||||||
|
densities = {}
|
||||||
|
for keyword in target_keywords:
|
||||||
|
keyword_lower = keyword.lower()
|
||||||
|
occurrences = text_lower.count(keyword_lower)
|
||||||
|
density = (occurrences / total_words) * 100 if total_words > 0 else 0
|
||||||
|
densities[keyword] = round(density, 2)
|
||||||
|
|
||||||
|
return densities
|
||||||
|
|
||||||
|
def _calculate_competition_level(self, competing_apps: int) -> str:
|
||||||
|
"""Determine competition level based on number of competing apps."""
|
||||||
|
if competing_apps < self.COMPETITION_THRESHOLDS['low']:
|
||||||
|
return 'low'
|
||||||
|
elif competing_apps < self.COMPETITION_THRESHOLDS['medium']:
|
||||||
|
return 'medium'
|
||||||
|
elif competing_apps < self.COMPETITION_THRESHOLDS['high']:
|
||||||
|
return 'high'
|
||||||
|
else:
|
||||||
|
return 'very_high'
|
||||||
|
|
||||||
|
def _categorize_search_volume(self, search_volume: int) -> str:
|
||||||
|
"""Categorize search volume."""
|
||||||
|
if search_volume < self.VOLUME_CATEGORIES['very_low']:
|
||||||
|
return 'very_low'
|
||||||
|
elif search_volume < self.VOLUME_CATEGORIES['low']:
|
||||||
|
return 'low'
|
||||||
|
elif search_volume < self.VOLUME_CATEGORIES['medium']:
|
||||||
|
return 'medium'
|
||||||
|
elif search_volume < self.VOLUME_CATEGORIES['high']:
|
||||||
|
return 'high'
|
||||||
|
else:
|
||||||
|
return 'very_high'
|
||||||
|
|
||||||
|
def _calculate_keyword_difficulty(
|
||||||
|
self,
|
||||||
|
search_volume: int,
|
||||||
|
competing_apps: int
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Calculate keyword difficulty score (0-100).
|
||||||
|
Higher score = harder to rank.
|
||||||
|
"""
|
||||||
|
if competing_apps == 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
# Competition factor (0-1)
|
||||||
|
competition_factor = min(competing_apps / 50000, 1.0)
|
||||||
|
|
||||||
|
# Volume factor (0-1) - higher volume = more difficulty
|
||||||
|
volume_factor = min(search_volume / 1000000, 1.0)
|
||||||
|
|
||||||
|
# Difficulty score (weighted average)
|
||||||
|
difficulty = (competition_factor * 0.7 + volume_factor * 0.3) * 100
|
||||||
|
|
||||||
|
return round(difficulty, 1)
|
||||||
|
|
||||||
|
def _calculate_potential_score(
|
||||||
|
self,
|
||||||
|
search_volume: int,
|
||||||
|
competing_apps: int,
|
||||||
|
relevance_score: float
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Calculate overall keyword potential (0-100).
|
||||||
|
Higher score = better opportunity.
|
||||||
|
"""
|
||||||
|
# Volume score (0-40 points)
|
||||||
|
volume_score = min((search_volume / 100000) * 40, 40)
|
||||||
|
|
||||||
|
# Competition score (0-30 points) - inverse relationship
|
||||||
|
if competing_apps > 0:
|
||||||
|
competition_score = max(30 - (competing_apps / 500), 0)
|
||||||
|
else:
|
||||||
|
competition_score = 30
|
||||||
|
|
||||||
|
# Relevance score (0-30 points)
|
||||||
|
relevance_points = relevance_score * 30
|
||||||
|
|
||||||
|
total_score = volume_score + competition_score + relevance_points
|
||||||
|
|
||||||
|
return round(min(total_score, 100), 1)
|
||||||
|
|
||||||
|
def _generate_recommendation(
|
||||||
|
self,
|
||||||
|
potential_score: float,
|
||||||
|
difficulty_score: float,
|
||||||
|
relevance_score: float
|
||||||
|
) -> str:
|
||||||
|
"""Generate actionable recommendation for keyword."""
|
||||||
|
if relevance_score < 0.5:
|
||||||
|
return "Low relevance - avoid targeting"
|
||||||
|
|
||||||
|
if potential_score >= 70:
|
||||||
|
return "High priority - target immediately"
|
||||||
|
elif potential_score >= 50:
|
||||||
|
if difficulty_score < 50:
|
||||||
|
return "Good opportunity - include in metadata"
|
||||||
|
else:
|
||||||
|
return "Competitive - use in description, not title"
|
||||||
|
elif potential_score >= 30:
|
||||||
|
return "Secondary keyword - use for long-tail variations"
|
||||||
|
else:
|
||||||
|
return "Low potential - deprioritize"
|
||||||
|
|
||||||
|
def _generate_comparison_summary(
|
||||||
|
self,
|
||||||
|
primary_keywords: List[Dict[str, Any]],
|
||||||
|
secondary_keywords: List[Dict[str, Any]],
|
||||||
|
long_tail_keywords: List[Dict[str, Any]]
|
||||||
|
) -> str:
|
||||||
|
"""Generate summary of keyword comparison."""
|
||||||
|
summary_parts = []
|
||||||
|
|
||||||
|
summary_parts.append(
|
||||||
|
f"Identified {len(primary_keywords)} high-priority primary keywords."
|
||||||
|
)
|
||||||
|
|
||||||
|
if primary_keywords:
|
||||||
|
top_keyword = primary_keywords[0]['keyword']
|
||||||
|
summary_parts.append(
|
||||||
|
f"Top recommendation: '{top_keyword}' (potential score: {primary_keywords[0]['potential_score']})."
|
||||||
|
)
|
||||||
|
|
||||||
|
summary_parts.append(
|
||||||
|
f"Found {len(secondary_keywords)} secondary keywords for description and metadata."
|
||||||
|
)
|
||||||
|
|
||||||
|
summary_parts.append(
|
||||||
|
f"Discovered {len(long_tail_keywords)} long-tail opportunities with lower competition."
|
||||||
|
)
|
||||||
|
|
||||||
|
return " ".join(summary_parts)
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_keyword_set(keywords_data: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Convenience function to analyze a set of keywords.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keywords_data: List of keyword data dictionaries
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complete analysis report
|
||||||
|
"""
|
||||||
|
analyzer = KeywordAnalyzer()
|
||||||
|
return analyzer.compare_keywords(keywords_data)
|
||||||
@@ -0,0 +1,739 @@
|
|||||||
|
"""
|
||||||
|
Launch checklist module for App Store Optimization.
|
||||||
|
Generates comprehensive pre-launch and update checklists.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
|
class LaunchChecklistGenerator:
|
||||||
|
"""Generates comprehensive checklists for app launches and updates."""
|
||||||
|
|
||||||
|
def __init__(self, platform: str = 'both'):
|
||||||
|
"""
|
||||||
|
Initialize checklist generator.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
platform: 'apple', 'google', or 'both'
|
||||||
|
"""
|
||||||
|
if platform not in ['apple', 'google', 'both']:
|
||||||
|
raise ValueError("Platform must be 'apple', 'google', or 'both'")
|
||||||
|
|
||||||
|
self.platform = platform
|
||||||
|
|
||||||
|
def generate_prelaunch_checklist(
|
||||||
|
self,
|
||||||
|
app_info: Dict[str, Any],
|
||||||
|
launch_date: Optional[str] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Generate comprehensive pre-launch checklist.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_info: App information (name, category, target_audience)
|
||||||
|
launch_date: Target launch date (YYYY-MM-DD)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complete pre-launch checklist
|
||||||
|
"""
|
||||||
|
checklist = {
|
||||||
|
'app_info': app_info,
|
||||||
|
'launch_date': launch_date,
|
||||||
|
'checklists': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate platform-specific checklists
|
||||||
|
if self.platform in ['apple', 'both']:
|
||||||
|
checklist['checklists']['apple'] = self._generate_apple_checklist(app_info)
|
||||||
|
|
||||||
|
if self.platform in ['google', 'both']:
|
||||||
|
checklist['checklists']['google'] = self._generate_google_checklist(app_info)
|
||||||
|
|
||||||
|
# Add universal checklist items
|
||||||
|
checklist['checklists']['universal'] = self._generate_universal_checklist(app_info)
|
||||||
|
|
||||||
|
# Generate timeline
|
||||||
|
if launch_date:
|
||||||
|
checklist['timeline'] = self._generate_launch_timeline(launch_date)
|
||||||
|
|
||||||
|
# Calculate completion status
|
||||||
|
checklist['summary'] = self._calculate_checklist_summary(checklist['checklists'])
|
||||||
|
|
||||||
|
return checklist
|
||||||
|
|
||||||
|
def validate_app_store_compliance(
|
||||||
|
self,
|
||||||
|
app_data: Dict[str, Any],
|
||||||
|
platform: str = 'apple'
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Validate compliance with app store guidelines.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_data: App data including metadata, privacy policy, etc.
|
||||||
|
platform: 'apple' or 'google'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Compliance validation report
|
||||||
|
"""
|
||||||
|
validation_results = {
|
||||||
|
'platform': platform,
|
||||||
|
'is_compliant': True,
|
||||||
|
'errors': [],
|
||||||
|
'warnings': [],
|
||||||
|
'recommendations': []
|
||||||
|
}
|
||||||
|
|
||||||
|
if platform == 'apple':
|
||||||
|
self._validate_apple_compliance(app_data, validation_results)
|
||||||
|
elif platform == 'google':
|
||||||
|
self._validate_google_compliance(app_data, validation_results)
|
||||||
|
|
||||||
|
# Determine overall compliance
|
||||||
|
validation_results['is_compliant'] = len(validation_results['errors']) == 0
|
||||||
|
|
||||||
|
return validation_results
|
||||||
|
|
||||||
|
def create_update_plan(
|
||||||
|
self,
|
||||||
|
current_version: str,
|
||||||
|
planned_features: List[str],
|
||||||
|
update_frequency: str = 'monthly'
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Create update cadence and feature rollout plan.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_version: Current app version
|
||||||
|
planned_features: List of planned features
|
||||||
|
update_frequency: 'weekly', 'biweekly', 'monthly', 'quarterly'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Update plan with cadence and feature schedule
|
||||||
|
"""
|
||||||
|
# Calculate next versions
|
||||||
|
next_versions = self._calculate_next_versions(
|
||||||
|
current_version,
|
||||||
|
update_frequency,
|
||||||
|
len(planned_features)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Distribute features across versions
|
||||||
|
feature_schedule = self._distribute_features(
|
||||||
|
planned_features,
|
||||||
|
next_versions
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate "What's New" templates
|
||||||
|
whats_new_templates = [
|
||||||
|
self._generate_whats_new_template(version_data)
|
||||||
|
for version_data in feature_schedule
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'current_version': current_version,
|
||||||
|
'update_frequency': update_frequency,
|
||||||
|
'planned_updates': len(feature_schedule),
|
||||||
|
'feature_schedule': feature_schedule,
|
||||||
|
'whats_new_templates': whats_new_templates,
|
||||||
|
'recommendations': self._generate_update_recommendations(update_frequency)
|
||||||
|
}
|
||||||
|
|
||||||
|
def optimize_launch_timing(
|
||||||
|
self,
|
||||||
|
app_category: str,
|
||||||
|
target_audience: str,
|
||||||
|
current_date: Optional[str] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Recommend optimal launch timing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_category: App category
|
||||||
|
target_audience: Target audience description
|
||||||
|
current_date: Current date (YYYY-MM-DD), defaults to today
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Launch timing recommendations
|
||||||
|
"""
|
||||||
|
if not current_date:
|
||||||
|
current_date = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
# Analyze launch timing factors
|
||||||
|
day_of_week_rec = self._recommend_day_of_week(app_category)
|
||||||
|
seasonal_rec = self._recommend_seasonal_timing(app_category, current_date)
|
||||||
|
competitive_rec = self._analyze_competitive_timing(app_category)
|
||||||
|
|
||||||
|
# Calculate optimal dates
|
||||||
|
optimal_dates = self._calculate_optimal_dates(
|
||||||
|
current_date,
|
||||||
|
day_of_week_rec,
|
||||||
|
seasonal_rec
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'current_date': current_date,
|
||||||
|
'optimal_launch_dates': optimal_dates,
|
||||||
|
'day_of_week_recommendation': day_of_week_rec,
|
||||||
|
'seasonal_considerations': seasonal_rec,
|
||||||
|
'competitive_timing': competitive_rec,
|
||||||
|
'final_recommendation': self._generate_timing_recommendation(
|
||||||
|
optimal_dates,
|
||||||
|
seasonal_rec
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def plan_seasonal_campaigns(
|
||||||
|
self,
|
||||||
|
app_category: str,
|
||||||
|
current_month: int = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Identify seasonal opportunities for ASO campaigns.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_category: App category
|
||||||
|
current_month: Current month (1-12), defaults to current
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Seasonal campaign opportunities
|
||||||
|
"""
|
||||||
|
if not current_month:
|
||||||
|
current_month = datetime.now().month
|
||||||
|
|
||||||
|
# Identify relevant seasonal events
|
||||||
|
seasonal_opportunities = self._identify_seasonal_opportunities(
|
||||||
|
app_category,
|
||||||
|
current_month
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate campaign ideas
|
||||||
|
campaigns = [
|
||||||
|
self._generate_seasonal_campaign(opportunity)
|
||||||
|
for opportunity in seasonal_opportunities
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'current_month': current_month,
|
||||||
|
'category': app_category,
|
||||||
|
'seasonal_opportunities': seasonal_opportunities,
|
||||||
|
'campaign_ideas': campaigns,
|
||||||
|
'implementation_timeline': self._create_seasonal_timeline(campaigns)
|
||||||
|
}
|
||||||
|
|
||||||
|
def _generate_apple_checklist(self, app_info: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""Generate Apple App Store specific checklist."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'category': 'App Store Connect Setup',
|
||||||
|
'items': [
|
||||||
|
{'task': 'App Store Connect account created', 'status': 'pending'},
|
||||||
|
{'task': 'App bundle ID registered', 'status': 'pending'},
|
||||||
|
{'task': 'App Privacy declarations completed', 'status': 'pending'},
|
||||||
|
{'task': 'Age rating questionnaire completed', 'status': 'pending'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'category': 'Metadata (Apple)',
|
||||||
|
'items': [
|
||||||
|
{'task': 'App title (30 chars max)', 'status': 'pending'},
|
||||||
|
{'task': 'Subtitle (30 chars max)', 'status': 'pending'},
|
||||||
|
{'task': 'Promotional text (170 chars max)', 'status': 'pending'},
|
||||||
|
{'task': 'Description (4000 chars max)', 'status': 'pending'},
|
||||||
|
{'task': 'Keywords (100 chars, comma-separated)', 'status': 'pending'},
|
||||||
|
{'task': 'Category selection (primary + secondary)', 'status': 'pending'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'category': 'Visual Assets (Apple)',
|
||||||
|
'items': [
|
||||||
|
{'task': 'App icon (1024x1024px)', 'status': 'pending'},
|
||||||
|
{'task': 'Screenshots (iPhone 6.7" required)', 'status': 'pending'},
|
||||||
|
{'task': 'Screenshots (iPhone 5.5" required)', 'status': 'pending'},
|
||||||
|
{'task': 'Screenshots (iPad Pro 12.9" if iPad app)', 'status': 'pending'},
|
||||||
|
{'task': 'App preview video (optional but recommended)', 'status': 'pending'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'category': 'Technical Requirements (Apple)',
|
||||||
|
'items': [
|
||||||
|
{'task': 'Build uploaded to App Store Connect', 'status': 'pending'},
|
||||||
|
{'task': 'TestFlight testing completed', 'status': 'pending'},
|
||||||
|
{'task': 'App tested on required iOS versions', 'status': 'pending'},
|
||||||
|
{'task': 'Crash-free rate > 99%', 'status': 'pending'},
|
||||||
|
{'task': 'All links in app/metadata working', 'status': 'pending'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'category': 'Legal & Privacy (Apple)',
|
||||||
|
'items': [
|
||||||
|
{'task': 'Privacy Policy URL provided', 'status': 'pending'},
|
||||||
|
{'task': 'Terms of Service URL (if applicable)', 'status': 'pending'},
|
||||||
|
{'task': 'Data collection declarations accurate', 'status': 'pending'},
|
||||||
|
{'task': 'Third-party SDKs disclosed', 'status': 'pending'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def _generate_google_checklist(self, app_info: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""Generate Google Play Store specific checklist."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'category': 'Play Console Setup',
|
||||||
|
'items': [
|
||||||
|
{'task': 'Google Play Console account created', 'status': 'pending'},
|
||||||
|
{'task': 'Developer profile completed', 'status': 'pending'},
|
||||||
|
{'task': 'Payment merchant account linked (if paid app)', 'status': 'pending'},
|
||||||
|
{'task': 'Content rating questionnaire completed', 'status': 'pending'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'category': 'Metadata (Google)',
|
||||||
|
'items': [
|
||||||
|
{'task': 'App title (50 chars max)', 'status': 'pending'},
|
||||||
|
{'task': 'Short description (80 chars max)', 'status': 'pending'},
|
||||||
|
{'task': 'Full description (4000 chars max)', 'status': 'pending'},
|
||||||
|
{'task': 'Category selection', 'status': 'pending'},
|
||||||
|
{'task': 'Tags (up to 5)', 'status': 'pending'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'category': 'Visual Assets (Google)',
|
||||||
|
'items': [
|
||||||
|
{'task': 'App icon (512x512px)', 'status': 'pending'},
|
||||||
|
{'task': 'Feature graphic (1024x500px)', 'status': 'pending'},
|
||||||
|
{'task': 'Screenshots (2-8 required, phone)', 'status': 'pending'},
|
||||||
|
{'task': 'Screenshots (tablet, if applicable)', 'status': 'pending'},
|
||||||
|
{'task': 'Promo video (YouTube link, optional)', 'status': 'pending'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'category': 'Technical Requirements (Google)',
|
||||||
|
'items': [
|
||||||
|
{'task': 'APK/AAB uploaded to Play Console', 'status': 'pending'},
|
||||||
|
{'task': 'Internal testing completed', 'status': 'pending'},
|
||||||
|
{'task': 'App tested on required Android versions', 'status': 'pending'},
|
||||||
|
{'task': 'Target API level meets requirements', 'status': 'pending'},
|
||||||
|
{'task': 'All permissions justified', 'status': 'pending'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'category': 'Legal & Privacy (Google)',
|
||||||
|
'items': [
|
||||||
|
{'task': 'Privacy Policy URL provided', 'status': 'pending'},
|
||||||
|
{'task': 'Data safety section completed', 'status': 'pending'},
|
||||||
|
{'task': 'Ads disclosure (if applicable)', 'status': 'pending'},
|
||||||
|
{'task': 'In-app purchase disclosure (if applicable)', 'status': 'pending'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def _generate_universal_checklist(self, app_info: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""Generate universal (both platforms) checklist."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'category': 'Pre-Launch Marketing',
|
||||||
|
'items': [
|
||||||
|
{'task': 'Landing page created', 'status': 'pending'},
|
||||||
|
{'task': 'Social media accounts setup', 'status': 'pending'},
|
||||||
|
{'task': 'Press kit prepared', 'status': 'pending'},
|
||||||
|
{'task': 'Beta tester feedback collected', 'status': 'pending'},
|
||||||
|
{'task': 'Launch announcement drafted', 'status': 'pending'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'category': 'ASO Preparation',
|
||||||
|
'items': [
|
||||||
|
{'task': 'Keyword research completed', 'status': 'pending'},
|
||||||
|
{'task': 'Competitor analysis done', 'status': 'pending'},
|
||||||
|
{'task': 'A/B test plan created for post-launch', 'status': 'pending'},
|
||||||
|
{'task': 'Analytics tracking configured', 'status': 'pending'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'category': 'Quality Assurance',
|
||||||
|
'items': [
|
||||||
|
{'task': 'All core features tested', 'status': 'pending'},
|
||||||
|
{'task': 'User flows validated', 'status': 'pending'},
|
||||||
|
{'task': 'Performance testing completed', 'status': 'pending'},
|
||||||
|
{'task': 'Accessibility features tested', 'status': 'pending'},
|
||||||
|
{'task': 'Security audit completed', 'status': 'pending'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'category': 'Support Infrastructure',
|
||||||
|
'items': [
|
||||||
|
{'task': 'Support email/system setup', 'status': 'pending'},
|
||||||
|
{'task': 'FAQ page created', 'status': 'pending'},
|
||||||
|
{'task': 'Documentation for users prepared', 'status': 'pending'},
|
||||||
|
{'task': 'Team trained on handling reviews', 'status': 'pending'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def _generate_launch_timeline(self, launch_date: str) -> List[Dict[str, Any]]:
|
||||||
|
"""Generate timeline with milestones leading to launch."""
|
||||||
|
launch_dt = datetime.strptime(launch_date, '%Y-%m-%d')
|
||||||
|
|
||||||
|
milestones = [
|
||||||
|
{
|
||||||
|
'date': (launch_dt - timedelta(days=90)).strftime('%Y-%m-%d'),
|
||||||
|
'milestone': '90 days before: Complete keyword research and competitor analysis'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'date': (launch_dt - timedelta(days=60)).strftime('%Y-%m-%d'),
|
||||||
|
'milestone': '60 days before: Finalize metadata and visual assets'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'date': (launch_dt - timedelta(days=45)).strftime('%Y-%m-%d'),
|
||||||
|
'milestone': '45 days before: Begin beta testing program'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'date': (launch_dt - timedelta(days=30)).strftime('%Y-%m-%d'),
|
||||||
|
'milestone': '30 days before: Submit app for review (Apple typically takes 1-2 days, Google instant)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'date': (launch_dt - timedelta(days=14)).strftime('%Y-%m-%d'),
|
||||||
|
'milestone': '14 days before: Prepare launch marketing materials'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'date': (launch_dt - timedelta(days=7)).strftime('%Y-%m-%d'),
|
||||||
|
'milestone': '7 days before: Set up analytics and monitoring'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'date': launch_dt.strftime('%Y-%m-%d'),
|
||||||
|
'milestone': 'Launch Day: Release app and execute marketing plan'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'date': (launch_dt + timedelta(days=7)).strftime('%Y-%m-%d'),
|
||||||
|
'milestone': '7 days after: Monitor metrics, respond to reviews, address critical issues'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'date': (launch_dt + timedelta(days=30)).strftime('%Y-%m-%d'),
|
||||||
|
'milestone': '30 days after: Analyze launch metrics, plan first update'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return milestones
|
||||||
|
|
||||||
|
def _calculate_checklist_summary(self, checklists: Dict[str, List[Dict[str, Any]]]) -> Dict[str, Any]:
|
||||||
|
"""Calculate completion summary."""
|
||||||
|
total_items = 0
|
||||||
|
completed_items = 0
|
||||||
|
|
||||||
|
for platform, categories in checklists.items():
|
||||||
|
for category in categories:
|
||||||
|
for item in category['items']:
|
||||||
|
total_items += 1
|
||||||
|
if item['status'] == 'completed':
|
||||||
|
completed_items += 1
|
||||||
|
|
||||||
|
completion_percentage = (completed_items / total_items * 100) if total_items > 0 else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total_items': total_items,
|
||||||
|
'completed_items': completed_items,
|
||||||
|
'pending_items': total_items - completed_items,
|
||||||
|
'completion_percentage': round(completion_percentage, 1),
|
||||||
|
'is_ready_to_launch': completion_percentage == 100
|
||||||
|
}
|
||||||
|
|
||||||
|
def _validate_apple_compliance(
|
||||||
|
self,
|
||||||
|
app_data: Dict[str, Any],
|
||||||
|
validation_results: Dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Validate Apple App Store compliance."""
|
||||||
|
# Check for required fields
|
||||||
|
if not app_data.get('privacy_policy_url'):
|
||||||
|
validation_results['errors'].append("Privacy Policy URL is required")
|
||||||
|
|
||||||
|
if not app_data.get('app_icon'):
|
||||||
|
validation_results['errors'].append("App icon (1024x1024px) is required")
|
||||||
|
|
||||||
|
# Check metadata character limits
|
||||||
|
title = app_data.get('title', '')
|
||||||
|
if len(title) > 30:
|
||||||
|
validation_results['errors'].append(f"Title exceeds 30 characters ({len(title)})")
|
||||||
|
|
||||||
|
# Warnings for best practices
|
||||||
|
subtitle = app_data.get('subtitle', '')
|
||||||
|
if not subtitle:
|
||||||
|
validation_results['warnings'].append("Subtitle is empty - consider adding for better discoverability")
|
||||||
|
|
||||||
|
keywords = app_data.get('keywords', '')
|
||||||
|
if len(keywords) < 80:
|
||||||
|
validation_results['warnings'].append(
|
||||||
|
f"Keywords field underutilized ({len(keywords)}/100 chars) - add more keywords"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _validate_google_compliance(
|
||||||
|
self,
|
||||||
|
app_data: Dict[str, Any],
|
||||||
|
validation_results: Dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Validate Google Play Store compliance."""
|
||||||
|
# Check for required fields
|
||||||
|
if not app_data.get('privacy_policy_url'):
|
||||||
|
validation_results['errors'].append("Privacy Policy URL is required")
|
||||||
|
|
||||||
|
if not app_data.get('feature_graphic'):
|
||||||
|
validation_results['errors'].append("Feature graphic (1024x500px) is required")
|
||||||
|
|
||||||
|
# Check metadata character limits
|
||||||
|
title = app_data.get('title', '')
|
||||||
|
if len(title) > 50:
|
||||||
|
validation_results['errors'].append(f"Title exceeds 50 characters ({len(title)})")
|
||||||
|
|
||||||
|
short_desc = app_data.get('short_description', '')
|
||||||
|
if len(short_desc) > 80:
|
||||||
|
validation_results['errors'].append(f"Short description exceeds 80 characters ({len(short_desc)})")
|
||||||
|
|
||||||
|
# Warnings
|
||||||
|
if not short_desc:
|
||||||
|
validation_results['warnings'].append("Short description is empty")
|
||||||
|
|
||||||
|
def _calculate_next_versions(
|
||||||
|
self,
|
||||||
|
current_version: str,
|
||||||
|
update_frequency: str,
|
||||||
|
feature_count: int
|
||||||
|
) -> List[str]:
|
||||||
|
"""Calculate next version numbers."""
|
||||||
|
# Parse current version (assume semantic versioning)
|
||||||
|
parts = current_version.split('.')
|
||||||
|
major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2] if len(parts) > 2 else 0)
|
||||||
|
|
||||||
|
versions = []
|
||||||
|
for i in range(feature_count):
|
||||||
|
if update_frequency == 'weekly':
|
||||||
|
patch += 1
|
||||||
|
elif update_frequency == 'biweekly':
|
||||||
|
patch += 1
|
||||||
|
elif update_frequency == 'monthly':
|
||||||
|
minor += 1
|
||||||
|
patch = 0
|
||||||
|
else: # quarterly
|
||||||
|
minor += 1
|
||||||
|
patch = 0
|
||||||
|
|
||||||
|
versions.append(f"{major}.{minor}.{patch}")
|
||||||
|
|
||||||
|
return versions
|
||||||
|
|
||||||
|
def _distribute_features(
|
||||||
|
self,
|
||||||
|
features: List[str],
|
||||||
|
versions: List[str]
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Distribute features across versions."""
|
||||||
|
features_per_version = max(1, len(features) // len(versions))
|
||||||
|
|
||||||
|
schedule = []
|
||||||
|
for i, version in enumerate(versions):
|
||||||
|
start_idx = i * features_per_version
|
||||||
|
end_idx = start_idx + features_per_version if i < len(versions) - 1 else len(features)
|
||||||
|
|
||||||
|
schedule.append({
|
||||||
|
'version': version,
|
||||||
|
'features': features[start_idx:end_idx],
|
||||||
|
'release_priority': 'high' if i == 0 else ('medium' if i < len(versions) // 2 else 'low')
|
||||||
|
})
|
||||||
|
|
||||||
|
return schedule
|
||||||
|
|
||||||
|
def _generate_whats_new_template(self, version_data: Dict[str, Any]) -> Dict[str, str]:
|
||||||
|
"""Generate What's New template for version."""
|
||||||
|
features_list = '\n'.join([f"• {feature}" for feature in version_data['features']])
|
||||||
|
|
||||||
|
template = f"""Version {version_data['version']}
|
||||||
|
|
||||||
|
{features_list}
|
||||||
|
|
||||||
|
We're constantly improving your experience. Thanks for using [App Name]!
|
||||||
|
|
||||||
|
Have feedback? Contact us at support@[company].com"""
|
||||||
|
|
||||||
|
return {
|
||||||
|
'version': version_data['version'],
|
||||||
|
'template': template
|
||||||
|
}
|
||||||
|
|
||||||
|
def _generate_update_recommendations(self, update_frequency: str) -> List[str]:
|
||||||
|
"""Generate recommendations for update strategy."""
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
if update_frequency == 'weekly':
|
||||||
|
recommendations.append("Weekly updates show active development but ensure quality doesn't suffer")
|
||||||
|
elif update_frequency == 'monthly':
|
||||||
|
recommendations.append("Monthly updates are optimal for most apps - balance features and stability")
|
||||||
|
|
||||||
|
recommendations.extend([
|
||||||
|
"Include bug fixes in every update",
|
||||||
|
"Update 'What's New' section with each release",
|
||||||
|
"Respond to reviews mentioning fixed issues"
|
||||||
|
])
|
||||||
|
|
||||||
|
return recommendations
|
||||||
|
|
||||||
|
def _recommend_day_of_week(self, app_category: str) -> Dict[str, Any]:
|
||||||
|
"""Recommend best day of week to launch."""
|
||||||
|
# General recommendations based on category
|
||||||
|
if app_category.lower() in ['games', 'entertainment']:
|
||||||
|
return {
|
||||||
|
'recommended_day': 'Thursday',
|
||||||
|
'rationale': 'People download entertainment apps before weekend'
|
||||||
|
}
|
||||||
|
elif app_category.lower() in ['productivity', 'business']:
|
||||||
|
return {
|
||||||
|
'recommended_day': 'Tuesday',
|
||||||
|
'rationale': 'Business users most active mid-week'
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'recommended_day': 'Wednesday',
|
||||||
|
'rationale': 'Mid-week provides good balance and review potential'
|
||||||
|
}
|
||||||
|
|
||||||
|
def _recommend_seasonal_timing(self, app_category: str, current_date: str) -> Dict[str, Any]:
|
||||||
|
"""Recommend seasonal timing considerations."""
|
||||||
|
current_dt = datetime.strptime(current_date, '%Y-%m-%d')
|
||||||
|
month = current_dt.month
|
||||||
|
|
||||||
|
# Avoid certain periods
|
||||||
|
avoid_periods = []
|
||||||
|
if month == 12:
|
||||||
|
avoid_periods.append("Late December - low user engagement during holidays")
|
||||||
|
if month in [7, 8]:
|
||||||
|
avoid_periods.append("Summer months - some categories see lower engagement")
|
||||||
|
|
||||||
|
# Recommend periods
|
||||||
|
good_periods = []
|
||||||
|
if month in [1, 9]:
|
||||||
|
good_periods.append("New Year/Back-to-school - high user engagement")
|
||||||
|
if month in [10, 11]:
|
||||||
|
good_periods.append("Pre-holiday season - good for shopping/gift apps")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'current_month': month,
|
||||||
|
'avoid_periods': avoid_periods,
|
||||||
|
'good_periods': good_periods
|
||||||
|
}
|
||||||
|
|
||||||
|
def _analyze_competitive_timing(self, app_category: str) -> Dict[str, str]:
|
||||||
|
"""Analyze competitive timing considerations."""
|
||||||
|
return {
|
||||||
|
'recommendation': 'Research competitor launch schedules in your category',
|
||||||
|
'strategy': 'Avoid launching same week as major competitor updates'
|
||||||
|
}
|
||||||
|
|
||||||
|
def _calculate_optimal_dates(
|
||||||
|
self,
|
||||||
|
current_date: str,
|
||||||
|
day_rec: Dict[str, Any],
|
||||||
|
seasonal_rec: Dict[str, Any]
|
||||||
|
) -> List[str]:
|
||||||
|
"""Calculate optimal launch dates."""
|
||||||
|
current_dt = datetime.strptime(current_date, '%Y-%m-%d')
|
||||||
|
|
||||||
|
# Find next occurrence of recommended day
|
||||||
|
target_day = day_rec['recommended_day']
|
||||||
|
days_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4}
|
||||||
|
target_day_num = days_map.get(target_day, 2)
|
||||||
|
|
||||||
|
days_ahead = (target_day_num - current_dt.weekday()) % 7
|
||||||
|
if days_ahead == 0:
|
||||||
|
days_ahead = 7
|
||||||
|
|
||||||
|
next_target_date = current_dt + timedelta(days=days_ahead)
|
||||||
|
|
||||||
|
optimal_dates = [
|
||||||
|
next_target_date.strftime('%Y-%m-%d'),
|
||||||
|
(next_target_date + timedelta(days=7)).strftime('%Y-%m-%d'),
|
||||||
|
(next_target_date + timedelta(days=14)).strftime('%Y-%m-%d')
|
||||||
|
]
|
||||||
|
|
||||||
|
return optimal_dates
|
||||||
|
|
||||||
|
def _generate_timing_recommendation(
|
||||||
|
self,
|
||||||
|
optimal_dates: List[str],
|
||||||
|
seasonal_rec: Dict[str, Any]
|
||||||
|
) -> str:
|
||||||
|
"""Generate final timing recommendation."""
|
||||||
|
if seasonal_rec['avoid_periods']:
|
||||||
|
return f"Consider launching in {optimal_dates[1]} to avoid {seasonal_rec['avoid_periods'][0]}"
|
||||||
|
elif seasonal_rec['good_periods']:
|
||||||
|
return f"Launch on {optimal_dates[0]} to capitalize on {seasonal_rec['good_periods'][0]}"
|
||||||
|
else:
|
||||||
|
return f"Recommended launch date: {optimal_dates[0]}"
|
||||||
|
|
||||||
|
def _identify_seasonal_opportunities(
|
||||||
|
self,
|
||||||
|
app_category: str,
|
||||||
|
current_month: int
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Identify seasonal opportunities for category."""
|
||||||
|
opportunities = []
|
||||||
|
|
||||||
|
# Universal opportunities
|
||||||
|
if current_month == 1:
|
||||||
|
opportunities.append({
|
||||||
|
'event': 'New Year Resolutions',
|
||||||
|
'dates': 'January 1-31',
|
||||||
|
'relevance': 'high' if app_category.lower() in ['health', 'fitness', 'productivity'] else 'medium'
|
||||||
|
})
|
||||||
|
|
||||||
|
if current_month in [11, 12]:
|
||||||
|
opportunities.append({
|
||||||
|
'event': 'Holiday Shopping Season',
|
||||||
|
'dates': 'November-December',
|
||||||
|
'relevance': 'high' if app_category.lower() in ['shopping', 'gifts'] else 'low'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Category-specific
|
||||||
|
if app_category.lower() == 'education' and current_month in [8, 9]:
|
||||||
|
opportunities.append({
|
||||||
|
'event': 'Back to School',
|
||||||
|
'dates': 'August-September',
|
||||||
|
'relevance': 'high'
|
||||||
|
})
|
||||||
|
|
||||||
|
return opportunities
|
||||||
|
|
||||||
|
def _generate_seasonal_campaign(self, opportunity: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Generate campaign idea for seasonal opportunity."""
|
||||||
|
return {
|
||||||
|
'event': opportunity['event'],
|
||||||
|
'campaign_idea': f"Create themed visuals and messaging for {opportunity['event']}",
|
||||||
|
'metadata_updates': 'Update app description and screenshots with seasonal themes',
|
||||||
|
'promotion_strategy': 'Consider limited-time features or discounts'
|
||||||
|
}
|
||||||
|
|
||||||
|
def _create_seasonal_timeline(self, campaigns: List[Dict[str, Any]]) -> List[str]:
|
||||||
|
"""Create implementation timeline for campaigns."""
|
||||||
|
return [
|
||||||
|
f"30 days before: Plan {campaign['event']} campaign strategy"
|
||||||
|
for campaign in campaigns
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def generate_launch_checklist(
|
||||||
|
platform: str,
|
||||||
|
app_info: Dict[str, Any],
|
||||||
|
launch_date: Optional[str] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Convenience function to generate launch checklist.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
platform: Platform ('apple', 'google', or 'both')
|
||||||
|
app_info: App information
|
||||||
|
launch_date: Target launch date
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complete launch checklist
|
||||||
|
"""
|
||||||
|
generator = LaunchChecklistGenerator(platform)
|
||||||
|
return generator.generate_prelaunch_checklist(app_info, launch_date)
|
||||||
+588
@@ -0,0 +1,588 @@
|
|||||||
|
"""
|
||||||
|
Localization helper module for App Store Optimization.
|
||||||
|
Manages multi-language ASO optimization strategies.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, List, Any, Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class LocalizationHelper:
|
||||||
|
"""Helps manage multi-language ASO optimization."""
|
||||||
|
|
||||||
|
# Priority markets by language (based on app store revenue and user base)
|
||||||
|
PRIORITY_MARKETS = {
|
||||||
|
'tier_1': [
|
||||||
|
{'language': 'en-US', 'market': 'United States', 'revenue_share': 0.25},
|
||||||
|
{'language': 'zh-CN', 'market': 'China', 'revenue_share': 0.20},
|
||||||
|
{'language': 'ja-JP', 'market': 'Japan', 'revenue_share': 0.10},
|
||||||
|
{'language': 'de-DE', 'market': 'Germany', 'revenue_share': 0.08},
|
||||||
|
{'language': 'en-GB', 'market': 'United Kingdom', 'revenue_share': 0.06}
|
||||||
|
],
|
||||||
|
'tier_2': [
|
||||||
|
{'language': 'fr-FR', 'market': 'France', 'revenue_share': 0.05},
|
||||||
|
{'language': 'ko-KR', 'market': 'South Korea', 'revenue_share': 0.05},
|
||||||
|
{'language': 'es-ES', 'market': 'Spain', 'revenue_share': 0.03},
|
||||||
|
{'language': 'it-IT', 'market': 'Italy', 'revenue_share': 0.03},
|
||||||
|
{'language': 'pt-BR', 'market': 'Brazil', 'revenue_share': 0.03}
|
||||||
|
],
|
||||||
|
'tier_3': [
|
||||||
|
{'language': 'ru-RU', 'market': 'Russia', 'revenue_share': 0.02},
|
||||||
|
{'language': 'es-MX', 'market': 'Mexico', 'revenue_share': 0.02},
|
||||||
|
{'language': 'nl-NL', 'market': 'Netherlands', 'revenue_share': 0.02},
|
||||||
|
{'language': 'sv-SE', 'market': 'Sweden', 'revenue_share': 0.01},
|
||||||
|
{'language': 'pl-PL', 'market': 'Poland', 'revenue_share': 0.01}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Character limit multipliers by language (some languages need more/less space)
|
||||||
|
CHAR_MULTIPLIERS = {
|
||||||
|
'en': 1.0,
|
||||||
|
'zh': 0.6, # Chinese characters are more compact
|
||||||
|
'ja': 0.7, # Japanese uses kanji
|
||||||
|
'ko': 0.8, # Korean is relatively compact
|
||||||
|
'de': 1.3, # German words are typically longer
|
||||||
|
'fr': 1.2, # French tends to be longer
|
||||||
|
'es': 1.1, # Spanish slightly longer
|
||||||
|
'pt': 1.1, # Portuguese similar to Spanish
|
||||||
|
'ru': 1.1, # Russian similar length
|
||||||
|
'ar': 1.0, # Arabic varies
|
||||||
|
'it': 1.1 # Italian similar to Spanish
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, app_category: str = 'general'):
|
||||||
|
"""
|
||||||
|
Initialize localization helper.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_category: App category to prioritize relevant markets
|
||||||
|
"""
|
||||||
|
self.app_category = app_category
|
||||||
|
self.localization_plans = []
|
||||||
|
|
||||||
|
def identify_target_markets(
|
||||||
|
self,
|
||||||
|
current_market: str = 'en-US',
|
||||||
|
budget_level: str = 'medium',
|
||||||
|
target_market_count: int = 5
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Recommend priority markets for localization.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_market: Current/primary market
|
||||||
|
budget_level: 'low', 'medium', or 'high'
|
||||||
|
target_market_count: Number of markets to target
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Prioritized market recommendations
|
||||||
|
"""
|
||||||
|
# Determine tier priorities based on budget
|
||||||
|
if budget_level == 'low':
|
||||||
|
priority_tiers = ['tier_1']
|
||||||
|
max_markets = min(target_market_count, 3)
|
||||||
|
elif budget_level == 'medium':
|
||||||
|
priority_tiers = ['tier_1', 'tier_2']
|
||||||
|
max_markets = min(target_market_count, 8)
|
||||||
|
else: # high budget
|
||||||
|
priority_tiers = ['tier_1', 'tier_2', 'tier_3']
|
||||||
|
max_markets = target_market_count
|
||||||
|
|
||||||
|
# Collect markets from priority tiers
|
||||||
|
recommended_markets = []
|
||||||
|
for tier in priority_tiers:
|
||||||
|
for market in self.PRIORITY_MARKETS[tier]:
|
||||||
|
if market['language'] != current_market:
|
||||||
|
recommended_markets.append({
|
||||||
|
**market,
|
||||||
|
'tier': tier,
|
||||||
|
'estimated_translation_cost': self._estimate_translation_cost(
|
||||||
|
market['language']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort by revenue share and limit
|
||||||
|
recommended_markets.sort(key=lambda x: x['revenue_share'], reverse=True)
|
||||||
|
recommended_markets = recommended_markets[:max_markets]
|
||||||
|
|
||||||
|
# Calculate potential ROI
|
||||||
|
total_potential_revenue_share = sum(m['revenue_share'] for m in recommended_markets)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'recommended_markets': recommended_markets,
|
||||||
|
'total_markets': len(recommended_markets),
|
||||||
|
'estimated_total_revenue_lift': f"{total_potential_revenue_share*100:.1f}%",
|
||||||
|
'estimated_cost': self._estimate_total_localization_cost(recommended_markets),
|
||||||
|
'implementation_priority': self._prioritize_implementation(recommended_markets)
|
||||||
|
}
|
||||||
|
|
||||||
|
def translate_metadata(
|
||||||
|
self,
|
||||||
|
source_metadata: Dict[str, str],
|
||||||
|
source_language: str,
|
||||||
|
target_language: str,
|
||||||
|
platform: str = 'apple'
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Generate localized metadata with character limit considerations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_metadata: Original metadata (title, description, etc.)
|
||||||
|
source_language: Source language code (e.g., 'en')
|
||||||
|
target_language: Target language code (e.g., 'es')
|
||||||
|
platform: 'apple' or 'google'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Localized metadata with character limit validation
|
||||||
|
"""
|
||||||
|
# Get character multiplier
|
||||||
|
target_lang_code = target_language.split('-')[0]
|
||||||
|
char_multiplier = self.CHAR_MULTIPLIERS.get(target_lang_code, 1.0)
|
||||||
|
|
||||||
|
# Platform-specific limits
|
||||||
|
if platform == 'apple':
|
||||||
|
limits = {'title': 30, 'subtitle': 30, 'description': 4000, 'keywords': 100}
|
||||||
|
else:
|
||||||
|
limits = {'title': 50, 'short_description': 80, 'description': 4000}
|
||||||
|
|
||||||
|
localized_metadata = {}
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
for field, text in source_metadata.items():
|
||||||
|
if field not in limits:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Estimate target length
|
||||||
|
estimated_length = int(len(text) * char_multiplier)
|
||||||
|
limit = limits[field]
|
||||||
|
|
||||||
|
localized_metadata[field] = {
|
||||||
|
'original_text': text,
|
||||||
|
'original_length': len(text),
|
||||||
|
'estimated_target_length': estimated_length,
|
||||||
|
'character_limit': limit,
|
||||||
|
'fits_within_limit': estimated_length <= limit,
|
||||||
|
'translation_notes': self._get_translation_notes(
|
||||||
|
field,
|
||||||
|
target_language,
|
||||||
|
estimated_length,
|
||||||
|
limit
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if estimated_length > limit:
|
||||||
|
warnings.append(
|
||||||
|
f"{field}: Estimated length ({estimated_length}) may exceed limit ({limit}) - "
|
||||||
|
f"condensing may be required"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'source_language': source_language,
|
||||||
|
'target_language': target_language,
|
||||||
|
'platform': platform,
|
||||||
|
'localized_fields': localized_metadata,
|
||||||
|
'character_multiplier': char_multiplier,
|
||||||
|
'warnings': warnings,
|
||||||
|
'recommendations': self._generate_translation_recommendations(
|
||||||
|
target_language,
|
||||||
|
warnings
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def adapt_keywords(
|
||||||
|
self,
|
||||||
|
source_keywords: List[str],
|
||||||
|
source_language: str,
|
||||||
|
target_language: str,
|
||||||
|
target_market: str
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Adapt keywords for target market (not just direct translation).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_keywords: Original keywords
|
||||||
|
source_language: Source language code
|
||||||
|
target_language: Target language code
|
||||||
|
target_market: Target market (e.g., 'France', 'Japan')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Adapted keyword recommendations
|
||||||
|
"""
|
||||||
|
# Cultural adaptation considerations
|
||||||
|
cultural_notes = self._get_cultural_keyword_considerations(target_market)
|
||||||
|
|
||||||
|
# Search behavior differences
|
||||||
|
search_patterns = self._get_search_patterns(target_market)
|
||||||
|
|
||||||
|
adapted_keywords = []
|
||||||
|
for keyword in source_keywords:
|
||||||
|
adapted_keywords.append({
|
||||||
|
'source_keyword': keyword,
|
||||||
|
'adaptation_strategy': self._determine_adaptation_strategy(
|
||||||
|
keyword,
|
||||||
|
target_market
|
||||||
|
),
|
||||||
|
'cultural_considerations': cultural_notes.get(keyword, []),
|
||||||
|
'priority': 'high' if keyword in source_keywords[:3] else 'medium'
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'source_language': source_language,
|
||||||
|
'target_language': target_language,
|
||||||
|
'target_market': target_market,
|
||||||
|
'adapted_keywords': adapted_keywords,
|
||||||
|
'search_behavior_notes': search_patterns,
|
||||||
|
'recommendations': [
|
||||||
|
'Use native speakers for keyword research',
|
||||||
|
'Test keywords with local users before finalizing',
|
||||||
|
'Consider local competitors\' keyword strategies',
|
||||||
|
'Monitor search trends in target market'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_translations(
|
||||||
|
self,
|
||||||
|
translated_metadata: Dict[str, str],
|
||||||
|
target_language: str,
|
||||||
|
platform: str = 'apple'
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Validate translated metadata for character limits and quality.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
translated_metadata: Translated text fields
|
||||||
|
target_language: Target language code
|
||||||
|
platform: 'apple' or 'google'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Validation report
|
||||||
|
"""
|
||||||
|
# Platform limits
|
||||||
|
if platform == 'apple':
|
||||||
|
limits = {'title': 30, 'subtitle': 30, 'description': 4000, 'keywords': 100}
|
||||||
|
else:
|
||||||
|
limits = {'title': 50, 'short_description': 80, 'description': 4000}
|
||||||
|
|
||||||
|
validation_results = {
|
||||||
|
'is_valid': True,
|
||||||
|
'field_validations': {},
|
||||||
|
'errors': [],
|
||||||
|
'warnings': []
|
||||||
|
}
|
||||||
|
|
||||||
|
for field, text in translated_metadata.items():
|
||||||
|
if field not in limits:
|
||||||
|
continue
|
||||||
|
|
||||||
|
actual_length = len(text)
|
||||||
|
limit = limits[field]
|
||||||
|
is_within_limit = actual_length <= limit
|
||||||
|
|
||||||
|
validation_results['field_validations'][field] = {
|
||||||
|
'text': text,
|
||||||
|
'length': actual_length,
|
||||||
|
'limit': limit,
|
||||||
|
'is_valid': is_within_limit,
|
||||||
|
'usage_percentage': round((actual_length / limit) * 100, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if not is_within_limit:
|
||||||
|
validation_results['is_valid'] = False
|
||||||
|
validation_results['errors'].append(
|
||||||
|
f"{field} exceeds limit: {actual_length}/{limit} characters"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Quality checks
|
||||||
|
quality_issues = self._check_translation_quality(
|
||||||
|
translated_metadata,
|
||||||
|
target_language
|
||||||
|
)
|
||||||
|
|
||||||
|
validation_results['quality_checks'] = quality_issues
|
||||||
|
|
||||||
|
if quality_issues:
|
||||||
|
validation_results['warnings'].extend(
|
||||||
|
[f"Quality issue: {issue}" for issue in quality_issues]
|
||||||
|
)
|
||||||
|
|
||||||
|
return validation_results
|
||||||
|
|
||||||
|
def calculate_localization_roi(
|
||||||
|
self,
|
||||||
|
target_markets: List[str],
|
||||||
|
current_monthly_downloads: int,
|
||||||
|
localization_cost: float,
|
||||||
|
expected_lift_percentage: float = 0.15
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Estimate ROI of localization investment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_markets: List of market codes
|
||||||
|
current_monthly_downloads: Current monthly downloads
|
||||||
|
localization_cost: Total cost to localize
|
||||||
|
expected_lift_percentage: Expected download increase (default 15%)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ROI analysis
|
||||||
|
"""
|
||||||
|
# Estimate market-specific lift
|
||||||
|
market_data = []
|
||||||
|
total_expected_lift = 0
|
||||||
|
|
||||||
|
for market_code in target_markets:
|
||||||
|
# Find market in priority lists
|
||||||
|
market_info = None
|
||||||
|
for tier_name, markets in self.PRIORITY_MARKETS.items():
|
||||||
|
for m in markets:
|
||||||
|
if m['language'] == market_code:
|
||||||
|
market_info = m
|
||||||
|
break
|
||||||
|
|
||||||
|
if not market_info:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Estimate downloads from this market
|
||||||
|
market_downloads = int(current_monthly_downloads * market_info['revenue_share'])
|
||||||
|
expected_increase = int(market_downloads * expected_lift_percentage)
|
||||||
|
total_expected_lift += expected_increase
|
||||||
|
|
||||||
|
market_data.append({
|
||||||
|
'market': market_info['market'],
|
||||||
|
'current_monthly_downloads': market_downloads,
|
||||||
|
'expected_increase': expected_increase,
|
||||||
|
'revenue_potential': market_info['revenue_share']
|
||||||
|
})
|
||||||
|
|
||||||
|
# Calculate payback period (assuming $2 revenue per download)
|
||||||
|
revenue_per_download = 2.0
|
||||||
|
monthly_additional_revenue = total_expected_lift * revenue_per_download
|
||||||
|
payback_months = (localization_cost / monthly_additional_revenue) if monthly_additional_revenue > 0 else float('inf')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'markets_analyzed': len(market_data),
|
||||||
|
'market_breakdown': market_data,
|
||||||
|
'total_expected_monthly_lift': total_expected_lift,
|
||||||
|
'expected_monthly_revenue_increase': f"${monthly_additional_revenue:,.2f}",
|
||||||
|
'localization_cost': f"${localization_cost:,.2f}",
|
||||||
|
'payback_period_months': round(payback_months, 1) if payback_months != float('inf') else 'N/A',
|
||||||
|
'annual_roi': f"{((monthly_additional_revenue * 12 - localization_cost) / localization_cost * 100):.1f}%" if payback_months != float('inf') else 'Negative',
|
||||||
|
'recommendation': self._generate_roi_recommendation(payback_months)
|
||||||
|
}
|
||||||
|
|
||||||
|
def _estimate_translation_cost(self, language: str) -> Dict[str, float]:
|
||||||
|
"""Estimate translation cost for a language."""
|
||||||
|
# Base cost per word (professional translation)
|
||||||
|
base_cost_per_word = 0.12
|
||||||
|
|
||||||
|
# Language-specific multipliers
|
||||||
|
multipliers = {
|
||||||
|
'zh-CN': 1.5, # Chinese requires specialist
|
||||||
|
'ja-JP': 1.5, # Japanese requires specialist
|
||||||
|
'ko-KR': 1.3,
|
||||||
|
'ar-SA': 1.4, # Arabic (right-to-left)
|
||||||
|
'default': 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
multiplier = multipliers.get(language, multipliers['default'])
|
||||||
|
|
||||||
|
# Typical word counts for app store metadata
|
||||||
|
typical_word_counts = {
|
||||||
|
'title': 5,
|
||||||
|
'subtitle': 5,
|
||||||
|
'description': 300,
|
||||||
|
'keywords': 20,
|
||||||
|
'screenshots': 50 # Caption text
|
||||||
|
}
|
||||||
|
|
||||||
|
total_words = sum(typical_word_counts.values())
|
||||||
|
estimated_cost = total_words * base_cost_per_word * multiplier
|
||||||
|
|
||||||
|
return {
|
||||||
|
'cost_per_word': base_cost_per_word * multiplier,
|
||||||
|
'total_words': total_words,
|
||||||
|
'estimated_cost': round(estimated_cost, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
def _estimate_total_localization_cost(self, markets: List[Dict[str, Any]]) -> str:
|
||||||
|
"""Estimate total cost for multiple markets."""
|
||||||
|
total = sum(m['estimated_translation_cost']['estimated_cost'] for m in markets)
|
||||||
|
return f"${total:,.2f}"
|
||||||
|
|
||||||
|
def _prioritize_implementation(self, markets: List[Dict[str, Any]]) -> List[Dict[str, str]]:
|
||||||
|
"""Create phased implementation plan."""
|
||||||
|
phases = []
|
||||||
|
|
||||||
|
# Phase 1: Top revenue markets
|
||||||
|
phase_1 = [m for m in markets[:3]]
|
||||||
|
if phase_1:
|
||||||
|
phases.append({
|
||||||
|
'phase': 'Phase 1 (First 30 days)',
|
||||||
|
'markets': ', '.join([m['market'] for m in phase_1]),
|
||||||
|
'rationale': 'Highest revenue potential markets'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Phase 2: Remaining tier 1 and top tier 2
|
||||||
|
phase_2 = [m for m in markets[3:6]]
|
||||||
|
if phase_2:
|
||||||
|
phases.append({
|
||||||
|
'phase': 'Phase 2 (Days 31-60)',
|
||||||
|
'markets': ', '.join([m['market'] for m in phase_2]),
|
||||||
|
'rationale': 'Strong revenue markets with good ROI'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Phase 3: Remaining markets
|
||||||
|
phase_3 = [m for m in markets[6:]]
|
||||||
|
if phase_3:
|
||||||
|
phases.append({
|
||||||
|
'phase': 'Phase 3 (Days 61-90)',
|
||||||
|
'markets': ', '.join([m['market'] for m in phase_3]),
|
||||||
|
'rationale': 'Complete global coverage'
|
||||||
|
})
|
||||||
|
|
||||||
|
return phases
|
||||||
|
|
||||||
|
def _get_translation_notes(
|
||||||
|
self,
|
||||||
|
field: str,
|
||||||
|
target_language: str,
|
||||||
|
estimated_length: int,
|
||||||
|
limit: int
|
||||||
|
) -> List[str]:
|
||||||
|
"""Get translation-specific notes for field."""
|
||||||
|
notes = []
|
||||||
|
|
||||||
|
if estimated_length > limit:
|
||||||
|
notes.append(f"Condensing required - aim for {limit - 10} characters to allow buffer")
|
||||||
|
|
||||||
|
if field == 'title' and target_language.startswith('zh'):
|
||||||
|
notes.append("Chinese characters convey more meaning - may need fewer characters")
|
||||||
|
|
||||||
|
if field == 'keywords' and target_language.startswith('de'):
|
||||||
|
notes.append("German compound words may be longer - prioritize shorter keywords")
|
||||||
|
|
||||||
|
return notes
|
||||||
|
|
||||||
|
def _generate_translation_recommendations(
|
||||||
|
self,
|
||||||
|
target_language: str,
|
||||||
|
warnings: List[str]
|
||||||
|
) -> List[str]:
|
||||||
|
"""Generate translation recommendations."""
|
||||||
|
recommendations = [
|
||||||
|
"Use professional native speakers for translation",
|
||||||
|
"Test translations with local users before finalizing"
|
||||||
|
]
|
||||||
|
|
||||||
|
if warnings:
|
||||||
|
recommendations.append("Work with translator to condense text while preserving meaning")
|
||||||
|
|
||||||
|
if target_language.startswith('zh') or target_language.startswith('ja'):
|
||||||
|
recommendations.append("Consider cultural context and local idioms")
|
||||||
|
|
||||||
|
return recommendations
|
||||||
|
|
||||||
|
def _get_cultural_keyword_considerations(self, target_market: str) -> Dict[str, List[str]]:
|
||||||
|
"""Get cultural considerations for keywords by market."""
|
||||||
|
# Simplified example - real implementation would be more comprehensive
|
||||||
|
considerations = {
|
||||||
|
'China': ['Avoid politically sensitive terms', 'Consider local alternatives to blocked services'],
|
||||||
|
'Japan': ['Honorific language important', 'Technical terms often use katakana'],
|
||||||
|
'Germany': ['Privacy and security terms resonate', 'Efficiency and quality valued'],
|
||||||
|
'France': ['French language protection laws', 'Prefer French terms over English'],
|
||||||
|
'default': ['Research local search behavior', 'Test with native speakers']
|
||||||
|
}
|
||||||
|
|
||||||
|
return considerations.get(target_market, considerations['default'])
|
||||||
|
|
||||||
|
def _get_search_patterns(self, target_market: str) -> List[str]:
|
||||||
|
"""Get search pattern notes for market."""
|
||||||
|
patterns = {
|
||||||
|
'China': ['Use both simplified characters and romanization', 'Brand names often romanized'],
|
||||||
|
'Japan': ['Mix of kanji, hiragana, and katakana', 'English words common in tech'],
|
||||||
|
'Germany': ['Compound words common', 'Specific technical terminology'],
|
||||||
|
'default': ['Research local search trends', 'Monitor competitor keywords']
|
||||||
|
}
|
||||||
|
|
||||||
|
return patterns.get(target_market, patterns['default'])
|
||||||
|
|
||||||
|
def _determine_adaptation_strategy(self, keyword: str, target_market: str) -> str:
|
||||||
|
"""Determine how to adapt keyword for market."""
|
||||||
|
# Simplified logic
|
||||||
|
if target_market in ['China', 'Japan', 'Korea']:
|
||||||
|
return 'full_localization' # Complete translation needed
|
||||||
|
elif target_market in ['Germany', 'France', 'Spain']:
|
||||||
|
return 'adapt_and_translate' # Some adaptation needed
|
||||||
|
else:
|
||||||
|
return 'direct_translation' # Direct translation usually sufficient
|
||||||
|
|
||||||
|
def _check_translation_quality(
|
||||||
|
self,
|
||||||
|
translated_metadata: Dict[str, str],
|
||||||
|
target_language: str
|
||||||
|
) -> List[str]:
|
||||||
|
"""Basic quality checks for translations."""
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
# Check for untranslated placeholders
|
||||||
|
for field, text in translated_metadata.items():
|
||||||
|
if '[' in text or '{' in text or 'TODO' in text.upper():
|
||||||
|
issues.append(f"{field} contains placeholder text")
|
||||||
|
|
||||||
|
# Check for excessive punctuation
|
||||||
|
for field, text in translated_metadata.items():
|
||||||
|
if text.count('!') > 3:
|
||||||
|
issues.append(f"{field} has excessive exclamation marks")
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
def _generate_roi_recommendation(self, payback_months: float) -> str:
|
||||||
|
"""Generate ROI recommendation."""
|
||||||
|
if payback_months <= 3:
|
||||||
|
return "Excellent ROI - proceed immediately"
|
||||||
|
elif payback_months <= 6:
|
||||||
|
return "Good ROI - recommended investment"
|
||||||
|
elif payback_months <= 12:
|
||||||
|
return "Moderate ROI - consider if strategic market"
|
||||||
|
else:
|
||||||
|
return "Low ROI - reconsider or focus on higher-priority markets first"
|
||||||
|
|
||||||
|
|
||||||
|
def plan_localization_strategy(
|
||||||
|
current_market: str,
|
||||||
|
budget_level: str,
|
||||||
|
monthly_downloads: int
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Convenience function to plan localization strategy.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_market: Current market code
|
||||||
|
budget_level: Budget level
|
||||||
|
monthly_downloads: Current monthly downloads
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complete localization plan
|
||||||
|
"""
|
||||||
|
helper = LocalizationHelper()
|
||||||
|
|
||||||
|
target_markets = helper.identify_target_markets(
|
||||||
|
current_market=current_market,
|
||||||
|
budget_level=budget_level
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract market codes
|
||||||
|
market_codes = [m['language'] for m in target_markets['recommended_markets']]
|
||||||
|
|
||||||
|
# Calculate ROI
|
||||||
|
estimated_cost = float(target_markets['estimated_cost'].replace('$', '').replace(',', ''))
|
||||||
|
|
||||||
|
roi_analysis = helper.calculate_localization_roi(
|
||||||
|
market_codes,
|
||||||
|
monthly_downloads,
|
||||||
|
estimated_cost
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'target_markets': target_markets,
|
||||||
|
'roi_analysis': roi_analysis
|
||||||
|
}
|
||||||
@@ -0,0 +1,581 @@
|
|||||||
|
"""
|
||||||
|
Metadata optimization module for App Store Optimization.
|
||||||
|
Optimizes titles, descriptions, and keyword fields with platform-specific character limit validation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, List, Any, Optional, Tuple
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataOptimizer:
|
||||||
|
"""Optimizes app store metadata for maximum discoverability and conversion."""
|
||||||
|
|
||||||
|
# Platform-specific character limits
|
||||||
|
CHAR_LIMITS = {
|
||||||
|
'apple': {
|
||||||
|
'title': 30,
|
||||||
|
'subtitle': 30,
|
||||||
|
'promotional_text': 170,
|
||||||
|
'description': 4000,
|
||||||
|
'keywords': 100,
|
||||||
|
'whats_new': 4000
|
||||||
|
},
|
||||||
|
'google': {
|
||||||
|
'title': 50,
|
||||||
|
'short_description': 80,
|
||||||
|
'full_description': 4000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, platform: str = 'apple'):
|
||||||
|
"""
|
||||||
|
Initialize metadata optimizer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
platform: 'apple' or 'google'
|
||||||
|
"""
|
||||||
|
if platform not in ['apple', 'google']:
|
||||||
|
raise ValueError("Platform must be 'apple' or 'google'")
|
||||||
|
|
||||||
|
self.platform = platform
|
||||||
|
self.limits = self.CHAR_LIMITS[platform]
|
||||||
|
|
||||||
|
def optimize_title(
|
||||||
|
self,
|
||||||
|
app_name: str,
|
||||||
|
target_keywords: List[str],
|
||||||
|
include_brand: bool = True
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Optimize app title with keyword integration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_name: Your app's brand name
|
||||||
|
target_keywords: List of keywords to potentially include
|
||||||
|
include_brand: Whether to include brand name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optimized title options with analysis
|
||||||
|
"""
|
||||||
|
max_length = self.limits['title']
|
||||||
|
|
||||||
|
title_options = []
|
||||||
|
|
||||||
|
# Option 1: Brand name only
|
||||||
|
if include_brand:
|
||||||
|
option1 = app_name[:max_length]
|
||||||
|
title_options.append({
|
||||||
|
'title': option1,
|
||||||
|
'length': len(option1),
|
||||||
|
'remaining_chars': max_length - len(option1),
|
||||||
|
'keywords_included': [],
|
||||||
|
'strategy': 'brand_only',
|
||||||
|
'pros': ['Maximum brand recognition', 'Clean and simple'],
|
||||||
|
'cons': ['No keyword targeting', 'Lower discoverability']
|
||||||
|
})
|
||||||
|
|
||||||
|
# Option 2: Brand + Primary Keyword
|
||||||
|
if target_keywords:
|
||||||
|
primary_keyword = target_keywords[0]
|
||||||
|
option2 = self._build_title_with_keywords(
|
||||||
|
app_name,
|
||||||
|
[primary_keyword],
|
||||||
|
max_length
|
||||||
|
)
|
||||||
|
if option2:
|
||||||
|
title_options.append({
|
||||||
|
'title': option2,
|
||||||
|
'length': len(option2),
|
||||||
|
'remaining_chars': max_length - len(option2),
|
||||||
|
'keywords_included': [primary_keyword],
|
||||||
|
'strategy': 'brand_plus_primary',
|
||||||
|
'pros': ['Targets main keyword', 'Maintains brand identity'],
|
||||||
|
'cons': ['Limited keyword coverage']
|
||||||
|
})
|
||||||
|
|
||||||
|
# Option 3: Brand + Multiple Keywords (if space allows)
|
||||||
|
if len(target_keywords) > 1:
|
||||||
|
option3 = self._build_title_with_keywords(
|
||||||
|
app_name,
|
||||||
|
target_keywords[:2],
|
||||||
|
max_length
|
||||||
|
)
|
||||||
|
if option3:
|
||||||
|
title_options.append({
|
||||||
|
'title': option3,
|
||||||
|
'length': len(option3),
|
||||||
|
'remaining_chars': max_length - len(option3),
|
||||||
|
'keywords_included': target_keywords[:2],
|
||||||
|
'strategy': 'brand_plus_multiple',
|
||||||
|
'pros': ['Multiple keyword targets', 'Better discoverability'],
|
||||||
|
'cons': ['May feel cluttered', 'Less brand focus']
|
||||||
|
})
|
||||||
|
|
||||||
|
# Option 4: Keyword-first approach (for new apps)
|
||||||
|
if target_keywords and not include_brand:
|
||||||
|
option4 = " ".join(target_keywords[:2])[:max_length]
|
||||||
|
title_options.append({
|
||||||
|
'title': option4,
|
||||||
|
'length': len(option4),
|
||||||
|
'remaining_chars': max_length - len(option4),
|
||||||
|
'keywords_included': target_keywords[:2],
|
||||||
|
'strategy': 'keyword_first',
|
||||||
|
'pros': ['Maximum SEO benefit', 'Clear functionality'],
|
||||||
|
'cons': ['No brand recognition', 'Generic appearance']
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'platform': self.platform,
|
||||||
|
'max_length': max_length,
|
||||||
|
'options': title_options,
|
||||||
|
'recommendation': self._recommend_title_option(title_options)
|
||||||
|
}
|
||||||
|
|
||||||
|
def optimize_description(
|
||||||
|
self,
|
||||||
|
app_info: Dict[str, Any],
|
||||||
|
target_keywords: List[str],
|
||||||
|
description_type: str = 'full'
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Optimize app description with keyword integration and conversion focus.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_info: Dict with 'name', 'key_features', 'unique_value', 'target_audience'
|
||||||
|
target_keywords: List of keywords to integrate naturally
|
||||||
|
description_type: 'full', 'short' (Google), 'subtitle' (Apple)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optimized description with analysis
|
||||||
|
"""
|
||||||
|
if description_type == 'short' and self.platform == 'google':
|
||||||
|
return self._optimize_short_description(app_info, target_keywords)
|
||||||
|
elif description_type == 'subtitle' and self.platform == 'apple':
|
||||||
|
return self._optimize_subtitle(app_info, target_keywords)
|
||||||
|
else:
|
||||||
|
return self._optimize_full_description(app_info, target_keywords)
|
||||||
|
|
||||||
|
def optimize_keyword_field(
|
||||||
|
self,
|
||||||
|
target_keywords: List[str],
|
||||||
|
app_title: str = "",
|
||||||
|
app_description: str = ""
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Optimize Apple's 100-character keyword field.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- No spaces between commas
|
||||||
|
- No plural forms if singular exists
|
||||||
|
- No duplicates
|
||||||
|
- Keywords in title/subtitle are already indexed
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_keywords: List of target keywords
|
||||||
|
app_title: Current app title (to avoid duplication)
|
||||||
|
app_description: Current description (to check coverage)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optimized keyword field (comma-separated, no spaces)
|
||||||
|
"""
|
||||||
|
if self.platform != 'apple':
|
||||||
|
return {'error': 'Keyword field optimization only applies to Apple App Store'}
|
||||||
|
|
||||||
|
max_length = self.limits['keywords']
|
||||||
|
|
||||||
|
# Extract words already in title (these don't need to be in keyword field)
|
||||||
|
title_words = set(app_title.lower().split()) if app_title else set()
|
||||||
|
|
||||||
|
# Process keywords
|
||||||
|
processed_keywords = []
|
||||||
|
for keyword in target_keywords:
|
||||||
|
keyword_lower = keyword.lower().strip()
|
||||||
|
|
||||||
|
# Skip if already in title
|
||||||
|
if keyword_lower in title_words:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Remove duplicates and process
|
||||||
|
words = keyword_lower.split()
|
||||||
|
for word in words:
|
||||||
|
if word not in processed_keywords and word not in title_words:
|
||||||
|
processed_keywords.append(word)
|
||||||
|
|
||||||
|
# Remove plurals if singular exists
|
||||||
|
deduplicated = self._remove_plural_duplicates(processed_keywords)
|
||||||
|
|
||||||
|
# Build keyword field within 100 character limit
|
||||||
|
keyword_field = self._build_keyword_field(deduplicated, max_length)
|
||||||
|
|
||||||
|
# Calculate keyword density in description
|
||||||
|
density = self._calculate_coverage(target_keywords, app_description)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'keyword_field': keyword_field,
|
||||||
|
'length': len(keyword_field),
|
||||||
|
'remaining_chars': max_length - len(keyword_field),
|
||||||
|
'keywords_included': keyword_field.split(','),
|
||||||
|
'keywords_count': len(keyword_field.split(',')),
|
||||||
|
'keywords_excluded': [kw for kw in target_keywords if kw.lower() not in keyword_field],
|
||||||
|
'description_coverage': density,
|
||||||
|
'optimization_tips': [
|
||||||
|
'Keywords in title are auto-indexed - no need to repeat',
|
||||||
|
'Use singular forms only (Apple indexes plurals automatically)',
|
||||||
|
'No spaces between commas to maximize character usage',
|
||||||
|
'Update keyword field with each app update to test variations'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_character_limits(
|
||||||
|
self,
|
||||||
|
metadata: Dict[str, str]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Validate all metadata fields against platform character limits.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
metadata: Dictionary of field_name: value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Validation report with errors and warnings
|
||||||
|
"""
|
||||||
|
validation_results = {
|
||||||
|
'is_valid': True,
|
||||||
|
'errors': [],
|
||||||
|
'warnings': [],
|
||||||
|
'field_status': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
for field_name, value in metadata.items():
|
||||||
|
if field_name not in self.limits:
|
||||||
|
validation_results['warnings'].append(
|
||||||
|
f"Unknown field '{field_name}' for {self.platform} platform"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
max_length = self.limits[field_name]
|
||||||
|
actual_length = len(value)
|
||||||
|
remaining = max_length - actual_length
|
||||||
|
|
||||||
|
field_status = {
|
||||||
|
'value': value,
|
||||||
|
'length': actual_length,
|
||||||
|
'limit': max_length,
|
||||||
|
'remaining': remaining,
|
||||||
|
'is_valid': actual_length <= max_length,
|
||||||
|
'usage_percentage': round((actual_length / max_length) * 100, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
validation_results['field_status'][field_name] = field_status
|
||||||
|
|
||||||
|
if actual_length > max_length:
|
||||||
|
validation_results['is_valid'] = False
|
||||||
|
validation_results['errors'].append(
|
||||||
|
f"'{field_name}' exceeds limit: {actual_length}/{max_length} chars"
|
||||||
|
)
|
||||||
|
elif remaining > max_length * 0.2: # More than 20% unused
|
||||||
|
validation_results['warnings'].append(
|
||||||
|
f"'{field_name}' under-utilizes space: {remaining} chars remaining"
|
||||||
|
)
|
||||||
|
|
||||||
|
return validation_results
|
||||||
|
|
||||||
|
def calculate_keyword_density(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
target_keywords: List[str]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Calculate keyword density in text.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Text to analyze
|
||||||
|
target_keywords: Keywords to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Density analysis
|
||||||
|
"""
|
||||||
|
text_lower = text.lower()
|
||||||
|
total_words = len(text_lower.split())
|
||||||
|
|
||||||
|
keyword_densities = {}
|
||||||
|
for keyword in target_keywords:
|
||||||
|
keyword_lower = keyword.lower()
|
||||||
|
count = text_lower.count(keyword_lower)
|
||||||
|
density = (count / total_words * 100) if total_words > 0 else 0
|
||||||
|
|
||||||
|
keyword_densities[keyword] = {
|
||||||
|
'occurrences': count,
|
||||||
|
'density_percentage': round(density, 2),
|
||||||
|
'status': self._assess_density(density)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Overall assessment
|
||||||
|
total_keyword_occurrences = sum(kw['occurrences'] for kw in keyword_densities.values())
|
||||||
|
overall_density = (total_keyword_occurrences / total_words * 100) if total_words > 0 else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total_words': total_words,
|
||||||
|
'keyword_densities': keyword_densities,
|
||||||
|
'overall_keyword_density': round(overall_density, 2),
|
||||||
|
'assessment': self._assess_overall_density(overall_density),
|
||||||
|
'recommendations': self._generate_density_recommendations(keyword_densities)
|
||||||
|
}
|
||||||
|
|
||||||
|
def _build_title_with_keywords(
|
||||||
|
self,
|
||||||
|
app_name: str,
|
||||||
|
keywords: List[str],
|
||||||
|
max_length: int
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""Build title combining app name and keywords within limit."""
|
||||||
|
separators = [' - ', ': ', ' | ']
|
||||||
|
|
||||||
|
for sep in separators:
|
||||||
|
for kw in keywords:
|
||||||
|
title = f"{app_name}{sep}{kw}"
|
||||||
|
if len(title) <= max_length:
|
||||||
|
return title
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _optimize_short_description(
|
||||||
|
self,
|
||||||
|
app_info: Dict[str, Any],
|
||||||
|
target_keywords: List[str]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Optimize Google Play short description (80 chars)."""
|
||||||
|
max_length = self.limits['short_description']
|
||||||
|
|
||||||
|
# Focus on unique value proposition with primary keyword
|
||||||
|
unique_value = app_info.get('unique_value', '')
|
||||||
|
primary_keyword = target_keywords[0] if target_keywords else ''
|
||||||
|
|
||||||
|
# Template: [Primary Keyword] - [Unique Value]
|
||||||
|
short_desc = f"{primary_keyword.title()} - {unique_value}"[:max_length]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'short_description': short_desc,
|
||||||
|
'length': len(short_desc),
|
||||||
|
'remaining_chars': max_length - len(short_desc),
|
||||||
|
'keywords_included': [primary_keyword] if primary_keyword in short_desc.lower() else [],
|
||||||
|
'strategy': 'keyword_value_proposition'
|
||||||
|
}
|
||||||
|
|
||||||
|
def _optimize_subtitle(
|
||||||
|
self,
|
||||||
|
app_info: Dict[str, Any],
|
||||||
|
target_keywords: List[str]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Optimize Apple App Store subtitle (30 chars)."""
|
||||||
|
max_length = self.limits['subtitle']
|
||||||
|
|
||||||
|
# Very concise - primary keyword or key feature
|
||||||
|
primary_keyword = target_keywords[0] if target_keywords else ''
|
||||||
|
key_feature = app_info.get('key_features', [''])[0] if app_info.get('key_features') else ''
|
||||||
|
|
||||||
|
options = [
|
||||||
|
primary_keyword[:max_length],
|
||||||
|
key_feature[:max_length],
|
||||||
|
f"{primary_keyword} App"[:max_length]
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'subtitle_options': [opt for opt in options if opt],
|
||||||
|
'max_length': max_length,
|
||||||
|
'recommendation': options[0] if options else ''
|
||||||
|
}
|
||||||
|
|
||||||
|
def _optimize_full_description(
|
||||||
|
self,
|
||||||
|
app_info: Dict[str, Any],
|
||||||
|
target_keywords: List[str]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Optimize full app description (4000 chars for both platforms)."""
|
||||||
|
max_length = self.limits.get('description', self.limits.get('full_description', 4000))
|
||||||
|
|
||||||
|
# Structure: Hook → Features → Benefits → Social Proof → CTA
|
||||||
|
sections = []
|
||||||
|
|
||||||
|
# Hook (with primary keyword)
|
||||||
|
primary_keyword = target_keywords[0] if target_keywords else ''
|
||||||
|
unique_value = app_info.get('unique_value', '')
|
||||||
|
hook = f"{unique_value} {primary_keyword.title()} that helps you achieve more.\n\n"
|
||||||
|
sections.append(hook)
|
||||||
|
|
||||||
|
# Features (with keywords naturally integrated)
|
||||||
|
features = app_info.get('key_features', [])
|
||||||
|
if features:
|
||||||
|
sections.append("KEY FEATURES:\n")
|
||||||
|
for i, feature in enumerate(features[:5], 1):
|
||||||
|
# Integrate keywords naturally
|
||||||
|
feature_text = f"• {feature}"
|
||||||
|
if i <= len(target_keywords):
|
||||||
|
keyword = target_keywords[i-1]
|
||||||
|
if keyword.lower() not in feature.lower():
|
||||||
|
feature_text = f"• {feature} with {keyword}"
|
||||||
|
sections.append(f"{feature_text}\n")
|
||||||
|
sections.append("\n")
|
||||||
|
|
||||||
|
# Benefits
|
||||||
|
target_audience = app_info.get('target_audience', 'users')
|
||||||
|
sections.append(f"PERFECT FOR:\n{target_audience}\n\n")
|
||||||
|
|
||||||
|
# Social proof placeholder
|
||||||
|
sections.append("WHY USERS LOVE US:\n")
|
||||||
|
sections.append("Join thousands of satisfied users who have transformed their workflow.\n\n")
|
||||||
|
|
||||||
|
# CTA
|
||||||
|
sections.append("Download now and start experiencing the difference!")
|
||||||
|
|
||||||
|
# Combine and validate length
|
||||||
|
full_description = "".join(sections)
|
||||||
|
if len(full_description) > max_length:
|
||||||
|
full_description = full_description[:max_length-3] + "..."
|
||||||
|
|
||||||
|
# Calculate keyword density
|
||||||
|
density = self.calculate_keyword_density(full_description, target_keywords)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'full_description': full_description,
|
||||||
|
'length': len(full_description),
|
||||||
|
'remaining_chars': max_length - len(full_description),
|
||||||
|
'keyword_analysis': density,
|
||||||
|
'structure': {
|
||||||
|
'has_hook': True,
|
||||||
|
'has_features': len(features) > 0,
|
||||||
|
'has_benefits': True,
|
||||||
|
'has_cta': True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _remove_plural_duplicates(self, keywords: List[str]) -> List[str]:
|
||||||
|
"""Remove plural forms if singular exists."""
|
||||||
|
deduplicated = []
|
||||||
|
singular_set = set()
|
||||||
|
|
||||||
|
for keyword in keywords:
|
||||||
|
if keyword.endswith('s') and len(keyword) > 1:
|
||||||
|
singular = keyword[:-1]
|
||||||
|
if singular not in singular_set:
|
||||||
|
deduplicated.append(singular)
|
||||||
|
singular_set.add(singular)
|
||||||
|
else:
|
||||||
|
if keyword not in singular_set:
|
||||||
|
deduplicated.append(keyword)
|
||||||
|
singular_set.add(keyword)
|
||||||
|
|
||||||
|
return deduplicated
|
||||||
|
|
||||||
|
def _build_keyword_field(self, keywords: List[str], max_length: int) -> str:
|
||||||
|
"""Build comma-separated keyword field within character limit."""
|
||||||
|
keyword_field = ""
|
||||||
|
|
||||||
|
for keyword in keywords:
|
||||||
|
test_field = f"{keyword_field},{keyword}" if keyword_field else keyword
|
||||||
|
if len(test_field) <= max_length:
|
||||||
|
keyword_field = test_field
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
return keyword_field
|
||||||
|
|
||||||
|
def _calculate_coverage(self, keywords: List[str], text: str) -> Dict[str, int]:
|
||||||
|
"""Calculate how many keywords are covered in text."""
|
||||||
|
text_lower = text.lower()
|
||||||
|
coverage = {}
|
||||||
|
|
||||||
|
for keyword in keywords:
|
||||||
|
coverage[keyword] = text_lower.count(keyword.lower())
|
||||||
|
|
||||||
|
return coverage
|
||||||
|
|
||||||
|
def _assess_density(self, density: float) -> str:
|
||||||
|
"""Assess individual keyword density."""
|
||||||
|
if density < 0.5:
|
||||||
|
return "too_low"
|
||||||
|
elif density <= 2.5:
|
||||||
|
return "optimal"
|
||||||
|
else:
|
||||||
|
return "too_high"
|
||||||
|
|
||||||
|
def _assess_overall_density(self, density: float) -> str:
|
||||||
|
"""Assess overall keyword density."""
|
||||||
|
if density < 2:
|
||||||
|
return "Under-optimized: Consider adding more keyword variations"
|
||||||
|
elif density <= 5:
|
||||||
|
return "Optimal: Good keyword integration without stuffing"
|
||||||
|
elif density <= 8:
|
||||||
|
return "High: Approaching keyword stuffing - reduce keyword usage"
|
||||||
|
else:
|
||||||
|
return "Too High: Keyword stuffing detected - rewrite for natural flow"
|
||||||
|
|
||||||
|
def _generate_density_recommendations(
|
||||||
|
self,
|
||||||
|
keyword_densities: Dict[str, Dict[str, Any]]
|
||||||
|
) -> List[str]:
|
||||||
|
"""Generate recommendations based on keyword density analysis."""
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
for keyword, data in keyword_densities.items():
|
||||||
|
if data['status'] == 'too_low':
|
||||||
|
recommendations.append(
|
||||||
|
f"Increase usage of '{keyword}' - currently only {data['occurrences']} times"
|
||||||
|
)
|
||||||
|
elif data['status'] == 'too_high':
|
||||||
|
recommendations.append(
|
||||||
|
f"Reduce usage of '{keyword}' - appears {data['occurrences']} times (keyword stuffing risk)"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not recommendations:
|
||||||
|
recommendations.append("Keyword density is well-balanced")
|
||||||
|
|
||||||
|
return recommendations
|
||||||
|
|
||||||
|
def _recommend_title_option(self, options: List[Dict[str, Any]]) -> str:
|
||||||
|
"""Recommend best title option based on strategy."""
|
||||||
|
if not options:
|
||||||
|
return "No valid options available"
|
||||||
|
|
||||||
|
# Prefer brand_plus_primary for established apps
|
||||||
|
for option in options:
|
||||||
|
if option['strategy'] == 'brand_plus_primary':
|
||||||
|
return f"Recommended: '{option['title']}' (Balance of brand and SEO)"
|
||||||
|
|
||||||
|
# Fallback to first option
|
||||||
|
return f"Recommended: '{options[0]['title']}' ({options[0]['strategy']})"
|
||||||
|
|
||||||
|
|
||||||
|
def optimize_app_metadata(
|
||||||
|
platform: str,
|
||||||
|
app_info: Dict[str, Any],
|
||||||
|
target_keywords: List[str]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Convenience function to optimize all metadata fields.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
platform: 'apple' or 'google'
|
||||||
|
app_info: App information dictionary
|
||||||
|
target_keywords: Target keywords list
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complete metadata optimization package
|
||||||
|
"""
|
||||||
|
optimizer = MetadataOptimizer(platform)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'platform': platform,
|
||||||
|
'title': optimizer.optimize_title(
|
||||||
|
app_info['name'],
|
||||||
|
target_keywords
|
||||||
|
),
|
||||||
|
'description': optimizer.optimize_description(
|
||||||
|
app_info,
|
||||||
|
target_keywords,
|
||||||
|
'full'
|
||||||
|
),
|
||||||
|
'keyword_field': optimizer.optimize_keyword_field(
|
||||||
|
target_keywords
|
||||||
|
) if platform == 'apple' else None
|
||||||
|
}
|
||||||
@@ -0,0 +1,714 @@
|
|||||||
|
"""
|
||||||
|
Review analysis module for App Store Optimization.
|
||||||
|
Analyzes user reviews for sentiment, issues, and feature requests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, List, Any, Optional, Tuple
|
||||||
|
from collections import Counter
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewAnalyzer:
|
||||||
|
"""Analyzes user reviews for actionable insights."""
|
||||||
|
|
||||||
|
# Sentiment keywords
|
||||||
|
POSITIVE_KEYWORDS = [
|
||||||
|
'great', 'awesome', 'excellent', 'amazing', 'love', 'best', 'perfect',
|
||||||
|
'fantastic', 'wonderful', 'brilliant', 'outstanding', 'superb'
|
||||||
|
]
|
||||||
|
|
||||||
|
NEGATIVE_KEYWORDS = [
|
||||||
|
'bad', 'terrible', 'awful', 'horrible', 'hate', 'worst', 'useless',
|
||||||
|
'broken', 'crash', 'bug', 'slow', 'disappointing', 'frustrating'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Issue indicators
|
||||||
|
ISSUE_KEYWORDS = [
|
||||||
|
'crash', 'bug', 'error', 'broken', 'not working', 'doesnt work',
|
||||||
|
'freezes', 'slow', 'laggy', 'glitch', 'problem', 'issue', 'fail'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Feature request indicators
|
||||||
|
FEATURE_REQUEST_KEYWORDS = [
|
||||||
|
'wish', 'would be nice', 'should add', 'need', 'want', 'hope',
|
||||||
|
'please add', 'missing', 'lacks', 'feature request'
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, app_name: str):
|
||||||
|
"""
|
||||||
|
Initialize review analyzer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_name: Name of the app
|
||||||
|
"""
|
||||||
|
self.app_name = app_name
|
||||||
|
self.reviews = []
|
||||||
|
self.analysis_cache = {}
|
||||||
|
|
||||||
|
def analyze_sentiment(
|
||||||
|
self,
|
||||||
|
reviews: List[Dict[str, Any]]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Analyze sentiment across reviews.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reviews: List of review dicts with 'text', 'rating', 'date'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Sentiment analysis summary
|
||||||
|
"""
|
||||||
|
self.reviews = reviews
|
||||||
|
|
||||||
|
sentiment_counts = {
|
||||||
|
'positive': 0,
|
||||||
|
'neutral': 0,
|
||||||
|
'negative': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
detailed_sentiments = []
|
||||||
|
|
||||||
|
for review in reviews:
|
||||||
|
text = review.get('text', '').lower()
|
||||||
|
rating = review.get('rating', 3)
|
||||||
|
|
||||||
|
# Calculate sentiment score
|
||||||
|
sentiment_score = self._calculate_sentiment_score(text, rating)
|
||||||
|
sentiment_category = self._categorize_sentiment(sentiment_score)
|
||||||
|
|
||||||
|
sentiment_counts[sentiment_category] += 1
|
||||||
|
|
||||||
|
detailed_sentiments.append({
|
||||||
|
'review_id': review.get('id', ''),
|
||||||
|
'rating': rating,
|
||||||
|
'sentiment_score': sentiment_score,
|
||||||
|
'sentiment': sentiment_category,
|
||||||
|
'text_preview': text[:100] + '...' if len(text) > 100 else text
|
||||||
|
})
|
||||||
|
|
||||||
|
# Calculate percentages
|
||||||
|
total = len(reviews)
|
||||||
|
sentiment_distribution = {
|
||||||
|
'positive': round((sentiment_counts['positive'] / total) * 100, 1) if total > 0 else 0,
|
||||||
|
'neutral': round((sentiment_counts['neutral'] / total) * 100, 1) if total > 0 else 0,
|
||||||
|
'negative': round((sentiment_counts['negative'] / total) * 100, 1) if total > 0 else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate average rating
|
||||||
|
avg_rating = sum(r.get('rating', 0) for r in reviews) / total if total > 0 else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total_reviews_analyzed': total,
|
||||||
|
'average_rating': round(avg_rating, 2),
|
||||||
|
'sentiment_distribution': sentiment_distribution,
|
||||||
|
'sentiment_counts': sentiment_counts,
|
||||||
|
'sentiment_trend': self._assess_sentiment_trend(sentiment_distribution),
|
||||||
|
'detailed_sentiments': detailed_sentiments[:50] # Limit output
|
||||||
|
}
|
||||||
|
|
||||||
|
def extract_common_themes(
|
||||||
|
self,
|
||||||
|
reviews: List[Dict[str, Any]],
|
||||||
|
min_mentions: int = 3
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract frequently mentioned themes and topics.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reviews: List of review dicts
|
||||||
|
min_mentions: Minimum mentions to be considered common
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Common themes analysis
|
||||||
|
"""
|
||||||
|
# Extract all words from reviews
|
||||||
|
all_words = []
|
||||||
|
all_phrases = []
|
||||||
|
|
||||||
|
for review in reviews:
|
||||||
|
text = review.get('text', '').lower()
|
||||||
|
# Clean text
|
||||||
|
text = re.sub(r'[^\w\s]', ' ', text)
|
||||||
|
words = text.split()
|
||||||
|
|
||||||
|
# Filter out common words
|
||||||
|
stop_words = {
|
||||||
|
'the', 'and', 'for', 'with', 'this', 'that', 'from', 'have',
|
||||||
|
'app', 'apps', 'very', 'really', 'just', 'but', 'not', 'you'
|
||||||
|
}
|
||||||
|
words = [w for w in words if w not in stop_words and len(w) > 3]
|
||||||
|
|
||||||
|
all_words.extend(words)
|
||||||
|
|
||||||
|
# Extract 2-3 word phrases
|
||||||
|
for i in range(len(words) - 1):
|
||||||
|
phrase = f"{words[i]} {words[i+1]}"
|
||||||
|
all_phrases.append(phrase)
|
||||||
|
|
||||||
|
# Count frequency
|
||||||
|
word_freq = Counter(all_words)
|
||||||
|
phrase_freq = Counter(all_phrases)
|
||||||
|
|
||||||
|
# Filter by min_mentions
|
||||||
|
common_words = [
|
||||||
|
{'word': word, 'mentions': count}
|
||||||
|
for word, count in word_freq.most_common(30)
|
||||||
|
if count >= min_mentions
|
||||||
|
]
|
||||||
|
|
||||||
|
common_phrases = [
|
||||||
|
{'phrase': phrase, 'mentions': count}
|
||||||
|
for phrase, count in phrase_freq.most_common(20)
|
||||||
|
if count >= min_mentions
|
||||||
|
]
|
||||||
|
|
||||||
|
# Categorize themes
|
||||||
|
themes = self._categorize_themes(common_words, common_phrases)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'common_words': common_words,
|
||||||
|
'common_phrases': common_phrases,
|
||||||
|
'identified_themes': themes,
|
||||||
|
'insights': self._generate_theme_insights(themes)
|
||||||
|
}
|
||||||
|
|
||||||
|
def identify_issues(
|
||||||
|
self,
|
||||||
|
reviews: List[Dict[str, Any]],
|
||||||
|
rating_threshold: int = 3
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Identify bugs, crashes, and other issues from reviews.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reviews: List of review dicts
|
||||||
|
rating_threshold: Only analyze reviews at or below this rating
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Issue identification report
|
||||||
|
"""
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
for review in reviews:
|
||||||
|
rating = review.get('rating', 5)
|
||||||
|
if rating > rating_threshold:
|
||||||
|
continue
|
||||||
|
|
||||||
|
text = review.get('text', '').lower()
|
||||||
|
|
||||||
|
# Check for issue keywords
|
||||||
|
mentioned_issues = []
|
||||||
|
for keyword in self.ISSUE_KEYWORDS:
|
||||||
|
if keyword in text:
|
||||||
|
mentioned_issues.append(keyword)
|
||||||
|
|
||||||
|
if mentioned_issues:
|
||||||
|
issues.append({
|
||||||
|
'review_id': review.get('id', ''),
|
||||||
|
'rating': rating,
|
||||||
|
'date': review.get('date', ''),
|
||||||
|
'issue_keywords': mentioned_issues,
|
||||||
|
'text': text[:200] + '...' if len(text) > 200 else text
|
||||||
|
})
|
||||||
|
|
||||||
|
# Group by issue type
|
||||||
|
issue_frequency = Counter()
|
||||||
|
for issue in issues:
|
||||||
|
for keyword in issue['issue_keywords']:
|
||||||
|
issue_frequency[keyword] += 1
|
||||||
|
|
||||||
|
# Categorize issues
|
||||||
|
categorized_issues = self._categorize_issues(issues)
|
||||||
|
|
||||||
|
# Calculate issue severity
|
||||||
|
severity_scores = self._calculate_issue_severity(
|
||||||
|
categorized_issues,
|
||||||
|
len(reviews)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total_issues_found': len(issues),
|
||||||
|
'issue_frequency': dict(issue_frequency.most_common(15)),
|
||||||
|
'categorized_issues': categorized_issues,
|
||||||
|
'severity_scores': severity_scores,
|
||||||
|
'top_issues': self._rank_issues_by_severity(severity_scores),
|
||||||
|
'recommendations': self._generate_issue_recommendations(
|
||||||
|
categorized_issues,
|
||||||
|
severity_scores
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def find_feature_requests(
|
||||||
|
self,
|
||||||
|
reviews: List[Dict[str, Any]]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract feature requests and desired improvements.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reviews: List of review dicts
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Feature request analysis
|
||||||
|
"""
|
||||||
|
feature_requests = []
|
||||||
|
|
||||||
|
for review in reviews:
|
||||||
|
text = review.get('text', '').lower()
|
||||||
|
rating = review.get('rating', 3)
|
||||||
|
|
||||||
|
# Check for feature request indicators
|
||||||
|
is_feature_request = any(
|
||||||
|
keyword in text
|
||||||
|
for keyword in self.FEATURE_REQUEST_KEYWORDS
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_feature_request:
|
||||||
|
# Extract the specific request
|
||||||
|
request_text = self._extract_feature_request_text(text)
|
||||||
|
|
||||||
|
feature_requests.append({
|
||||||
|
'review_id': review.get('id', ''),
|
||||||
|
'rating': rating,
|
||||||
|
'date': review.get('date', ''),
|
||||||
|
'request_text': request_text,
|
||||||
|
'full_review': text[:200] + '...' if len(text) > 200 else text
|
||||||
|
})
|
||||||
|
|
||||||
|
# Cluster similar requests
|
||||||
|
clustered_requests = self._cluster_feature_requests(feature_requests)
|
||||||
|
|
||||||
|
# Prioritize based on frequency and rating context
|
||||||
|
prioritized_requests = self._prioritize_feature_requests(clustered_requests)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total_feature_requests': len(feature_requests),
|
||||||
|
'clustered_requests': clustered_requests,
|
||||||
|
'prioritized_requests': prioritized_requests,
|
||||||
|
'implementation_recommendations': self._generate_feature_recommendations(
|
||||||
|
prioritized_requests
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def track_sentiment_trends(
|
||||||
|
self,
|
||||||
|
reviews_by_period: Dict[str, List[Dict[str, Any]]]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Track sentiment changes over time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reviews_by_period: Dict of period_name: reviews
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Trend analysis
|
||||||
|
"""
|
||||||
|
trends = []
|
||||||
|
|
||||||
|
for period, reviews in reviews_by_period.items():
|
||||||
|
sentiment = self.analyze_sentiment(reviews)
|
||||||
|
|
||||||
|
trends.append({
|
||||||
|
'period': period,
|
||||||
|
'total_reviews': len(reviews),
|
||||||
|
'average_rating': sentiment['average_rating'],
|
||||||
|
'positive_percentage': sentiment['sentiment_distribution']['positive'],
|
||||||
|
'negative_percentage': sentiment['sentiment_distribution']['negative']
|
||||||
|
})
|
||||||
|
|
||||||
|
# Calculate trend direction
|
||||||
|
if len(trends) >= 2:
|
||||||
|
first_period = trends[0]
|
||||||
|
last_period = trends[-1]
|
||||||
|
|
||||||
|
rating_change = last_period['average_rating'] - first_period['average_rating']
|
||||||
|
sentiment_change = last_period['positive_percentage'] - first_period['positive_percentage']
|
||||||
|
|
||||||
|
trend_direction = self._determine_trend_direction(
|
||||||
|
rating_change,
|
||||||
|
sentiment_change
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
trend_direction = 'insufficient_data'
|
||||||
|
|
||||||
|
return {
|
||||||
|
'periods_analyzed': len(trends),
|
||||||
|
'trend_data': trends,
|
||||||
|
'trend_direction': trend_direction,
|
||||||
|
'insights': self._generate_trend_insights(trends, trend_direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
def generate_response_templates(
|
||||||
|
self,
|
||||||
|
issue_category: str
|
||||||
|
) -> List[Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
Generate response templates for common review scenarios.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
issue_category: Category of issue ('crash', 'feature_request', 'positive', etc.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response templates
|
||||||
|
"""
|
||||||
|
templates = {
|
||||||
|
'crash': [
|
||||||
|
{
|
||||||
|
'scenario': 'App crash reported',
|
||||||
|
'template': "Thank you for bringing this to our attention. We're sorry you experienced a crash. "
|
||||||
|
"Our team is investigating this issue. Could you please share more details about when "
|
||||||
|
"this occurred (device model, iOS/Android version) by contacting support@[company].com? "
|
||||||
|
"We're committed to fixing this quickly."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'scenario': 'Crash already fixed',
|
||||||
|
'template': "Thank you for your feedback. We've identified and fixed this crash issue in version [X.X]. "
|
||||||
|
"Please update to the latest version. If the problem persists, please reach out to "
|
||||||
|
"support@[company].com and we'll help you directly."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'bug': [
|
||||||
|
{
|
||||||
|
'scenario': 'Bug reported',
|
||||||
|
'template': "Thanks for reporting this bug. We take these issues seriously. Our team is looking into it "
|
||||||
|
"and we'll have a fix in an upcoming update. We appreciate your patience and will notify you "
|
||||||
|
"when it's resolved."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'feature_request': [
|
||||||
|
{
|
||||||
|
'scenario': 'Feature request received',
|
||||||
|
'template': "Thank you for this suggestion! We're always looking to improve [app_name]. We've added your "
|
||||||
|
"request to our roadmap and will consider it for a future update. Follow us @[social] for "
|
||||||
|
"updates on new features."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'scenario': 'Feature already planned',
|
||||||
|
'template': "Great news! This feature is already on our roadmap and we're working on it. Stay tuned for "
|
||||||
|
"updates in the coming months. Thanks for your feedback!"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'positive': [
|
||||||
|
{
|
||||||
|
'scenario': 'Positive review',
|
||||||
|
'template': "Thank you so much for your kind words! We're thrilled that you're enjoying [app_name]. "
|
||||||
|
"Reviews like yours motivate our team to keep improving. If you ever have suggestions, "
|
||||||
|
"we'd love to hear them!"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'negative_general': [
|
||||||
|
{
|
||||||
|
'scenario': 'General complaint',
|
||||||
|
'template': "We're sorry to hear you're not satisfied with your experience. We'd like to make this right. "
|
||||||
|
"Please contact us at support@[company].com so we can understand the issue better and help "
|
||||||
|
"you directly. Thank you for giving us a chance to improve."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates.get(issue_category, templates['negative_general'])
|
||||||
|
|
||||||
|
def _calculate_sentiment_score(self, text: str, rating: int) -> float:
|
||||||
|
"""Calculate sentiment score (-1 to 1)."""
|
||||||
|
# Start with rating-based score
|
||||||
|
rating_score = (rating - 3) / 2 # Convert 1-5 to -1 to 1
|
||||||
|
|
||||||
|
# Adjust based on text sentiment
|
||||||
|
positive_count = sum(1 for keyword in self.POSITIVE_KEYWORDS if keyword in text)
|
||||||
|
negative_count = sum(1 for keyword in self.NEGATIVE_KEYWORDS if keyword in text)
|
||||||
|
|
||||||
|
text_score = (positive_count - negative_count) / 10 # Normalize
|
||||||
|
|
||||||
|
# Weighted average (60% rating, 40% text)
|
||||||
|
final_score = (rating_score * 0.6) + (text_score * 0.4)
|
||||||
|
|
||||||
|
return max(min(final_score, 1.0), -1.0)
|
||||||
|
|
||||||
|
def _categorize_sentiment(self, score: float) -> str:
|
||||||
|
"""Categorize sentiment score."""
|
||||||
|
if score > 0.3:
|
||||||
|
return 'positive'
|
||||||
|
elif score < -0.3:
|
||||||
|
return 'negative'
|
||||||
|
else:
|
||||||
|
return 'neutral'
|
||||||
|
|
||||||
|
def _assess_sentiment_trend(self, distribution: Dict[str, float]) -> str:
|
||||||
|
"""Assess overall sentiment trend."""
|
||||||
|
positive = distribution['positive']
|
||||||
|
negative = distribution['negative']
|
||||||
|
|
||||||
|
if positive > 70:
|
||||||
|
return 'very_positive'
|
||||||
|
elif positive > 50:
|
||||||
|
return 'positive'
|
||||||
|
elif negative > 30:
|
||||||
|
return 'concerning'
|
||||||
|
elif negative > 50:
|
||||||
|
return 'critical'
|
||||||
|
else:
|
||||||
|
return 'mixed'
|
||||||
|
|
||||||
|
def _categorize_themes(
|
||||||
|
self,
|
||||||
|
common_words: List[Dict[str, Any]],
|
||||||
|
common_phrases: List[Dict[str, Any]]
|
||||||
|
) -> Dict[str, List[str]]:
|
||||||
|
"""Categorize themes from words and phrases."""
|
||||||
|
themes = {
|
||||||
|
'features': [],
|
||||||
|
'performance': [],
|
||||||
|
'usability': [],
|
||||||
|
'support': [],
|
||||||
|
'pricing': []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keywords for each category
|
||||||
|
feature_keywords = {'feature', 'functionality', 'option', 'tool'}
|
||||||
|
performance_keywords = {'fast', 'slow', 'crash', 'lag', 'speed', 'performance'}
|
||||||
|
usability_keywords = {'easy', 'difficult', 'intuitive', 'confusing', 'interface', 'design'}
|
||||||
|
support_keywords = {'support', 'help', 'customer', 'service', 'response'}
|
||||||
|
pricing_keywords = {'price', 'cost', 'expensive', 'cheap', 'subscription', 'free'}
|
||||||
|
|
||||||
|
for word_data in common_words:
|
||||||
|
word = word_data['word']
|
||||||
|
if any(kw in word for kw in feature_keywords):
|
||||||
|
themes['features'].append(word)
|
||||||
|
elif any(kw in word for kw in performance_keywords):
|
||||||
|
themes['performance'].append(word)
|
||||||
|
elif any(kw in word for kw in usability_keywords):
|
||||||
|
themes['usability'].append(word)
|
||||||
|
elif any(kw in word for kw in support_keywords):
|
||||||
|
themes['support'].append(word)
|
||||||
|
elif any(kw in word for kw in pricing_keywords):
|
||||||
|
themes['pricing'].append(word)
|
||||||
|
|
||||||
|
return {k: v for k, v in themes.items() if v} # Remove empty categories
|
||||||
|
|
||||||
|
def _generate_theme_insights(self, themes: Dict[str, List[str]]) -> List[str]:
|
||||||
|
"""Generate insights from themes."""
|
||||||
|
insights = []
|
||||||
|
|
||||||
|
for category, keywords in themes.items():
|
||||||
|
if keywords:
|
||||||
|
insights.append(
|
||||||
|
f"{category.title()}: Users frequently mention {', '.join(keywords[:3])}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return insights[:5]
|
||||||
|
|
||||||
|
def _categorize_issues(self, issues: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
|
||||||
|
"""Categorize issues by type."""
|
||||||
|
categories = {
|
||||||
|
'crashes': [],
|
||||||
|
'bugs': [],
|
||||||
|
'performance': [],
|
||||||
|
'compatibility': []
|
||||||
|
}
|
||||||
|
|
||||||
|
for issue in issues:
|
||||||
|
keywords = issue['issue_keywords']
|
||||||
|
|
||||||
|
if 'crash' in keywords or 'freezes' in keywords:
|
||||||
|
categories['crashes'].append(issue)
|
||||||
|
elif 'bug' in keywords or 'error' in keywords or 'broken' in keywords:
|
||||||
|
categories['bugs'].append(issue)
|
||||||
|
elif 'slow' in keywords or 'laggy' in keywords:
|
||||||
|
categories['performance'].append(issue)
|
||||||
|
else:
|
||||||
|
categories['compatibility'].append(issue)
|
||||||
|
|
||||||
|
return {k: v for k, v in categories.items() if v}
|
||||||
|
|
||||||
|
def _calculate_issue_severity(
|
||||||
|
self,
|
||||||
|
categorized_issues: Dict[str, List[Dict[str, Any]]],
|
||||||
|
total_reviews: int
|
||||||
|
) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""Calculate severity scores for each issue category."""
|
||||||
|
severity_scores = {}
|
||||||
|
|
||||||
|
for category, issues in categorized_issues.items():
|
||||||
|
count = len(issues)
|
||||||
|
percentage = (count / total_reviews) * 100 if total_reviews > 0 else 0
|
||||||
|
|
||||||
|
# Calculate average rating of affected reviews
|
||||||
|
avg_rating = sum(i['rating'] for i in issues) / count if count > 0 else 0
|
||||||
|
|
||||||
|
# Severity score (0-100)
|
||||||
|
severity = min((percentage * 10) + ((5 - avg_rating) * 10), 100)
|
||||||
|
|
||||||
|
severity_scores[category] = {
|
||||||
|
'count': count,
|
||||||
|
'percentage': round(percentage, 2),
|
||||||
|
'average_rating': round(avg_rating, 2),
|
||||||
|
'severity_score': round(severity, 1),
|
||||||
|
'priority': 'critical' if severity > 70 else ('high' if severity > 40 else 'medium')
|
||||||
|
}
|
||||||
|
|
||||||
|
return severity_scores
|
||||||
|
|
||||||
|
def _rank_issues_by_severity(
|
||||||
|
self,
|
||||||
|
severity_scores: Dict[str, Dict[str, Any]]
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Rank issues by severity score."""
|
||||||
|
ranked = sorted(
|
||||||
|
[{'category': cat, **data} for cat, data in severity_scores.items()],
|
||||||
|
key=lambda x: x['severity_score'],
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
return ranked
|
||||||
|
|
||||||
|
def _generate_issue_recommendations(
|
||||||
|
self,
|
||||||
|
categorized_issues: Dict[str, List[Dict[str, Any]]],
|
||||||
|
severity_scores: Dict[str, Dict[str, Any]]
|
||||||
|
) -> List[str]:
|
||||||
|
"""Generate recommendations for addressing issues."""
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
for category, score_data in severity_scores.items():
|
||||||
|
if score_data['priority'] == 'critical':
|
||||||
|
recommendations.append(
|
||||||
|
f"URGENT: Address {category} issues immediately - affecting {score_data['percentage']}% of reviews"
|
||||||
|
)
|
||||||
|
elif score_data['priority'] == 'high':
|
||||||
|
recommendations.append(
|
||||||
|
f"HIGH PRIORITY: Focus on {category} issues in next update"
|
||||||
|
)
|
||||||
|
|
||||||
|
return recommendations
|
||||||
|
|
||||||
|
def _extract_feature_request_text(self, text: str) -> str:
|
||||||
|
"""Extract the specific feature request from review text."""
|
||||||
|
# Simple extraction - find sentence with feature request keywords
|
||||||
|
sentences = text.split('.')
|
||||||
|
for sentence in sentences:
|
||||||
|
if any(keyword in sentence for keyword in self.FEATURE_REQUEST_KEYWORDS):
|
||||||
|
return sentence.strip()
|
||||||
|
return text[:100] # Fallback
|
||||||
|
|
||||||
|
def _cluster_feature_requests(
|
||||||
|
self,
|
||||||
|
feature_requests: List[Dict[str, Any]]
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Cluster similar feature requests."""
|
||||||
|
# Simplified clustering - group by common keywords
|
||||||
|
clusters = {}
|
||||||
|
|
||||||
|
for request in feature_requests:
|
||||||
|
text = request['request_text'].lower()
|
||||||
|
# Extract key words
|
||||||
|
words = [w for w in text.split() if len(w) > 4]
|
||||||
|
|
||||||
|
# Try to find matching cluster
|
||||||
|
matched = False
|
||||||
|
for cluster_key in clusters:
|
||||||
|
if any(word in cluster_key for word in words[:3]):
|
||||||
|
clusters[cluster_key].append(request)
|
||||||
|
matched = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not matched and words:
|
||||||
|
cluster_key = ' '.join(words[:2])
|
||||||
|
clusters[cluster_key] = [request]
|
||||||
|
|
||||||
|
return [
|
||||||
|
{'feature_theme': theme, 'request_count': len(requests), 'examples': requests[:3]}
|
||||||
|
for theme, requests in clusters.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
def _prioritize_feature_requests(
|
||||||
|
self,
|
||||||
|
clustered_requests: List[Dict[str, Any]]
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Prioritize feature requests by frequency."""
|
||||||
|
return sorted(
|
||||||
|
clustered_requests,
|
||||||
|
key=lambda x: x['request_count'],
|
||||||
|
reverse=True
|
||||||
|
)[:10]
|
||||||
|
|
||||||
|
def _generate_feature_recommendations(
|
||||||
|
self,
|
||||||
|
prioritized_requests: List[Dict[str, Any]]
|
||||||
|
) -> List[str]:
|
||||||
|
"""Generate recommendations for feature requests."""
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
if prioritized_requests:
|
||||||
|
top_request = prioritized_requests[0]
|
||||||
|
recommendations.append(
|
||||||
|
f"Most requested feature: {top_request['feature_theme']} "
|
||||||
|
f"({top_request['request_count']} mentions) - consider for next major release"
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(prioritized_requests) > 1:
|
||||||
|
recommendations.append(
|
||||||
|
f"Also consider: {prioritized_requests[1]['feature_theme']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return recommendations
|
||||||
|
|
||||||
|
def _determine_trend_direction(
|
||||||
|
self,
|
||||||
|
rating_change: float,
|
||||||
|
sentiment_change: float
|
||||||
|
) -> str:
|
||||||
|
"""Determine overall trend direction."""
|
||||||
|
if rating_change > 0.2 and sentiment_change > 5:
|
||||||
|
return 'improving'
|
||||||
|
elif rating_change < -0.2 and sentiment_change < -5:
|
||||||
|
return 'declining'
|
||||||
|
else:
|
||||||
|
return 'stable'
|
||||||
|
|
||||||
|
def _generate_trend_insights(
|
||||||
|
self,
|
||||||
|
trends: List[Dict[str, Any]],
|
||||||
|
trend_direction: str
|
||||||
|
) -> List[str]:
|
||||||
|
"""Generate insights from trend analysis."""
|
||||||
|
insights = []
|
||||||
|
|
||||||
|
if trend_direction == 'improving':
|
||||||
|
insights.append("Positive trend: User satisfaction is increasing over time")
|
||||||
|
elif trend_direction == 'declining':
|
||||||
|
insights.append("WARNING: User satisfaction is declining - immediate action needed")
|
||||||
|
else:
|
||||||
|
insights.append("Sentiment is stable - maintain current quality")
|
||||||
|
|
||||||
|
# Review velocity insight
|
||||||
|
if len(trends) >= 2:
|
||||||
|
recent_reviews = trends[-1]['total_reviews']
|
||||||
|
previous_reviews = trends[-2]['total_reviews']
|
||||||
|
|
||||||
|
if recent_reviews > previous_reviews * 1.5:
|
||||||
|
insights.append("Review volume increasing - growing user base or recent controversy")
|
||||||
|
|
||||||
|
return insights
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_reviews(
|
||||||
|
app_name: str,
|
||||||
|
reviews: List[Dict[str, Any]]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Convenience function to perform comprehensive review analysis.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_name: App name
|
||||||
|
reviews: List of review dictionaries
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complete review analysis
|
||||||
|
"""
|
||||||
|
analyzer = ReviewAnalyzer(app_name)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'sentiment_analysis': analyzer.analyze_sentiment(reviews),
|
||||||
|
'common_themes': analyzer.extract_common_themes(reviews),
|
||||||
|
'issues_identified': analyzer.identify_issues(reviews),
|
||||||
|
'feature_requests': analyzer.find_feature_requests(reviews)
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"request_type": "keyword_research",
|
||||||
|
"app_info": {
|
||||||
|
"name": "TaskFlow Pro",
|
||||||
|
"category": "Productivity",
|
||||||
|
"target_audience": "Professionals aged 25-45 working in teams",
|
||||||
|
"key_features": [
|
||||||
|
"AI-powered task prioritization",
|
||||||
|
"Team collaboration tools",
|
||||||
|
"Calendar integration",
|
||||||
|
"Cross-platform sync"
|
||||||
|
],
|
||||||
|
"unique_value": "AI automatically prioritizes your tasks based on deadlines and importance"
|
||||||
|
},
|
||||||
|
"target_keywords": [
|
||||||
|
"task manager",
|
||||||
|
"productivity app",
|
||||||
|
"todo list",
|
||||||
|
"team collaboration",
|
||||||
|
"project management"
|
||||||
|
],
|
||||||
|
"competitors": [
|
||||||
|
"Todoist",
|
||||||
|
"Any.do",
|
||||||
|
"Microsoft To Do",
|
||||||
|
"Things 3"
|
||||||
|
],
|
||||||
|
"platform": "both",
|
||||||
|
"language": "en-US"
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
---
|
||||||
|
name: architect-review
|
||||||
|
description: "Master software architect specializing in modern architecture"
|
||||||
|
risk: unknown
|
||||||
|
source: community
|
||||||
|
date_added: "2026-02-27"
|
||||||
|
---
|
||||||
|
You are a master software architect specializing in modern software architecture patterns, clean architecture principles, and distributed systems design.
|
||||||
|
|
||||||
|
## Use this skill when
|
||||||
|
|
||||||
|
- Reviewing system architecture or major design changes
|
||||||
|
- Evaluating scalability, resilience, or maintainability impacts
|
||||||
|
- Assessing architecture compliance with standards and patterns
|
||||||
|
- Providing architectural guidance for complex systems
|
||||||
|
|
||||||
|
## Do not use this skill when
|
||||||
|
|
||||||
|
- You need a small code review without architectural impact
|
||||||
|
- The change is minor and local to a single module
|
||||||
|
- You lack system context or requirements to assess design
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. Gather system context, goals, and constraints.
|
||||||
|
2. Evaluate architecture decisions and identify risks.
|
||||||
|
3. Recommend improvements with tradeoffs and next steps.
|
||||||
|
4. Document decisions and follow up on validation.
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
|
||||||
|
- Avoid approving high-risk changes without validation plans.
|
||||||
|
- Document assumptions and dependencies to prevent regressions.
|
||||||
|
|
||||||
|
## Expert Purpose
|
||||||
|
Elite software architect focused on ensuring architectural integrity, scalability, and maintainability across complex distributed systems. Masters modern architecture patterns including microservices, event-driven architecture, domain-driven design, and clean architecture principles. Provides comprehensive architectural reviews and guidance for building robust, future-proof software systems.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### Modern Architecture Patterns
|
||||||
|
- Clean Architecture and Hexagonal Architecture implementation
|
||||||
|
- Microservices architecture with proper service boundaries
|
||||||
|
- Event-driven architecture (EDA) with event sourcing and CQRS
|
||||||
|
- Domain-Driven Design (DDD) with bounded contexts and ubiquitous language
|
||||||
|
- Serverless architecture patterns and Function-as-a-Service design
|
||||||
|
- API-first design with GraphQL, REST, and gRPC best practices
|
||||||
|
- Layered architecture with proper separation of concerns
|
||||||
|
|
||||||
|
### Distributed Systems Design
|
||||||
|
- Service mesh architecture with Istio, Linkerd, and Consul Connect
|
||||||
|
- Event streaming with Apache Kafka, Apache Pulsar, and NATS
|
||||||
|
- Distributed data patterns including Saga, Outbox, and Event Sourcing
|
||||||
|
- Circuit breaker, bulkhead, and timeout patterns for resilience
|
||||||
|
- Distributed caching strategies with Redis Cluster and Hazelcast
|
||||||
|
- Load balancing and service discovery patterns
|
||||||
|
- Distributed tracing and observability architecture
|
||||||
|
|
||||||
|
### SOLID Principles & Design Patterns
|
||||||
|
- Single Responsibility, Open/Closed, Liskov Substitution principles
|
||||||
|
- Interface Segregation and Dependency Inversion implementation
|
||||||
|
- Repository, Unit of Work, and Specification patterns
|
||||||
|
- Factory, Strategy, Observer, and Command patterns
|
||||||
|
- Decorator, Adapter, and Facade patterns for clean interfaces
|
||||||
|
- Dependency Injection and Inversion of Control containers
|
||||||
|
- Anti-corruption layers and adapter patterns
|
||||||
|
|
||||||
|
### Cloud-Native Architecture
|
||||||
|
- Container orchestration with Kubernetes and Docker Swarm
|
||||||
|
- Cloud provider patterns for AWS, Azure, and Google Cloud Platform
|
||||||
|
- Infrastructure as Code with Terraform, Pulumi, and CloudFormation
|
||||||
|
- GitOps and CI/CD pipeline architecture
|
||||||
|
- Auto-scaling patterns and resource optimization
|
||||||
|
- Multi-cloud and hybrid cloud architecture strategies
|
||||||
|
- Edge computing and CDN integration patterns
|
||||||
|
|
||||||
|
### Security Architecture
|
||||||
|
- Zero Trust security model implementation
|
||||||
|
- OAuth2, OpenID Connect, and JWT token management
|
||||||
|
- API security patterns including rate limiting and throttling
|
||||||
|
- Data encryption at rest and in transit
|
||||||
|
- Secret management with HashiCorp Vault and cloud key services
|
||||||
|
- Security boundaries and defense in depth strategies
|
||||||
|
- Container and Kubernetes security best practices
|
||||||
|
|
||||||
|
### Performance & Scalability
|
||||||
|
- Horizontal and vertical scaling patterns
|
||||||
|
- Caching strategies at multiple architectural layers
|
||||||
|
- Database scaling with sharding, partitioning, and read replicas
|
||||||
|
- Content Delivery Network (CDN) integration
|
||||||
|
- Asynchronous processing and message queue patterns
|
||||||
|
- Connection pooling and resource management
|
||||||
|
- Performance monitoring and APM integration
|
||||||
|
|
||||||
|
### Data Architecture
|
||||||
|
- Polyglot persistence with SQL and NoSQL databases
|
||||||
|
- Data lake, data warehouse, and data mesh architectures
|
||||||
|
- Event sourcing and Command Query Responsibility Segregation (CQRS)
|
||||||
|
- Database per service pattern in microservices
|
||||||
|
- Master-slave and master-master replication patterns
|
||||||
|
- Distributed transaction patterns and eventual consistency
|
||||||
|
- Data streaming and real-time processing architectures
|
||||||
|
|
||||||
|
### Quality Attributes Assessment
|
||||||
|
- Reliability, availability, and fault tolerance evaluation
|
||||||
|
- Scalability and performance characteristics analysis
|
||||||
|
- Security posture and compliance requirements
|
||||||
|
- Maintainability and technical debt assessment
|
||||||
|
- Testability and deployment pipeline evaluation
|
||||||
|
- Monitoring, logging, and observability capabilities
|
||||||
|
- Cost optimization and resource efficiency analysis
|
||||||
|
|
||||||
|
### Modern Development Practices
|
||||||
|
- Test-Driven Development (TDD) and Behavior-Driven Development (BDD)
|
||||||
|
- DevSecOps integration and shift-left security practices
|
||||||
|
- Feature flags and progressive deployment strategies
|
||||||
|
- Blue-green and canary deployment patterns
|
||||||
|
- Infrastructure immutability and cattle vs. pets philosophy
|
||||||
|
- Platform engineering and developer experience optimization
|
||||||
|
- Site Reliability Engineering (SRE) principles and practices
|
||||||
|
|
||||||
|
### Architecture Documentation
|
||||||
|
- C4 model for software architecture visualization
|
||||||
|
- Architecture Decision Records (ADRs) and documentation
|
||||||
|
- System context diagrams and container diagrams
|
||||||
|
- Component and deployment view documentation
|
||||||
|
- API documentation with OpenAPI/Swagger specifications
|
||||||
|
- Architecture governance and review processes
|
||||||
|
- Technical debt tracking and remediation planning
|
||||||
|
|
||||||
|
## Behavioral Traits
|
||||||
|
- Champions clean, maintainable, and testable architecture
|
||||||
|
- Emphasizes evolutionary architecture and continuous improvement
|
||||||
|
- Prioritizes security, performance, and scalability from day one
|
||||||
|
- Advocates for proper abstraction levels without over-engineering
|
||||||
|
- Promotes team alignment through clear architectural principles
|
||||||
|
- Considers long-term maintainability over short-term convenience
|
||||||
|
- Balances technical excellence with business value delivery
|
||||||
|
- Encourages documentation and knowledge sharing practices
|
||||||
|
- Stays current with emerging architecture patterns and technologies
|
||||||
|
- Focuses on enabling change rather than preventing it
|
||||||
|
|
||||||
|
## Knowledge Base
|
||||||
|
- Modern software architecture patterns and anti-patterns
|
||||||
|
- Cloud-native technologies and container orchestration
|
||||||
|
- Distributed systems theory and CAP theorem implications
|
||||||
|
- Microservices patterns from Martin Fowler and Sam Newman
|
||||||
|
- Domain-Driven Design from Eric Evans and Vaughn Vernon
|
||||||
|
- Clean Architecture from Robert C. Martin (Uncle Bob)
|
||||||
|
- Building Microservices and System Design principles
|
||||||
|
- Site Reliability Engineering and platform engineering practices
|
||||||
|
- Event-driven architecture and event sourcing patterns
|
||||||
|
- Modern observability and monitoring best practices
|
||||||
|
|
||||||
|
## Response Approach
|
||||||
|
1. **Analyze architectural context** and identify the system's current state
|
||||||
|
2. **Assess architectural impact** of proposed changes (High/Medium/Low)
|
||||||
|
3. **Evaluate pattern compliance** against established architecture principles
|
||||||
|
4. **Identify architectural violations** and anti-patterns
|
||||||
|
5. **Recommend improvements** with specific refactoring suggestions
|
||||||
|
6. **Consider scalability implications** for future growth
|
||||||
|
7. **Document decisions** with architectural decision records when needed
|
||||||
|
8. **Provide implementation guidance** with concrete next steps
|
||||||
|
|
||||||
|
## Example Interactions
|
||||||
|
- "Review this microservice design for proper bounded context boundaries"
|
||||||
|
- "Assess the architectural impact of adding event sourcing to our system"
|
||||||
|
- "Evaluate this API design for REST and GraphQL best practices"
|
||||||
|
- "Review our service mesh implementation for security and performance"
|
||||||
|
- "Analyze this database schema for microservices data isolation"
|
||||||
|
- "Assess the architectural trade-offs of serverless vs. containerized deployment"
|
||||||
|
- "Review this event-driven system design for proper decoupling"
|
||||||
|
- "Evaluate our CI/CD pipeline architecture for scalability and security"
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,769 @@
|
|||||||
|
---
|
||||||
|
name: autonomous-agent-patterns
|
||||||
|
description: "Design patterns for building autonomous coding agents, inspired by [Cline](https://github.com/cline/cline) and [OpenAI Codex](https://github.com/openai/codex)."
|
||||||
|
risk: critical
|
||||||
|
source: community
|
||||||
|
date_added: "2026-02-27"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🕹️ Autonomous Agent Patterns
|
||||||
|
|
||||||
|
> Design patterns for building autonomous coding agents, inspired by [Cline](https://github.com/cline/cline) and [OpenAI Codex](https://github.com/openai/codex).
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
Use this skill when:
|
||||||
|
|
||||||
|
- Building autonomous AI agents
|
||||||
|
- Designing tool/function calling APIs
|
||||||
|
- Implementing permission and approval systems
|
||||||
|
- Creating browser automation for agents
|
||||||
|
- Designing human-in-the-loop workflows
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Core Agent Architecture
|
||||||
|
|
||||||
|
### 1.1 Agent Loop
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ AGENT LOOP │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ Think │───▶│ Decide │───▶│ Act │ │
|
||||||
|
│ │ (Reason) │ │ (Plan) │ │ (Execute)│ │
|
||||||
|
│ └──────────┘ └──────────┘ └──────────┘ │
|
||||||
|
│ ▲ │ │
|
||||||
|
│ │ ┌──────────┐ │ │
|
||||||
|
│ └─────────│ Observe │◀─────────┘ │
|
||||||
|
│ │ (Result) │ │
|
||||||
|
│ └──────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentLoop:
|
||||||
|
def __init__(self, llm, tools, max_iterations=50):
|
||||||
|
self.llm = llm
|
||||||
|
self.tools = {t.name: t for t in tools}
|
||||||
|
self.max_iterations = max_iterations
|
||||||
|
self.history = []
|
||||||
|
|
||||||
|
def run(self, task: str) -> str:
|
||||||
|
self.history.append({"role": "user", "content": task})
|
||||||
|
|
||||||
|
for i in range(self.max_iterations):
|
||||||
|
# Think: Get LLM response with tool options
|
||||||
|
response = self.llm.chat(
|
||||||
|
messages=self.history,
|
||||||
|
tools=self._format_tools(),
|
||||||
|
tool_choice="auto"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Decide: Check if agent wants to use a tool
|
||||||
|
if response.tool_calls:
|
||||||
|
for tool_call in response.tool_calls:
|
||||||
|
# Act: Execute the tool
|
||||||
|
result = self._execute_tool(tool_call)
|
||||||
|
|
||||||
|
# Observe: Add result to history
|
||||||
|
self.history.append({
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": tool_call.id,
|
||||||
|
"content": str(result)
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# No more tool calls = task complete
|
||||||
|
return response.content
|
||||||
|
|
||||||
|
return "Max iterations reached"
|
||||||
|
|
||||||
|
def _execute_tool(self, tool_call) -> Any:
|
||||||
|
tool = self.tools[tool_call.name]
|
||||||
|
args = json.loads(tool_call.arguments)
|
||||||
|
return tool.execute(**args)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 Multi-Model Architecture
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MultiModelAgent:
|
||||||
|
"""
|
||||||
|
Use different models for different purposes:
|
||||||
|
- Fast model for planning
|
||||||
|
- Powerful model for complex reasoning
|
||||||
|
- Specialized model for code generation
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.models = {
|
||||||
|
"fast": "gpt-3.5-turbo", # Quick decisions
|
||||||
|
"smart": "gpt-4-turbo", # Complex reasoning
|
||||||
|
"code": "claude-3-sonnet", # Code generation
|
||||||
|
}
|
||||||
|
|
||||||
|
def select_model(self, task_type: str) -> str:
|
||||||
|
if task_type == "planning":
|
||||||
|
return self.models["fast"]
|
||||||
|
elif task_type == "analysis":
|
||||||
|
return self.models["smart"]
|
||||||
|
elif task_type == "code":
|
||||||
|
return self.models["code"]
|
||||||
|
return self.models["smart"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Tool Design Patterns
|
||||||
|
|
||||||
|
### 2.1 Tool Schema
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Tool:
|
||||||
|
"""Base class for agent tools"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def schema(self) -> dict:
|
||||||
|
"""JSON Schema for the tool"""
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"description": self.description,
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": self._get_parameters(),
|
||||||
|
"required": self._get_required()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def execute(self, **kwargs) -> ToolResult:
|
||||||
|
"""Execute the tool and return result"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
class ReadFileTool(Tool):
|
||||||
|
name = "read_file"
|
||||||
|
description = "Read the contents of a file from the filesystem"
|
||||||
|
|
||||||
|
def _get_parameters(self):
|
||||||
|
return {
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Absolute path to the file"
|
||||||
|
},
|
||||||
|
"start_line": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Line to start reading from (1-indexed)"
|
||||||
|
},
|
||||||
|
"end_line": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Line to stop reading at (inclusive)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_required(self):
|
||||||
|
return ["path"]
|
||||||
|
|
||||||
|
def execute(self, path: str, start_line: int = None, end_line: int = None) -> ToolResult:
|
||||||
|
try:
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
if start_line and end_line:
|
||||||
|
lines = lines[start_line-1:end_line]
|
||||||
|
|
||||||
|
return ToolResult(
|
||||||
|
success=True,
|
||||||
|
output="".join(lines)
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return ToolResult(
|
||||||
|
success=False,
|
||||||
|
error=f"File not found: {path}"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Essential Agent Tools
|
||||||
|
|
||||||
|
```python
|
||||||
|
CODING_AGENT_TOOLS = {
|
||||||
|
# File operations
|
||||||
|
"read_file": "Read file contents",
|
||||||
|
"write_file": "Create or overwrite a file",
|
||||||
|
"edit_file": "Make targeted edits to a file",
|
||||||
|
"list_directory": "List files and folders",
|
||||||
|
"search_files": "Search for files by pattern",
|
||||||
|
|
||||||
|
# Code understanding
|
||||||
|
"search_code": "Search for code patterns (grep)",
|
||||||
|
"get_definition": "Find function/class definition",
|
||||||
|
"get_references": "Find all references to a symbol",
|
||||||
|
|
||||||
|
# Terminal
|
||||||
|
"run_command": "Execute a shell command",
|
||||||
|
"read_output": "Read command output",
|
||||||
|
"send_input": "Send input to running command",
|
||||||
|
|
||||||
|
# Browser (optional)
|
||||||
|
"open_browser": "Open URL in browser",
|
||||||
|
"click_element": "Click on page element",
|
||||||
|
"type_text": "Type text into input",
|
||||||
|
"screenshot": "Capture screenshot",
|
||||||
|
|
||||||
|
# Context
|
||||||
|
"ask_user": "Ask the user a question",
|
||||||
|
"search_web": "Search the web for information"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Edit Tool Design
|
||||||
|
|
||||||
|
```python
|
||||||
|
class EditFileTool(Tool):
|
||||||
|
"""
|
||||||
|
Precise file editing with conflict detection.
|
||||||
|
Uses search/replace pattern for reliable edits.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "edit_file"
|
||||||
|
description = "Edit a file by replacing specific content"
|
||||||
|
|
||||||
|
def execute(
|
||||||
|
self,
|
||||||
|
path: str,
|
||||||
|
search: str,
|
||||||
|
replace: str,
|
||||||
|
expected_occurrences: int = 1
|
||||||
|
) -> ToolResult:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
path: File to edit
|
||||||
|
search: Exact text to find (must match exactly, including whitespace)
|
||||||
|
replace: Text to replace with
|
||||||
|
expected_occurrences: How many times search should appear (validation)
|
||||||
|
"""
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Validate
|
||||||
|
actual_occurrences = content.count(search)
|
||||||
|
if actual_occurrences != expected_occurrences:
|
||||||
|
return ToolResult(
|
||||||
|
success=False,
|
||||||
|
error=f"Expected {expected_occurrences} occurrences, found {actual_occurrences}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if actual_occurrences == 0:
|
||||||
|
return ToolResult(
|
||||||
|
success=False,
|
||||||
|
error="Search text not found in file"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply edit
|
||||||
|
new_content = content.replace(search, replace)
|
||||||
|
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
f.write(new_content)
|
||||||
|
|
||||||
|
return ToolResult(
|
||||||
|
success=True,
|
||||||
|
output=f"Replaced {actual_occurrences} occurrence(s)"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Permission & Safety Patterns
|
||||||
|
|
||||||
|
### 3.1 Permission Levels
|
||||||
|
|
||||||
|
```python
|
||||||
|
class PermissionLevel(Enum):
|
||||||
|
# Fully automatic - no user approval needed
|
||||||
|
AUTO = "auto"
|
||||||
|
|
||||||
|
# Ask once per session
|
||||||
|
ASK_ONCE = "ask_once"
|
||||||
|
|
||||||
|
# Ask every time
|
||||||
|
ASK_EACH = "ask_each"
|
||||||
|
|
||||||
|
# Never allow
|
||||||
|
NEVER = "never"
|
||||||
|
|
||||||
|
PERMISSION_CONFIG = {
|
||||||
|
# Low risk - can auto-approve
|
||||||
|
"read_file": PermissionLevel.AUTO,
|
||||||
|
"list_directory": PermissionLevel.AUTO,
|
||||||
|
"search_code": PermissionLevel.AUTO,
|
||||||
|
|
||||||
|
# Medium risk - ask once
|
||||||
|
"write_file": PermissionLevel.ASK_ONCE,
|
||||||
|
"edit_file": PermissionLevel.ASK_ONCE,
|
||||||
|
|
||||||
|
# High risk - ask each time
|
||||||
|
"run_command": PermissionLevel.ASK_EACH,
|
||||||
|
"delete_file": PermissionLevel.ASK_EACH,
|
||||||
|
|
||||||
|
# Dangerous - never auto-approve
|
||||||
|
"sudo_command": PermissionLevel.NEVER,
|
||||||
|
"format_disk": PermissionLevel.NEVER
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Approval UI Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ApprovalManager:
|
||||||
|
def __init__(self, ui, config):
|
||||||
|
self.ui = ui
|
||||||
|
self.config = config
|
||||||
|
self.session_approvals = {}
|
||||||
|
|
||||||
|
def request_approval(self, tool_name: str, args: dict) -> bool:
|
||||||
|
level = self.config.get(tool_name, PermissionLevel.ASK_EACH)
|
||||||
|
|
||||||
|
if level == PermissionLevel.AUTO:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if level == PermissionLevel.NEVER:
|
||||||
|
self.ui.show_error(f"Tool '{tool_name}' is not allowed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if level == PermissionLevel.ASK_ONCE:
|
||||||
|
if tool_name in self.session_approvals:
|
||||||
|
return self.session_approvals[tool_name]
|
||||||
|
|
||||||
|
# Show approval dialog
|
||||||
|
approved = self.ui.show_approval_dialog(
|
||||||
|
tool=tool_name,
|
||||||
|
args=args,
|
||||||
|
risk_level=self._assess_risk(tool_name, args)
|
||||||
|
)
|
||||||
|
|
||||||
|
if level == PermissionLevel.ASK_ONCE:
|
||||||
|
self.session_approvals[tool_name] = approved
|
||||||
|
|
||||||
|
return approved
|
||||||
|
|
||||||
|
def _assess_risk(self, tool_name: str, args: dict) -> str:
|
||||||
|
"""Analyze specific call for risk level"""
|
||||||
|
if tool_name == "run_command":
|
||||||
|
cmd = args.get("command", "")
|
||||||
|
if any(danger in cmd for danger in ["rm -rf", "sudo", "chmod"]):
|
||||||
|
return "HIGH"
|
||||||
|
return "MEDIUM"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Sandboxing
|
||||||
|
|
||||||
|
```python
|
||||||
|
class SandboxedExecution:
|
||||||
|
"""
|
||||||
|
Execute code/commands in isolated environment
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, workspace_dir: str):
|
||||||
|
self.workspace = workspace_dir
|
||||||
|
self.allowed_commands = ["npm", "python", "node", "git", "ls", "cat"]
|
||||||
|
self.blocked_paths = ["/etc", "/usr", "/bin", os.path.expanduser("~")]
|
||||||
|
|
||||||
|
def validate_path(self, path: str) -> bool:
|
||||||
|
"""Ensure path is within workspace"""
|
||||||
|
real_path = os.path.realpath(path)
|
||||||
|
workspace_real = os.path.realpath(self.workspace)
|
||||||
|
return real_path.startswith(workspace_real)
|
||||||
|
|
||||||
|
def validate_command(self, command: str) -> bool:
|
||||||
|
"""Check if command is allowed"""
|
||||||
|
cmd_parts = shlex.split(command)
|
||||||
|
if not cmd_parts:
|
||||||
|
return False
|
||||||
|
|
||||||
|
base_cmd = cmd_parts[0]
|
||||||
|
return base_cmd in self.allowed_commands
|
||||||
|
|
||||||
|
def execute_sandboxed(self, command: str) -> ToolResult:
|
||||||
|
if not self.validate_command(command):
|
||||||
|
return ToolResult(
|
||||||
|
success=False,
|
||||||
|
error=f"Command not allowed: {command}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Execute in isolated environment
|
||||||
|
result = subprocess.run(
|
||||||
|
command,
|
||||||
|
shell=True,
|
||||||
|
cwd=self.workspace,
|
||||||
|
capture_output=True,
|
||||||
|
timeout=30,
|
||||||
|
env={
|
||||||
|
**os.environ,
|
||||||
|
"HOME": self.workspace, # Isolate home directory
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return ToolResult(
|
||||||
|
success=result.returncode == 0,
|
||||||
|
output=result.stdout.decode(),
|
||||||
|
error=result.stderr.decode() if result.returncode != 0 else None
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Browser Automation
|
||||||
|
|
||||||
|
### 4.1 Browser Tool Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BrowserTool:
|
||||||
|
"""
|
||||||
|
Browser automation for agents using Playwright/Puppeteer.
|
||||||
|
Enables visual debugging and web testing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, headless: bool = True):
|
||||||
|
self.browser = None
|
||||||
|
self.page = None
|
||||||
|
self.headless = headless
|
||||||
|
|
||||||
|
async def open_url(self, url: str) -> ToolResult:
|
||||||
|
"""Navigate to URL and return page info"""
|
||||||
|
if not self.browser:
|
||||||
|
self.browser = await playwright.chromium.launch(headless=self.headless)
|
||||||
|
self.page = await self.browser.new_page()
|
||||||
|
|
||||||
|
await self.page.goto(url)
|
||||||
|
|
||||||
|
# Capture state
|
||||||
|
screenshot = await self.page.screenshot(type='png')
|
||||||
|
title = await self.page.title()
|
||||||
|
|
||||||
|
return ToolResult(
|
||||||
|
success=True,
|
||||||
|
output=f"Loaded: {title}",
|
||||||
|
metadata={
|
||||||
|
"screenshot": base64.b64encode(screenshot).decode(),
|
||||||
|
"url": self.page.url
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def click(self, selector: str) -> ToolResult:
|
||||||
|
"""Click on an element"""
|
||||||
|
try:
|
||||||
|
await self.page.click(selector, timeout=5000)
|
||||||
|
await self.page.wait_for_load_state("networkidle")
|
||||||
|
|
||||||
|
screenshot = await self.page.screenshot()
|
||||||
|
return ToolResult(
|
||||||
|
success=True,
|
||||||
|
output=f"Clicked: {selector}",
|
||||||
|
metadata={"screenshot": base64.b64encode(screenshot).decode()}
|
||||||
|
)
|
||||||
|
except TimeoutError:
|
||||||
|
return ToolResult(
|
||||||
|
success=False,
|
||||||
|
error=f"Element not found: {selector}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def type_text(self, selector: str, text: str) -> ToolResult:
|
||||||
|
"""Type text into an input"""
|
||||||
|
await self.page.fill(selector, text)
|
||||||
|
return ToolResult(success=True, output=f"Typed into {selector}")
|
||||||
|
|
||||||
|
async def get_page_content(self) -> ToolResult:
|
||||||
|
"""Get accessible text content of the page"""
|
||||||
|
content = await self.page.evaluate("""
|
||||||
|
() => {
|
||||||
|
// Get visible text
|
||||||
|
const walker = document.createTreeWalker(
|
||||||
|
document.body,
|
||||||
|
NodeFilter.SHOW_TEXT,
|
||||||
|
null,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
let text = '';
|
||||||
|
while (walker.nextNode()) {
|
||||||
|
const node = walker.currentNode;
|
||||||
|
if (node.textContent.trim()) {
|
||||||
|
text += node.textContent.trim() + '\\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
return ToolResult(success=True, output=content)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Visual Agent Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
class VisualAgent:
|
||||||
|
"""
|
||||||
|
Agent that uses screenshots to understand web pages.
|
||||||
|
Can identify elements visually without selectors.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, llm, browser):
|
||||||
|
self.llm = llm
|
||||||
|
self.browser = browser
|
||||||
|
|
||||||
|
async def describe_page(self) -> str:
|
||||||
|
"""Use vision model to describe current page"""
|
||||||
|
screenshot = await self.browser.screenshot()
|
||||||
|
|
||||||
|
response = self.llm.chat([
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": "Describe this webpage. List all interactive elements you see."},
|
||||||
|
{"type": "image", "data": screenshot}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
return response.content
|
||||||
|
|
||||||
|
async def find_and_click(self, description: str) -> ToolResult:
|
||||||
|
"""Find element by visual description and click it"""
|
||||||
|
screenshot = await self.browser.screenshot()
|
||||||
|
|
||||||
|
# Ask vision model to find element
|
||||||
|
response = self.llm.chat([
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": f"""
|
||||||
|
Find the element matching: "{description}"
|
||||||
|
Return the approximate coordinates as JSON: {{"x": number, "y": number}}
|
||||||
|
"""
|
||||||
|
},
|
||||||
|
{"type": "image", "data": screenshot}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
coords = json.loads(response.content)
|
||||||
|
await self.browser.page.mouse.click(coords["x"], coords["y"])
|
||||||
|
|
||||||
|
return ToolResult(success=True, output=f"Clicked at ({coords['x']}, {coords['y']})")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Context Management
|
||||||
|
|
||||||
|
### 5.1 Context Injection Patterns
|
||||||
|
|
||||||
|
````python
|
||||||
|
class ContextManager:
|
||||||
|
"""
|
||||||
|
Manage context provided to the agent.
|
||||||
|
Inspired by Cline's @-mention patterns.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, workspace: str):
|
||||||
|
self.workspace = workspace
|
||||||
|
self.context = []
|
||||||
|
|
||||||
|
def add_file(self, path: str) -> None:
|
||||||
|
"""@file - Add file contents to context"""
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
self.context.append({
|
||||||
|
"type": "file",
|
||||||
|
"path": path,
|
||||||
|
"content": content
|
||||||
|
})
|
||||||
|
|
||||||
|
def add_folder(self, path: str, max_files: int = 20) -> None:
|
||||||
|
"""@folder - Add all files in folder"""
|
||||||
|
for root, dirs, files in os.walk(path):
|
||||||
|
for file in files[:max_files]:
|
||||||
|
file_path = os.path.join(root, file)
|
||||||
|
self.add_file(file_path)
|
||||||
|
|
||||||
|
def add_url(self, url: str) -> None:
|
||||||
|
"""@url - Fetch and add URL content"""
|
||||||
|
response = requests.get(url)
|
||||||
|
content = html_to_markdown(response.text)
|
||||||
|
|
||||||
|
self.context.append({
|
||||||
|
"type": "url",
|
||||||
|
"url": url,
|
||||||
|
"content": content
|
||||||
|
})
|
||||||
|
|
||||||
|
def add_problems(self, diagnostics: list) -> None:
|
||||||
|
"""@problems - Add IDE diagnostics"""
|
||||||
|
self.context.append({
|
||||||
|
"type": "diagnostics",
|
||||||
|
"problems": diagnostics
|
||||||
|
})
|
||||||
|
|
||||||
|
def format_for_prompt(self) -> str:
|
||||||
|
"""Format all context for LLM prompt"""
|
||||||
|
parts = []
|
||||||
|
for item in self.context:
|
||||||
|
if item["type"] == "file":
|
||||||
|
parts.append(f"## File: {item['path']}\n```\n{item['content']}\n```")
|
||||||
|
elif item["type"] == "url":
|
||||||
|
parts.append(f"## URL: {item['url']}\n{item['content']}")
|
||||||
|
elif item["type"] == "diagnostics":
|
||||||
|
parts.append(f"## Problems:\n{json.dumps(item['problems'], indent=2)}")
|
||||||
|
|
||||||
|
return "\n\n".join(parts)
|
||||||
|
````
|
||||||
|
|
||||||
|
### 5.2 Checkpoint/Resume
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CheckpointManager:
|
||||||
|
"""
|
||||||
|
Save and restore agent state for long-running tasks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, storage_dir: str):
|
||||||
|
self.storage_dir = storage_dir
|
||||||
|
os.makedirs(storage_dir, exist_ok=True)
|
||||||
|
|
||||||
|
def save_checkpoint(self, session_id: str, state: dict) -> str:
|
||||||
|
"""Save current agent state"""
|
||||||
|
checkpoint = {
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"session_id": session_id,
|
||||||
|
"history": state["history"],
|
||||||
|
"context": state["context"],
|
||||||
|
"workspace_state": self._capture_workspace(state["workspace"]),
|
||||||
|
"metadata": state.get("metadata", {})
|
||||||
|
}
|
||||||
|
|
||||||
|
path = os.path.join(self.storage_dir, f"{session_id}.json")
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
json.dump(checkpoint, f, indent=2)
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
def restore_checkpoint(self, checkpoint_path: str) -> dict:
|
||||||
|
"""Restore agent state from checkpoint"""
|
||||||
|
with open(checkpoint_path, 'r') as f:
|
||||||
|
checkpoint = json.load(f)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"history": checkpoint["history"],
|
||||||
|
"context": checkpoint["context"],
|
||||||
|
"workspace": self._restore_workspace(checkpoint["workspace_state"]),
|
||||||
|
"metadata": checkpoint["metadata"]
|
||||||
|
}
|
||||||
|
|
||||||
|
def _capture_workspace(self, workspace: str) -> dict:
|
||||||
|
"""Capture relevant workspace state"""
|
||||||
|
# Git status, file hashes, etc.
|
||||||
|
return {
|
||||||
|
"git_ref": subprocess.getoutput(f"cd {workspace} && git rev-parse HEAD"),
|
||||||
|
"git_dirty": subprocess.getoutput(f"cd {workspace} && git status --porcelain")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. MCP (Model Context Protocol) Integration
|
||||||
|
|
||||||
|
### 6.1 MCP Server Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
from mcp import Server, Tool
|
||||||
|
|
||||||
|
class MCPAgent:
|
||||||
|
"""
|
||||||
|
Agent that can dynamically discover and use MCP tools.
|
||||||
|
'Add a tool that...' pattern from Cline.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, llm):
|
||||||
|
self.llm = llm
|
||||||
|
self.mcp_servers = {}
|
||||||
|
self.available_tools = {}
|
||||||
|
|
||||||
|
def connect_server(self, name: str, config: dict) -> None:
|
||||||
|
"""Connect to an MCP server"""
|
||||||
|
server = Server(config)
|
||||||
|
self.mcp_servers[name] = server
|
||||||
|
|
||||||
|
# Discover tools
|
||||||
|
tools = server.list_tools()
|
||||||
|
for tool in tools:
|
||||||
|
self.available_tools[tool.name] = {
|
||||||
|
"server": name,
|
||||||
|
"schema": tool.schema
|
||||||
|
}
|
||||||
|
|
||||||
|
async def create_tool(self, description: str) -> str:
|
||||||
|
"""
|
||||||
|
Create a new MCP server based on user description.
|
||||||
|
'Add a tool that fetches Jira tickets'
|
||||||
|
"""
|
||||||
|
# Generate MCP server code
|
||||||
|
code = self.llm.generate(f"""
|
||||||
|
Create a Python MCP server with a tool that does:
|
||||||
|
{description}
|
||||||
|
|
||||||
|
Use the FastMCP framework. Include proper error handling.
|
||||||
|
Return only the Python code.
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Save and install
|
||||||
|
server_name = self._extract_name(description)
|
||||||
|
path = f"./mcp_servers/{server_name}/server.py"
|
||||||
|
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
f.write(code)
|
||||||
|
|
||||||
|
# Hot-reload
|
||||||
|
self.connect_server(server_name, {"path": path})
|
||||||
|
|
||||||
|
return f"Created tool: {server_name}"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices Checklist
|
||||||
|
|
||||||
|
### Agent Design
|
||||||
|
|
||||||
|
- [ ] Clear task decomposition
|
||||||
|
- [ ] Appropriate tool granularity
|
||||||
|
- [ ] Error handling at each step
|
||||||
|
- [ ] Progress visibility to user
|
||||||
|
|
||||||
|
### Safety
|
||||||
|
|
||||||
|
- [ ] Permission system implemented
|
||||||
|
- [ ] Dangerous operations blocked
|
||||||
|
- [ ] Sandbox for untrusted code
|
||||||
|
- [ ] Audit logging enabled
|
||||||
|
|
||||||
|
### UX
|
||||||
|
|
||||||
|
- [ ] Approval UI is clear
|
||||||
|
- [ ] Progress updates provided
|
||||||
|
- [ ] Undo/rollback available
|
||||||
|
- [ ] Explanation of actions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Cline](https://github.com/cline/cline)
|
||||||
|
- [OpenAI Codex](https://github.com/openai/codex)
|
||||||
|
- [Model Context Protocol](https://modelcontextprotocol.io/)
|
||||||
|
- [Anthropic Tool Use](https://docs.anthropic.com/claude/docs/tool-use)
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
name: avoid-ai-writing
|
||||||
|
description: "Audit and rewrite content to remove 21 categories of AI writing patterns with a 43-entry replacement table"
|
||||||
|
risk: none
|
||||||
|
source: https://github.com/conorbronsdon/avoid-ai-writing
|
||||||
|
date_added: "2026-03-06"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Avoid AI Writing — Audit & Rewrite
|
||||||
|
|
||||||
|
Detects and fixes AI writing patterns ("AI-isms") that make text sound machine-generated. Covers 21 pattern categories with a 43-entry word/phrase replacement table that maps each flagged term to a specific, plainer alternative.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
- When asked to "remove AI-isms," "clean up AI writing," or "make this sound less like AI"
|
||||||
|
- After drafting content with AI and before publishing
|
||||||
|
- When editing any text that sounds like it was generated rather than written
|
||||||
|
- When auditing documentation, blog posts, marketing copy, or internal communications for AI tells
|
||||||
|
|
||||||
|
## What It Detects
|
||||||
|
|
||||||
|
**21 pattern categories:** formatting issues (em dashes, bold overuse, emoji headers, bullet-heavy sections), sentence structure problems (hedging, hollow intensifiers, rule of three), word/phrase replacements (43 entries like leverage→use, utilize→use, robust→reliable), template phrases, transition phrases, structural issues, significance inflation, copula avoidance, synonym cycling, vague attributions, filler phrases, generic conclusions, chatbot artifacts, notability name-dropping, superficial -ing analyses, promotional language, formulaic challenges, false ranges, inline-header lists, title case headings, and cutoff disclaimers.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
**Prompt:**
|
||||||
|
```
|
||||||
|
Audit this for AI writing patterns:
|
||||||
|
|
||||||
|
"In today's rapidly evolving AI landscape, developers are embarking on a pivotal journey to leverage cutting-edge tools that streamline their workflows. Moreover, these robust solutions serve as a testament to the industry's commitment to fostering seamless experiences."
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:** The skill returns four sections:
|
||||||
|
1. **Issues found** — every AI-ism quoted (landscape, embarking, pivotal, leverage, cutting-edge, streamline, robust, serves as, testament to, fostering, seamless, Moreover, In today's rapidly evolving...)
|
||||||
|
2. **Rewritten version** — "Developers are starting to use newer AI tools to simplify their work. These tools are reliable, and they're making development less painful."
|
||||||
|
3. **What changed** — summary of edits
|
||||||
|
4. **Second-pass audit** — re-reads the rewrite to catch any surviving tells
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Does not detect AI-generated code, only prose
|
||||||
|
- Pattern matching is guideline-based, not absolute — some flagged words are fine in context
|
||||||
|
- The replacement table suggests alternatives but the best choice depends on context
|
||||||
|
- Cannot verify factual claims or find real citations to replace vague attributions
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
name: blueprint
|
||||||
|
description: "Turn a one-line objective into a step-by-step construction plan any coding agent can execute cold. Each step has a self-contained context brief — a fresh agent in a new session can pick up any step without reading prior steps."
|
||||||
|
category: planning
|
||||||
|
risk: safe
|
||||||
|
source: community
|
||||||
|
date_added: "2026-03-10"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Blueprint — Construction Plan Generator
|
||||||
|
|
||||||
|
Turn a one-line objective into a step-by-step plan any coding agent can execute cold.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Blueprint is for multi-session, multi-agent engineering projects where each step must be independently executable by a fresh agent that has never seen the conversation history. Install it once, invoke it with `/blueprint <project> <objective>`.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
- Use when the task requires multiple PRs or sessions
|
||||||
|
- Use when multiple agents or team members need to share execution
|
||||||
|
- Use when you want adversarial review of the plan before execution
|
||||||
|
- Use when parallel step detection and dependency graphs matter
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **Research** — Scans the codebase, reads project memory, runs pre-flight checks
|
||||||
|
2. **Design** — Breaks the objective into one-PR-sized steps, identifies parallelism, assigns model tiers
|
||||||
|
3. **Draft** — Generates the plan from a structured template with branch workflow rules, CI policy, and rollback strategies inline
|
||||||
|
4. **Review** — Delegates adversarial review to a strongest-model sub-agent (falls back to default model if unavailable)
|
||||||
|
5. **Register** — Saves the plan and updates project memory
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1: Database migration
|
||||||
|
```
|
||||||
|
/blueprint myapp "migrate database to PostgreSQL"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Plugin extraction
|
||||||
|
```
|
||||||
|
/blueprint antbot "extract providers into plugins"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- ✅ Use for tasks requiring 3+ PRs or multiple sessions
|
||||||
|
- ✅ Let Blueprint auto-detect git/gh availability — it degrades gracefully
|
||||||
|
- ❌ Don't invoke for tasks completable in a single PR
|
||||||
|
- ❌ Don't invoke when the user says "just do it"
|
||||||
|
|
||||||
|
## Key Differentiators
|
||||||
|
|
||||||
|
- **Cold-start execution**: Every step has a self-contained context brief
|
||||||
|
- **Adversarial review gate**: Strongest-model review before execution
|
||||||
|
- **Zero runtime risk**: Pure markdown — no hooks, no scripts, no executable code
|
||||||
|
- **Plan mutation protocol**: Steps can be split, inserted, skipped with audit trail
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.claude/skills
|
||||||
|
git clone https://github.com/antbotlab/blueprint.git ~/.claude/skills/blueprint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [GitHub Repository](https://github.com/antbotlab/blueprint)
|
||||||
|
- [Examples: small plan](https://github.com/antbotlab/blueprint/blob/main/examples/small-plan.md)
|
||||||
|
- [Examples: large plan](https://github.com/antbotlab/blueprint/blob/main/examples/large-plan.md)
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
---
|
||||||
|
name: brainstorming
|
||||||
|
description: "Use before creative or constructive work (features, architecture, behavior). Transforms vague ideas into validated designs through disciplined reasoning and collaboration."
|
||||||
|
risk: unknown
|
||||||
|
source: community
|
||||||
|
date_added: "2026-02-27"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Brainstorming Ideas Into Designs
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Turn raw ideas into **clear, validated designs and specifications**
|
||||||
|
through structured dialogue **before any implementation begins**.
|
||||||
|
|
||||||
|
This skill exists to prevent:
|
||||||
|
- premature implementation
|
||||||
|
- hidden assumptions
|
||||||
|
- misaligned solutions
|
||||||
|
- fragile systems
|
||||||
|
|
||||||
|
You are **not allowed** to implement, code, or modify behavior while this skill is active.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Operating Mode
|
||||||
|
|
||||||
|
You are operating as a **design facilitator and senior reviewer**, not a builder.
|
||||||
|
|
||||||
|
- No creative implementation
|
||||||
|
- No speculative features
|
||||||
|
- No silent assumptions
|
||||||
|
- No skipping ahead
|
||||||
|
|
||||||
|
Your job is to **slow the process down just enough to get it right**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Process
|
||||||
|
|
||||||
|
### 1️⃣ Understand the Current Context (Mandatory First Step)
|
||||||
|
|
||||||
|
Before asking any questions:
|
||||||
|
|
||||||
|
- Review the current project state (if available):
|
||||||
|
- files
|
||||||
|
- documentation
|
||||||
|
- plans
|
||||||
|
- prior decisions
|
||||||
|
- Identify what already exists vs. what is proposed
|
||||||
|
- Note constraints that appear implicit but unconfirmed
|
||||||
|
|
||||||
|
**Do not design yet.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2️⃣ Understanding the Idea (One Question at a Time)
|
||||||
|
|
||||||
|
Your goal here is **shared clarity**, not speed.
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
|
||||||
|
- Ask **one question per message**
|
||||||
|
- Prefer **multiple-choice questions** when possible
|
||||||
|
- Use open-ended questions only when necessary
|
||||||
|
- If a topic needs depth, split it into multiple questions
|
||||||
|
|
||||||
|
Focus on understanding:
|
||||||
|
|
||||||
|
- purpose
|
||||||
|
- target users
|
||||||
|
- constraints
|
||||||
|
- success criteria
|
||||||
|
- explicit non-goals
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3️⃣ Non-Functional Requirements (Mandatory)
|
||||||
|
|
||||||
|
You MUST explicitly clarify or propose assumptions for:
|
||||||
|
|
||||||
|
- Performance expectations
|
||||||
|
- Scale (users, data, traffic)
|
||||||
|
- Security or privacy constraints
|
||||||
|
- Reliability / availability needs
|
||||||
|
- Maintenance and ownership expectations
|
||||||
|
|
||||||
|
If the user is unsure:
|
||||||
|
|
||||||
|
- Propose reasonable defaults
|
||||||
|
- Clearly mark them as **assumptions**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4️⃣ Understanding Lock (Hard Gate)
|
||||||
|
|
||||||
|
Before proposing **any design**, you MUST pause and do the following:
|
||||||
|
|
||||||
|
#### Understanding Summary
|
||||||
|
Provide a concise summary (5–7 bullets) covering:
|
||||||
|
- What is being built
|
||||||
|
- Why it exists
|
||||||
|
- Who it is for
|
||||||
|
- Key constraints
|
||||||
|
- Explicit non-goals
|
||||||
|
|
||||||
|
#### Assumptions
|
||||||
|
List all assumptions explicitly.
|
||||||
|
|
||||||
|
#### Open Questions
|
||||||
|
List unresolved questions, if any.
|
||||||
|
|
||||||
|
Then ask:
|
||||||
|
|
||||||
|
> “Does this accurately reflect your intent?
|
||||||
|
> Please confirm or correct anything before we move to design.”
|
||||||
|
|
||||||
|
**Do NOT proceed until explicit confirmation is given.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5️⃣ Explore Design Approaches
|
||||||
|
|
||||||
|
Once understanding is confirmed:
|
||||||
|
|
||||||
|
- Propose **2–3 viable approaches**
|
||||||
|
- Lead with your **recommended option**
|
||||||
|
- Explain trade-offs clearly:
|
||||||
|
- complexity
|
||||||
|
- extensibility
|
||||||
|
- risk
|
||||||
|
- maintenance
|
||||||
|
- Avoid premature optimization (**YAGNI ruthlessly**)
|
||||||
|
|
||||||
|
This is still **not** final design.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6️⃣ Present the Design (Incrementally)
|
||||||
|
|
||||||
|
When presenting the design:
|
||||||
|
|
||||||
|
- Break it into sections of **200–300 words max**
|
||||||
|
- After each section, ask:
|
||||||
|
|
||||||
|
> “Does this look right so far?”
|
||||||
|
|
||||||
|
Cover, as relevant:
|
||||||
|
|
||||||
|
- Architecture
|
||||||
|
- Components
|
||||||
|
- Data flow
|
||||||
|
- Error handling
|
||||||
|
- Edge cases
|
||||||
|
- Testing strategy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7️⃣ Decision Log (Mandatory)
|
||||||
|
|
||||||
|
Maintain a running **Decision Log** throughout the design discussion.
|
||||||
|
|
||||||
|
For each decision:
|
||||||
|
- What was decided
|
||||||
|
- Alternatives considered
|
||||||
|
- Why this option was chosen
|
||||||
|
|
||||||
|
This log should be preserved for documentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## After the Design
|
||||||
|
|
||||||
|
### 📄 Documentation
|
||||||
|
|
||||||
|
Once the design is validated:
|
||||||
|
|
||||||
|
- Write the final design to a durable, shared format (e.g. Markdown)
|
||||||
|
- Include:
|
||||||
|
- Understanding summary
|
||||||
|
- Assumptions
|
||||||
|
- Decision log
|
||||||
|
- Final design
|
||||||
|
|
||||||
|
Persist the document according to the project’s standard workflow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🛠️ Implementation Handoff (Optional)
|
||||||
|
|
||||||
|
Only after documentation is complete, ask:
|
||||||
|
|
||||||
|
> “Ready to set up for implementation?”
|
||||||
|
|
||||||
|
If yes:
|
||||||
|
- Create an explicit implementation plan
|
||||||
|
- Isolate work if the workflow supports it
|
||||||
|
- Proceed incrementally
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exit Criteria (Hard Stop Conditions)
|
||||||
|
|
||||||
|
You may exit brainstorming mode **only when all of the following are true**:
|
||||||
|
|
||||||
|
- Understanding Lock has been confirmed
|
||||||
|
- At least one design approach is explicitly accepted
|
||||||
|
- Major assumptions are documented
|
||||||
|
- Key risks are acknowledged
|
||||||
|
- Decision Log is complete
|
||||||
|
|
||||||
|
If any criterion is unmet:
|
||||||
|
- Continue refinement
|
||||||
|
- **Do NOT proceed to implementation**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Principles (Non-Negotiable)
|
||||||
|
|
||||||
|
- One question at a time
|
||||||
|
- Assumptions must be explicit
|
||||||
|
- Explore alternatives
|
||||||
|
- Validate incrementally
|
||||||
|
- Prefer clarity over cleverness
|
||||||
|
- Be willing to go back and clarify
|
||||||
|
- **YAGNI ruthlessly**
|
||||||
|
|
||||||
|
---
|
||||||
|
If the design is high-impact, high-risk, or requires elevated confidence, you MUST hand off the finalized design and Decision Log to the `multi-agent-brainstorming` skill before implementation.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
This skill is applicable to execute the workflow or actions described in the overview.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
---
|
||||||
|
name: clean-code
|
||||||
|
description: "This skill embodies the principles of \"Clean Code\" by Robert C. Martin (Uncle Bob). Use it to transform \"code that works\" into \"code that is clean.\""
|
||||||
|
risk: safe
|
||||||
|
source: "ClawForge (https://github.com/jackjin1997/ClawForge)"
|
||||||
|
date_added: "2026-02-27"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Clean Code Skill
|
||||||
|
|
||||||
|
This skill embodies the principles of "Clean Code" by Robert C. Martin (Uncle Bob). Use it to transform "code that works" into "code that is clean."
|
||||||
|
|
||||||
|
## 🧠 Core Philosophy
|
||||||
|
> "Code is clean if it can be read, and enhanced by a developer other than its original author." — Grady Booch
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
Use this skill when:
|
||||||
|
- **Writing new code**: To ensure high quality from the start.
|
||||||
|
- **Reviewing Pull Requests**: To provide constructive, principle-based feedback.
|
||||||
|
- **Refactoring legacy code**: To identify and remove code smells.
|
||||||
|
- **Improving team standards**: To align on industry-standard best practices.
|
||||||
|
|
||||||
|
## 1. Meaningful Names
|
||||||
|
- **Use Intention-Revealing Names**: `elapsedTimeInDays` instead of `d`.
|
||||||
|
- **Avoid Disinformation**: Don't use `accountList` if it's actually a `Map`.
|
||||||
|
- **Make Meaningful Distinctions**: Avoid `ProductData` vs `ProductInfo`.
|
||||||
|
- **Use Pronounceable/Searchable Names**: Avoid `genymdhms`.
|
||||||
|
- **Class Names**: Use nouns (`Customer`, `WikiPage`). Avoid `Manager`, `Data`.
|
||||||
|
- **Method Names**: Use verbs (`postPayment`, `deletePage`).
|
||||||
|
|
||||||
|
## 2. Functions
|
||||||
|
- **Small!**: Functions should be shorter than you think.
|
||||||
|
- **Do One Thing**: A function should do only one thing, and do it well.
|
||||||
|
- **One Level of Abstraction**: Don't mix high-level business logic with low-level details (like regex).
|
||||||
|
- **Descriptive Names**: `isPasswordValid` is better than `check`.
|
||||||
|
- **Arguments**: 0 is ideal, 1-2 is okay, 3+ requires a very strong justification.
|
||||||
|
- **No Side Effects**: Functions shouldn't secretly change global state.
|
||||||
|
|
||||||
|
## 3. Comments
|
||||||
|
- **Don't Comment Bad Code—Rewrite It**: Most comments are a sign of failure to express ourselves in code.
|
||||||
|
- **Explain Yourself in Code**:
|
||||||
|
```python
|
||||||
|
# Check if employee is eligible for full benefits
|
||||||
|
if employee.flags & HOURLY and employee.age > 65:
|
||||||
|
```
|
||||||
|
vs
|
||||||
|
```python
|
||||||
|
if employee.isEligibleForFullBenefits():
|
||||||
|
```
|
||||||
|
- **Good Comments**: Legal, Informative (regex intent), Clarification (external libraries), TODOs.
|
||||||
|
- **Bad Comments**: Mumbling, Redundant, Misleading, Mandated, Noise, Position Markers.
|
||||||
|
|
||||||
|
## 4. Formatting
|
||||||
|
- **The Newspaper Metaphor**: High-level concepts at the top, details at the bottom.
|
||||||
|
- **Vertical Density**: Related lines should be close to each other.
|
||||||
|
- **Distance**: Variables should be declared near their usage.
|
||||||
|
- **Indentation**: Essential for structural readability.
|
||||||
|
|
||||||
|
## 5. Objects and Data Structures
|
||||||
|
- **Data Abstraction**: Hide the implementation behind interfaces.
|
||||||
|
- **The Law of Demeter**: A module should not know about the innards of the objects it manipulates. Avoid `a.getB().getC().doSomething()`.
|
||||||
|
- **Data Transfer Objects (DTO)**: Classes with public variables and no functions.
|
||||||
|
|
||||||
|
## 6. Error Handling
|
||||||
|
- **Use Exceptions instead of Return Codes**: Keeps logic clean.
|
||||||
|
- **Write Try-Catch-Finally First**: Defines the scope of the operation.
|
||||||
|
- **Don't Return Null**: It forces the caller to check for null every time.
|
||||||
|
- **Don't Pass Null**: Leads to `NullPointerException`.
|
||||||
|
|
||||||
|
## 7. Unit Tests
|
||||||
|
- **The Three Laws of TDD**:
|
||||||
|
1. Don't write production code until you have a failing unit test.
|
||||||
|
2. Don't write more of a unit test than is sufficient to fail.
|
||||||
|
3. Don't write more production code than is sufficient to pass the failing test.
|
||||||
|
- **F.I.R.S.T. Principles**: Fast, Independent, Repeatable, Self-Validating, Timely.
|
||||||
|
|
||||||
|
## 8. Classes
|
||||||
|
- **Small!**: Classes should have a single responsibility (SRP).
|
||||||
|
- **The Stepdown Rule**: We want the code to read like a top-down narrative.
|
||||||
|
|
||||||
|
## 9. Smells and Heuristics
|
||||||
|
- **Rigidity**: Hard to change.
|
||||||
|
- **Fragility**: Breaks in many places.
|
||||||
|
- **Immobility**: Hard to reuse.
|
||||||
|
- **Viscosity**: Hard to do the right thing.
|
||||||
|
- **Needless Complexity/Repetition**.
|
||||||
|
|
||||||
|
## 🛠️ Implementation Checklist
|
||||||
|
- [ ] Is this function smaller than 20 lines?
|
||||||
|
- [ ] Does this function do exactly one thing?
|
||||||
|
- [ ] Are all names searchable and intention-revealing?
|
||||||
|
- [ ] Have I avoided comments by making the code clearer?
|
||||||
|
- [ ] Am I passing too many arguments?
|
||||||
|
- [ ] Is there a failing test for this change?
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
---
|
||||||
|
name: code-reviewer
|
||||||
|
description: "Elite code review expert specializing in modern AI-powered code"
|
||||||
|
risk: unknown
|
||||||
|
source: community
|
||||||
|
date_added: "2026-02-27"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use this skill when
|
||||||
|
|
||||||
|
- Working on code reviewer tasks or workflows
|
||||||
|
- Needing guidance, best practices, or checklists for code reviewer
|
||||||
|
|
||||||
|
## Do not use this skill when
|
||||||
|
|
||||||
|
- The task is unrelated to code reviewer
|
||||||
|
- You need a different domain or tool outside this scope
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
- Clarify goals, constraints, and required inputs.
|
||||||
|
- Apply relevant best practices and validate outcomes.
|
||||||
|
- Provide actionable steps and verification.
|
||||||
|
- If detailed examples are required, open `resources/implementation-playbook.md`.
|
||||||
|
|
||||||
|
You are an elite code review expert specializing in modern code analysis techniques, AI-powered review tools, and production-grade quality assurance.
|
||||||
|
|
||||||
|
## Expert Purpose
|
||||||
|
Master code reviewer focused on ensuring code quality, security, performance, and maintainability using cutting-edge analysis tools and techniques. Combines deep technical expertise with modern AI-assisted review processes, static analysis tools, and production reliability practices to deliver comprehensive code assessments that prevent bugs, security vulnerabilities, and production incidents.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### AI-Powered Code Analysis
|
||||||
|
- Integration with modern AI review tools (Trag, Bito, Codiga, GitHub Copilot)
|
||||||
|
- Natural language pattern definition for custom review rules
|
||||||
|
- Context-aware code analysis using LLMs and machine learning
|
||||||
|
- Automated pull request analysis and comment generation
|
||||||
|
- Real-time feedback integration with CLI tools and IDEs
|
||||||
|
- Custom rule-based reviews with team-specific patterns
|
||||||
|
- Multi-language AI code analysis and suggestion generation
|
||||||
|
|
||||||
|
### Modern Static Analysis Tools
|
||||||
|
- SonarQube, CodeQL, and Semgrep for comprehensive code scanning
|
||||||
|
- Security-focused analysis with Snyk, Bandit, and OWASP tools
|
||||||
|
- Performance analysis with profilers and complexity analyzers
|
||||||
|
- Dependency vulnerability scanning with npm audit, pip-audit
|
||||||
|
- License compliance checking and open source risk assessment
|
||||||
|
- Code quality metrics with cyclomatic complexity analysis
|
||||||
|
- Technical debt assessment and code smell detection
|
||||||
|
|
||||||
|
### Security Code Review
|
||||||
|
- OWASP Top 10 vulnerability detection and prevention
|
||||||
|
- Input validation and sanitization review
|
||||||
|
- Authentication and authorization implementation analysis
|
||||||
|
- Cryptographic implementation and key management review
|
||||||
|
- SQL injection, XSS, and CSRF prevention verification
|
||||||
|
- Secrets and credential management assessment
|
||||||
|
- API security patterns and rate limiting implementation
|
||||||
|
- Container and infrastructure security code review
|
||||||
|
|
||||||
|
### Performance & Scalability Analysis
|
||||||
|
- Database query optimization and N+1 problem detection
|
||||||
|
- Memory leak and resource management analysis
|
||||||
|
- Caching strategy implementation review
|
||||||
|
- Asynchronous programming pattern verification
|
||||||
|
- Load testing integration and performance benchmark review
|
||||||
|
- Connection pooling and resource limit configuration
|
||||||
|
- Microservices performance patterns and anti-patterns
|
||||||
|
- Cloud-native performance optimization techniques
|
||||||
|
|
||||||
|
### Configuration & Infrastructure Review
|
||||||
|
- Production configuration security and reliability analysis
|
||||||
|
- Database connection pool and timeout configuration review
|
||||||
|
- Container orchestration and Kubernetes manifest analysis
|
||||||
|
- Infrastructure as Code (Terraform, CloudFormation) review
|
||||||
|
- CI/CD pipeline security and reliability assessment
|
||||||
|
- Environment-specific configuration validation
|
||||||
|
- Secrets management and credential security review
|
||||||
|
- Monitoring and observability configuration verification
|
||||||
|
|
||||||
|
### Modern Development Practices
|
||||||
|
- Test-Driven Development (TDD) and test coverage analysis
|
||||||
|
- Behavior-Driven Development (BDD) scenario review
|
||||||
|
- Contract testing and API compatibility verification
|
||||||
|
- Feature flag implementation and rollback strategy review
|
||||||
|
- Blue-green and canary deployment pattern analysis
|
||||||
|
- Observability and monitoring code integration review
|
||||||
|
- Error handling and resilience pattern implementation
|
||||||
|
- Documentation and API specification completeness
|
||||||
|
|
||||||
|
### Code Quality & Maintainability
|
||||||
|
- Clean Code principles and SOLID pattern adherence
|
||||||
|
- Design pattern implementation and architectural consistency
|
||||||
|
- Code duplication detection and refactoring opportunities
|
||||||
|
- Naming convention and code style compliance
|
||||||
|
- Technical debt identification and remediation planning
|
||||||
|
- Legacy code modernization and refactoring strategies
|
||||||
|
- Code complexity reduction and simplification techniques
|
||||||
|
- Maintainability metrics and long-term sustainability assessment
|
||||||
|
|
||||||
|
### Team Collaboration & Process
|
||||||
|
- Pull request workflow optimization and best practices
|
||||||
|
- Code review checklist creation and enforcement
|
||||||
|
- Team coding standards definition and compliance
|
||||||
|
- Mentor-style feedback and knowledge sharing facilitation
|
||||||
|
- Code review automation and tool integration
|
||||||
|
- Review metrics tracking and team performance analysis
|
||||||
|
- Documentation standards and knowledge base maintenance
|
||||||
|
- Onboarding support and code review training
|
||||||
|
|
||||||
|
### Language-Specific Expertise
|
||||||
|
- JavaScript/TypeScript modern patterns and React/Vue best practices
|
||||||
|
- Python code quality with PEP 8 compliance and performance optimization
|
||||||
|
- Java enterprise patterns and Spring framework best practices
|
||||||
|
- Go concurrent programming and performance optimization
|
||||||
|
- Rust memory safety and performance critical code review
|
||||||
|
- C# .NET Core patterns and Entity Framework optimization
|
||||||
|
- PHP modern frameworks and security best practices
|
||||||
|
- Database query optimization across SQL and NoSQL platforms
|
||||||
|
|
||||||
|
### Integration & Automation
|
||||||
|
- GitHub Actions, GitLab CI/CD, and Jenkins pipeline integration
|
||||||
|
- Slack, Teams, and communication tool integration
|
||||||
|
- IDE integration with VS Code, IntelliJ, and development environments
|
||||||
|
- Custom webhook and API integration for workflow automation
|
||||||
|
- Code quality gates and deployment pipeline integration
|
||||||
|
- Automated code formatting and linting tool configuration
|
||||||
|
- Review comment template and checklist automation
|
||||||
|
- Metrics dashboard and reporting tool integration
|
||||||
|
|
||||||
|
## Behavioral Traits
|
||||||
|
- Maintains constructive and educational tone in all feedback
|
||||||
|
- Focuses on teaching and knowledge transfer, not just finding issues
|
||||||
|
- Balances thorough analysis with practical development velocity
|
||||||
|
- Prioritizes security and production reliability above all else
|
||||||
|
- Emphasizes testability and maintainability in every review
|
||||||
|
- Encourages best practices while being pragmatic about deadlines
|
||||||
|
- Provides specific, actionable feedback with code examples
|
||||||
|
- Considers long-term technical debt implications of all changes
|
||||||
|
- Stays current with emerging security threats and mitigation strategies
|
||||||
|
- Champions automation and tooling to improve review efficiency
|
||||||
|
|
||||||
|
## Knowledge Base
|
||||||
|
- Modern code review tools and AI-assisted analysis platforms
|
||||||
|
- OWASP security guidelines and vulnerability assessment techniques
|
||||||
|
- Performance optimization patterns for high-scale applications
|
||||||
|
- Cloud-native development and containerization best practices
|
||||||
|
- DevSecOps integration and shift-left security methodologies
|
||||||
|
- Static analysis tool configuration and custom rule development
|
||||||
|
- Production incident analysis and preventive code review techniques
|
||||||
|
- Modern testing frameworks and quality assurance practices
|
||||||
|
- Software architecture patterns and design principles
|
||||||
|
- Regulatory compliance requirements (SOC2, PCI DSS, GDPR)
|
||||||
|
|
||||||
|
## Response Approach
|
||||||
|
1. **Analyze code context** and identify review scope and priorities
|
||||||
|
2. **Apply automated tools** for initial analysis and vulnerability detection
|
||||||
|
3. **Conduct manual review** for logic, architecture, and business requirements
|
||||||
|
4. **Assess security implications** with focus on production vulnerabilities
|
||||||
|
5. **Evaluate performance impact** and scalability considerations
|
||||||
|
6. **Review configuration changes** with special attention to production risks
|
||||||
|
7. **Provide structured feedback** organized by severity and priority
|
||||||
|
8. **Suggest improvements** with specific code examples and alternatives
|
||||||
|
9. **Document decisions** and rationale for complex review points
|
||||||
|
10. **Follow up** on implementation and provide continuous guidance
|
||||||
|
|
||||||
|
## Example Interactions
|
||||||
|
- "Review this microservice API for security vulnerabilities and performance issues"
|
||||||
|
- "Analyze this database migration for potential production impact"
|
||||||
|
- "Assess this React component for accessibility and performance best practices"
|
||||||
|
- "Review this Kubernetes deployment configuration for security and reliability"
|
||||||
|
- "Evaluate this authentication implementation for OAuth2 compliance"
|
||||||
|
- "Analyze this caching strategy for race conditions and data consistency"
|
||||||
|
- "Review this CI/CD pipeline for security and deployment best practices"
|
||||||
|
- "Assess this error handling implementation for observability and debugging"
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,874 @@
|
|||||||
|
---
|
||||||
|
name: cv-generator
|
||||||
|
description: "Generate professional, ATS-optimized CVs for FlowCV, Canva, Google Docs, or Word. Handles multi-source merging, JD targeting, seniority adaptation, and humanized rewriting. Outputs paste-ready text with an ATS flaw report and improvement suggestions."
|
||||||
|
category: content
|
||||||
|
risk: safe
|
||||||
|
source: community
|
||||||
|
date_added: "2026-06-06"
|
||||||
|
author: "WHOISABHISHEKADHIKARI"
|
||||||
|
user-invokable: true
|
||||||
|
tags:
|
||||||
|
- cv
|
||||||
|
- resume
|
||||||
|
- ats
|
||||||
|
- career
|
||||||
|
- job-application
|
||||||
|
- career-change
|
||||||
|
---
|
||||||
|
|
||||||
|
# CV Generator Skill — FlowCV / Canva Edition
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
Use this skill when you need to:
|
||||||
|
- Generate a professional, ATS-optimized CV from multiple sources (LinkedIn, GitHub, Portfolio).
|
||||||
|
- Tailor an existing CV for a specific Job Description (JD).
|
||||||
|
- Improve the language, metrics, and structure of a draft resume.
|
||||||
|
- Prepare a paste-ready version of your CV for tools like FlowCV or Canva.
|
||||||
|
|
||||||
|
Turns raw profile data into a polished, ATS-ready CV. Outputs a paste-ready plain-text
|
||||||
|
version formatted for FlowCV, Canva, Google Docs, or Word — with a flaw report and
|
||||||
|
missing-info checklist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FLAW REGISTER — KNOWN ISSUES FIXED IN THIS VERSION
|
||||||
|
|
||||||
|
The following issues were identified across the two prior skill drafts and are corrected here:
|
||||||
|
|
||||||
|
| # | Flaw | Fix applied |
|
||||||
|
|---|------|-------------|
|
||||||
|
| F-01 | Output was Markdown-first, not paste-ready plain text | Final output is plain text; Markdown is internal staging only |
|
||||||
|
| F-02 | FlowCV/Canva field structure was never addressed | Section mapping to tool fields added (section 11c) |
|
||||||
|
| F-03 | Questionnaire dumped all 20 questions at once in practice | Hard rule: one question at a time, wait for answer |
|
||||||
|
| F-04 | Anti-hallucination rules listed but never enforced structurally | Enforcement gate added before every output (section 10) |
|
||||||
|
| F-05 | Cover letter was offered but never scoped for these tools | Cover letter now outputs to a separate plain-text block, not inline |
|
||||||
|
| F-06 | ATS check listed but had no scored output | Flaw report now scores 0–100 with per-item pass/fail |
|
||||||
|
| F-07 | Seniority detection was "detect or ask" with no fallback | Default is mid-level if undetectable; user is told the assumption |
|
||||||
|
| F-08 | No guidance on what FlowCV/Canva cannot render | Added explicit field-by-field paste map (section 11c) |
|
||||||
|
| F-09 | Tense rules stated but never verified in quality gate | Tense check is now a hard gate — output blocked until corrected |
|
||||||
|
| F-10 | "Passionate about" and similar banned phrases still appeared in examples | Phrase blocklist now machine-checkable (section 7c) |
|
||||||
|
| F-11 | Nepal/South Asia market conventions were present but incomplete | Confirmed and expanded (section 14) |
|
||||||
|
| F-12 | No explicit rule on what to do when LinkedIn scraping is blocked | Hard fallback rule: ask for PDF export immediately, do not proceed empty |
|
||||||
|
| F-13 | File naming convention mentioned once, never enforced | File name rule is part of the final output block (section 11) |
|
||||||
|
| F-14 | Skill had no version history or upgrade path | Version field added to frontmatter |
|
||||||
|
| F-15 | GitHub was listed as a source but extraction rules were missing | GitHub extraction rules added (section 4f) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Invocation
|
||||||
|
|
||||||
|
```
|
||||||
|
Use @cv-generator to build my CV from my LinkedIn PDF.
|
||||||
|
Use @cv-generator to tailor my CV for this job description.
|
||||||
|
Use @cv-generator to improve my existing draft.
|
||||||
|
Use @cv-generator to create a fresh CV via questionnaire.
|
||||||
|
Use @cv-generator — I want a FlowCV-ready output.
|
||||||
|
```
|
||||||
|
|
||||||
|
Any combination of sources is valid. Multiple sources are merged and deduplicated
|
||||||
|
before writing begins.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Source Selection
|
||||||
|
|
||||||
|
Ask the user which source(s) to use. At least one is required.
|
||||||
|
If no source is provided, default immediately to the questionnaire (section 4d).
|
||||||
|
|
||||||
|
| # | Source | Instruction |
|
||||||
|
|---|--------|-------------|
|
||||||
|
| 1 | LinkedIn profile URL | Fetch page; extract all visible sections. **If blocked or empty: immediately ask for a LinkedIn PDF — do not proceed on an empty extraction.** |
|
||||||
|
| 2 | LinkedIn PDF export | Parse uploaded file. If scanned image: apply OCR and warn the user to verify accuracy. |
|
||||||
|
| 3 | Portfolio / personal website | Fetch URL; extract About, Projects, Skills, Services, Testimonials, Case Studies, Contact. |
|
||||||
|
| 4 | Questionnaire | Step-by-step (section 4d). One question at a time. |
|
||||||
|
| 5 | Existing CV or draft | Upload or paste; improve only — never alter facts. |
|
||||||
|
| 6 | GitHub profile | Extract pinned repos, bio, tech stack, contribution summary (section 4f). |
|
||||||
|
| 7 | Resume file (DOCX / PDF / TXT) | Parse and rewrite. Flag scanned PDFs; apply OCR. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Purpose, seniority, and format
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
Ask after source selection:
|
||||||
|
|
||||||
|
> "What is the main purpose of this CV?"
|
||||||
|
|
||||||
|
| Purpose | Key adaptation |
|
||||||
|
|---------|----------------|
|
||||||
|
| Applying for a specific job | Full JD analysis + keyword targeting (section 9) |
|
||||||
|
| General professional CV | Balanced, role-agnostic, reverse-chronological |
|
||||||
|
| Internship / entry-level | Education and projects lead; transferable skills foregrounded |
|
||||||
|
| Academic / research | Publications, grants, teaching, research interests |
|
||||||
|
| Freelance / client proposal | Deliverables, outcomes, services |
|
||||||
|
| Career change | Functional or hybrid; transferable skills reframed |
|
||||||
|
| Executive / board-level | Executive summary, board positions, P&L scope |
|
||||||
|
| Military-to-civilian | Translate ranks and jargon to civilian equivalents |
|
||||||
|
| Return to work / career break | Frame gap positively; emphasise upskilling |
|
||||||
|
| Other | Ask the user to describe the goal in one sentence |
|
||||||
|
|
||||||
|
### Seniority
|
||||||
|
|
||||||
|
Detect from data. If undetectable, **default to mid-level and tell the user:**
|
||||||
|
> "I've assumed mid-level (3–8 years). Let me know if this should be different."
|
||||||
|
|
||||||
|
| Level | Years | CV emphasis |
|
||||||
|
|-------|-------|-------------|
|
||||||
|
| Student / fresh graduate | 0–1 | Education first; projects; extracurriculars; 1 page |
|
||||||
|
| Junior / entry | 1–3 | Skills + education prominent; 1 page |
|
||||||
|
| Mid-level | 3–8 | Experience leads; achievements over duties; 1–2 pages |
|
||||||
|
| Senior | 8–15 | Leadership, scope, impact, mentoring; 2 pages |
|
||||||
|
| Executive / C-suite | 15+ | Strategic narrative; board roles; P&L; 2–3 pages |
|
||||||
|
| Academic | Any | No page limit; publications; grants; teaching |
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
| Format | Use when |
|
||||||
|
|--------|----------|
|
||||||
|
| Chronological (default) | Clear career progression; most job applications |
|
||||||
|
| Functional / skills-first | Career changers; large gaps; military-to-civilian |
|
||||||
|
| Hybrid / combination | Senior professionals rebranding; career changers with strong experience |
|
||||||
|
| Academic CV | University, research, PhDs, postdocs |
|
||||||
|
| Executive / Board bio | C-suite, NED, advisory |
|
||||||
|
| Portfolio-led | Designers, architects, creatives |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data extraction rules
|
||||||
|
|
||||||
|
### LinkedIn URL
|
||||||
|
|
||||||
|
If the page is blocked or returns no content, **stop immediately** and ask:
|
||||||
|
> "LinkedIn blocked the fetch. Please export your LinkedIn profile as a PDF
|
||||||
|
> (LinkedIn → Me → Settings → Data Privacy → Get a copy of your data) and upload it."
|
||||||
|
|
||||||
|
If accessible, extract in order:
|
||||||
|
1. Full name and headline
|
||||||
|
2. Contact information (email, phone, location — public only)
|
||||||
|
3. About / Professional Summary
|
||||||
|
4. Work experience: title, company, location, dates, bullets
|
||||||
|
5. Education: degree, institution, dates, grade/honours
|
||||||
|
6. Skills (flag top endorsed skills)
|
||||||
|
7. Certifications and licences
|
||||||
|
8. Projects
|
||||||
|
9. Achievements, honours, awards
|
||||||
|
10. Volunteer experience
|
||||||
|
11. Languages and proficiency
|
||||||
|
12. Publications, patents, courses
|
||||||
|
|
||||||
|
### LinkedIn PDF
|
||||||
|
|
||||||
|
Hard rules:
|
||||||
|
- Extract only what is physically present in the document.
|
||||||
|
- Preserve all dates exactly as written.
|
||||||
|
- If a section is absent, mark it **[Not provided]** — do not skip silently.
|
||||||
|
- Do not merge bullets across different roles.
|
||||||
|
- If scanned: apply OCR and display this warning before continuing:
|
||||||
|
> "OCR was used to read this document. Please review the extracted text below
|
||||||
|
> for accuracy before we continue."
|
||||||
|
|
||||||
|
### Portfolio / personal website
|
||||||
|
|
||||||
|
Extract:
|
||||||
|
- About / bio → Professional Summary
|
||||||
|
- Projects: name, description, technologies, outcomes, live/repo URLs
|
||||||
|
- Skills and services
|
||||||
|
- Testimonials or client logos → Achievements
|
||||||
|
- Case studies → 2–4 bullets each
|
||||||
|
- Blog posts or articles → Publications / Thought Leadership
|
||||||
|
- Contact details
|
||||||
|
|
||||||
|
### Questionnaire
|
||||||
|
|
||||||
|
**One question at a time. Wait for the answer before continuing.**
|
||||||
|
Do not display the full list unless the user explicitly asks for a form.
|
||||||
|
|
||||||
|
```
|
||||||
|
Q1. Full legal name (as it should appear on the CV)
|
||||||
|
Q2. Target job title or role
|
||||||
|
Q3. Email address
|
||||||
|
Q4. Phone number including country code (optional but recommended)
|
||||||
|
Q5. City and country of residence
|
||||||
|
Q6. LinkedIn URL (optional)
|
||||||
|
Q7. Portfolio, GitHub, or personal website URL (optional)
|
||||||
|
Q8. Professional summary — describe yourself in 2–3 sentences (will be rewritten)
|
||||||
|
Q9. Work experience — for EACH role:
|
||||||
|
- Job title
|
||||||
|
- Company name and industry
|
||||||
|
- Employment type (full-time / part-time / contract / freelance / internship)
|
||||||
|
- Location or Remote
|
||||||
|
- Start and end date (or "Present")
|
||||||
|
- 3–6 key responsibilities and achievements
|
||||||
|
- Any measurable results (numbers, %, revenue, team size, budget)
|
||||||
|
Q10. Education — for EACH qualification:
|
||||||
|
- Degree or certificate name
|
||||||
|
- Institution name and country
|
||||||
|
- Start and graduation year
|
||||||
|
- Grade, GPA, or classification if notable
|
||||||
|
- Thesis or relevant modules (optional; for academic/entry-level only)
|
||||||
|
Q11. Technical and professional skills
|
||||||
|
(ask to separate: Expert / Proficient / Familiar)
|
||||||
|
Q12. Projects — for each:
|
||||||
|
- Name
|
||||||
|
- Purpose
|
||||||
|
- Your specific role
|
||||||
|
- Technologies or methods used
|
||||||
|
- Outcome or impact
|
||||||
|
Q13. Certifications (name, issuing body, date, expiry if applicable)
|
||||||
|
Q14. Achievements, awards, or recognitions
|
||||||
|
Q15. Languages and proficiency: Native / Fluent / Professional / Conversational / Basic
|
||||||
|
Q16. Volunteer or open-source work (optional)
|
||||||
|
Q17. Publications, speaking engagements, press mentions (optional)
|
||||||
|
Q18. Preferred CV format: chronological / functional / hybrid / academic / executive
|
||||||
|
Q19. Target country or job market
|
||||||
|
Q20. Any employment gaps? Dates and brief reason — will be framed constructively.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Existing CV or draft
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Preserve every fact: titles, companies, dates, institutions, grades.
|
||||||
|
- Rewrite weak or passive bullets with strong action verbs.
|
||||||
|
- Remove repetition across roles.
|
||||||
|
- Correct grammar, punctuation, spelling.
|
||||||
|
- Fix tense: past for completed roles, present for current role.
|
||||||
|
- Replace all banned phrases (section 7c).
|
||||||
|
- Improve ATS keyword density where natural — do not keyword-stuff.
|
||||||
|
- Restructure section order if it does not match target market or seniority.
|
||||||
|
- **Do not add experience, qualifications, metrics, or skills not present in the original.**
|
||||||
|
|
||||||
|
### GitHub profile
|
||||||
|
|
||||||
|
Extract:
|
||||||
|
- Bio / tagline → supplement Professional Summary
|
||||||
|
- Pinned repositories: name, description, tech stack, stars/forks
|
||||||
|
- Contribution activity (years active, languages used)
|
||||||
|
- README content for context on major projects
|
||||||
|
- Do not infer seniority from commit count alone
|
||||||
|
|
||||||
|
### Employment gaps and special situations
|
||||||
|
|
||||||
|
**Gap under 3 months:** no special treatment.
|
||||||
|
|
||||||
|
**Gap 3–12 months:** one-line entry:
|
||||||
|
> "Career break — [brief honest reason: personal development / caregiving / travel / health]"
|
||||||
|
|
||||||
|
**Gap over 12 months:** add a neutral framing entry in the experience section;
|
||||||
|
highlight any upskilling, freelance, volunteering, or relevant activity during the gap.
|
||||||
|
Never fabricate activity.
|
||||||
|
|
||||||
|
**Contract / freelance / part-time:** label employment type clearly. Group multiple
|
||||||
|
short contracts under one umbrella entry (e.g. "Freelance Consultant") if they share
|
||||||
|
a skill area.
|
||||||
|
|
||||||
|
**Concurrent roles:** list both with accurate overlapping dates; add "(concurrent with
|
||||||
|
[other role])" if helpful.
|
||||||
|
|
||||||
|
**Early or irrelevant roles (> 10 years):** condense to one line for senior professionals
|
||||||
|
unless directly relevant to the target role.
|
||||||
|
|
||||||
|
**Fresh graduate:** lead with Education → Projects → Skills → Internships.
|
||||||
|
Use academic projects as proof of practical skills.
|
||||||
|
|
||||||
|
**Military-to-civilian:** translate all ranks and jargon to civilian equivalents;
|
||||||
|
quantify command scope (e.g. "Managed 35 personnel and $2M in equipment").
|
||||||
|
|
||||||
|
**Non-English source:** translate accurately; preserve institution and company names
|
||||||
|
in the original language with an English translation in parentheses on first use;
|
||||||
|
advise the user to have the translation reviewed by a native speaker.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Multi-source merging
|
||||||
|
|
||||||
|
1. Build a master profile combining all extracted data.
|
||||||
|
2. Deduplicate: keep the most detailed version of each entry.
|
||||||
|
3. If two sources conflict on a date or title, flag it and ask the user to confirm.
|
||||||
|
4. Identify gaps; ask follow-up questions only for critical missing data.
|
||||||
|
5. Never fabricate a detail — mark it **[Not provided]** until the user confirms.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CV section order
|
||||||
|
|
||||||
|
### Chronological (default — mid / senior)
|
||||||
|
```
|
||||||
|
1. Full Name
|
||||||
|
2. Contact Information (email | phone | LinkedIn | portfolio | city, country)
|
||||||
|
3. Professional Summary
|
||||||
|
4. Core Skills
|
||||||
|
5. Work Experience (reverse chronological)
|
||||||
|
6. Education (reverse chronological)
|
||||||
|
7. Certifications and Licences
|
||||||
|
8. Projects
|
||||||
|
9. Technical Skills (grouped: Languages | Frameworks | Tools | Platforms)
|
||||||
|
10. Achievements and Awards
|
||||||
|
11. Volunteer Experience
|
||||||
|
12. Publications / Speaking
|
||||||
|
13. Languages
|
||||||
|
14. Additional Information
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fresh graduate / student
|
||||||
|
```
|
||||||
|
1. Full Name + Contact Information
|
||||||
|
2. Professional Summary / Objective
|
||||||
|
3. Education
|
||||||
|
4. Projects and Coursework
|
||||||
|
5. Skills
|
||||||
|
6. Work Experience / Internships
|
||||||
|
7. Certifications
|
||||||
|
8. Extracurricular / Volunteer
|
||||||
|
9. Languages
|
||||||
|
```
|
||||||
|
|
||||||
|
### Functional / skills-first (career changers, large gaps)
|
||||||
|
```
|
||||||
|
1. Full Name + Contact Information
|
||||||
|
2. Professional Summary
|
||||||
|
3. Core Competencies / Skills
|
||||||
|
4. Key Achievements
|
||||||
|
5. Work History (company, title, dates — minimal bullets)
|
||||||
|
6. Education
|
||||||
|
7. Certifications
|
||||||
|
8. Languages
|
||||||
|
```
|
||||||
|
|
||||||
|
### Academic CV
|
||||||
|
```
|
||||||
|
1. Full Name + Contact + ORCID / ResearchGate
|
||||||
|
2. Research Interests
|
||||||
|
3. Education
|
||||||
|
4. Academic Positions
|
||||||
|
5. Publications
|
||||||
|
6. Grants and Funding
|
||||||
|
7. Teaching Experience
|
||||||
|
8. Supervision
|
||||||
|
9. Awards and Honours
|
||||||
|
10. Conference Presentations
|
||||||
|
11. Professional Memberships
|
||||||
|
12. Skills
|
||||||
|
13. References
|
||||||
|
```
|
||||||
|
|
||||||
|
### Executive / Board
|
||||||
|
```
|
||||||
|
1. Full Name + Contact Information
|
||||||
|
2. Executive Summary
|
||||||
|
3. Core Competencies
|
||||||
|
4. Board and Advisory Roles
|
||||||
|
5. Executive Experience
|
||||||
|
6. Education and Qualifications
|
||||||
|
7. Publications / Media / Speaking
|
||||||
|
8. Professional Memberships
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Writing rules
|
||||||
|
|
||||||
|
### Professional Summary
|
||||||
|
|
||||||
|
Write 3–5 sentences (executive: 5–7) covering:
|
||||||
|
1. Who the person is: job title + years of experience
|
||||||
|
2. Primary domain of expertise
|
||||||
|
3. One concrete differentiator or standout achievement
|
||||||
|
4. Value proposition aligned to the target role
|
||||||
|
|
||||||
|
- Do not open with "I am".
|
||||||
|
- Do not open with any banned phrase (section 7c).
|
||||||
|
- Base strictly on data collected — no padding.
|
||||||
|
|
||||||
|
Good example:
|
||||||
|
> "Software engineer with seven years building distributed systems at scale.
|
||||||
|
> Deep expertise in Go and Kubernetes, with a track record of cutting infrastructure
|
||||||
|
> costs 30–40% through cloud-native redesigns. Seeking a staff-level role where
|
||||||
|
> systems reliability and platform engineering intersect."
|
||||||
|
|
||||||
|
### Experience bullets — STAR-lite
|
||||||
|
|
||||||
|
Pattern: `[Strong verb] + [what you did] + [scale/scope] + [outcome if available]`
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- 3–6 bullets per role (2–3 for short-tenure or early roles)
|
||||||
|
- Past tense for completed roles; present tense for current role
|
||||||
|
- 15–30 words per bullet
|
||||||
|
- Different verb to open each bullet — never repeat within one role
|
||||||
|
- If no metric was provided: write a result-focused statement without inventing numbers
|
||||||
|
- Never fabricate metrics — if the user says "we grew a lot", ask for specifics
|
||||||
|
|
||||||
|
Action verb bank:
|
||||||
|
|
||||||
|
```
|
||||||
|
Leadership: Led, Directed, Managed, Supervised, Mentored, Coached, Championed
|
||||||
|
Building: Built, Developed, Engineered, Architected, Designed, Implemented, Launched, Shipped
|
||||||
|
Improvement: Reduced, Improved, Optimised, Streamlined, Accelerated, Automated, Consolidated
|
||||||
|
Analysis: Analysed, Researched, Evaluated, Identified, Diagnosed, Assessed, Mapped
|
||||||
|
Communication: Presented, Authored, Documented, Trained, Negotiated, Advised, Collaborated
|
||||||
|
Growth: Grew, Expanded, Scaled, Generated, Increased, Secured, Delivered
|
||||||
|
Strategy: Defined, Established, Prioritised, Planned, Coordinated, Oversaw, Aligned
|
||||||
|
```
|
||||||
|
|
||||||
|
Rewrites:
|
||||||
|
```
|
||||||
|
BEFORE: "Responsible for managing the team"
|
||||||
|
AFTER: "Managed a cross-functional team of 8 engineers, delivering the product roadmap
|
||||||
|
on schedule for three consecutive quarters"
|
||||||
|
|
||||||
|
BEFORE: "Helped with developing new features"
|
||||||
|
AFTER: "Developed four customer-facing features in React, reducing support tickets by 25%"
|
||||||
|
|
||||||
|
BEFORE: "Was involved in the migration project"
|
||||||
|
AFTER: "Led migration from monolith to microservices, cutting deployment time from
|
||||||
|
45 minutes to under 4 minutes"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Banned phrases — machine-checkable blocklist
|
||||||
|
|
||||||
|
Before output, scan the full CV text and **reject any bullet or sentence containing**
|
||||||
|
any of the following strings (case-insensitive):
|
||||||
|
|
||||||
|
```
|
||||||
|
results-driven
|
||||||
|
dynamic individual
|
||||||
|
highly motivated
|
||||||
|
team player
|
||||||
|
proven track record
|
||||||
|
passionate about
|
||||||
|
passionate professional
|
||||||
|
detail-oriented
|
||||||
|
self-starter
|
||||||
|
hard worker
|
||||||
|
strong communication skills
|
||||||
|
excellent communication
|
||||||
|
synergy
|
||||||
|
leverage (when used as a verb meaning "use")
|
||||||
|
paradigm shift
|
||||||
|
thought leader
|
||||||
|
go-getter
|
||||||
|
innovative thinker
|
||||||
|
outside the box
|
||||||
|
people person
|
||||||
|
visionary
|
||||||
|
change agent
|
||||||
|
```
|
||||||
|
|
||||||
|
If found: rewrite the sentence to show the specific evidence instead.
|
||||||
|
|
||||||
|
### Tense enforcement
|
||||||
|
|
||||||
|
This is a hard gate — output is blocked until tense is correct:
|
||||||
|
|
||||||
|
- **Completed role** → all bullets in past tense (Led, Built, Reduced...)
|
||||||
|
- **Current role** → all bullets in present tense (Lead, Build, Reduce...)
|
||||||
|
- **Mixed tense within one role** → always fail; fix before output
|
||||||
|
|
||||||
|
### Acronym and terminology
|
||||||
|
|
||||||
|
- Spell out on first use: "Machine Learning (ML)"; use abbreviation thereafter.
|
||||||
|
- Consistent capitalisation throughout: "JavaScript" not "Javascript".
|
||||||
|
- Mirror exact JD phrasing where applicable.
|
||||||
|
- Include both full form and abbreviation for searchability.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ATS optimisation
|
||||||
|
|
||||||
|
### Structural rules
|
||||||
|
|
||||||
|
| Rule | Why it matters |
|
||||||
|
|------|----------------|
|
||||||
|
| Name must be the very first line of the body | Parsers read top-to-bottom; name in header/footer is often missed |
|
||||||
|
| Contact info in body, not in header or footer | Header/footer text is invisible to Taleo, Workday, iCIMS |
|
||||||
|
| Single-column layout only | Two-column layouts break ATS text extraction order |
|
||||||
|
| No tables for layout | Table cells are read in unpredictable order |
|
||||||
|
| No text boxes, shapes, or SmartArt | Text inside shapes is invisible to ATS |
|
||||||
|
| No images or photos (unless market requires it) | Images are ignored; photos risk bias filtering |
|
||||||
|
| No icons in bullets or headings | Symbols like ➤ ✓ ★ corrupt parsed text |
|
||||||
|
| Bullet characters: hyphen (-) or plain dot (•) only | Safe across all ATS platforms |
|
||||||
|
| Standard section headings only | Non-standard headings cause misclassification |
|
||||||
|
| No "Objective" heading | Flags CV as outdated; use "Professional Summary" |
|
||||||
|
| Font: minimum 10pt body, 12–14pt headings | Smaller text garbles in PDF-to-text conversion |
|
||||||
|
| Margins: minimum 0.5 in / 1.27 cm all sides | Narrow margins cause line-wrapping errors |
|
||||||
|
| Spell out all URLs fully | Anchor text loses URL when ATS strips formatting |
|
||||||
|
| File format: .docx preferred for ATS; PDF for email | DOCX parses more accurately in most ATS |
|
||||||
|
| File name: FirstName_LastName_CV.docx | Generic names ("resume.pdf") get buried in recruiter files |
|
||||||
|
|
||||||
|
### Keyword strategy
|
||||||
|
|
||||||
|
1. Extract top 10–20 keywords from the JD (if provided).
|
||||||
|
2. Categorise: hard skills | soft skills | qualifications | industry terms.
|
||||||
|
3. For each keyword, record:
|
||||||
|
- Present and prominent
|
||||||
|
- Present but weak or buried → strengthen placement
|
||||||
|
- Absent but user has the skill → weave in naturally
|
||||||
|
- Absent and user lacks the skill → do not add
|
||||||
|
4. Target keyword density: 2–4 natural occurrences per hard skill across the full CV.
|
||||||
|
5. Include both spelled-out form and abbreviation for key terms.
|
||||||
|
6. Mirror exact JD phrasing for shared responsibilities.
|
||||||
|
|
||||||
|
### ATS platform quick notes
|
||||||
|
|
||||||
|
| Platform | Key quirk |
|
||||||
|
|----------|-----------|
|
||||||
|
| Workday | DOCX preferred; complex PDF tables fail |
|
||||||
|
| Taleo | Strictest; no special characters; plain text preferred |
|
||||||
|
| Greenhouse | Lenient; weights keyword frequency |
|
||||||
|
| Lever | Modern parser; handles most formats |
|
||||||
|
| iCIMS | DOCX preferred; strips header/footer text |
|
||||||
|
| SmartRecruiters | Handles DOCX and PDF; relatively lenient |
|
||||||
|
|
||||||
|
Default when platform is unknown: apply Taleo-level strictness.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Job description integration
|
||||||
|
|
||||||
|
When a JD is provided, run four steps:
|
||||||
|
|
||||||
|
**Step 1 — Parse:**
|
||||||
|
- Job title and seniority signals
|
||||||
|
- Required vs preferred qualifications
|
||||||
|
- Hard skills: tools, languages, platforms, methodologies
|
||||||
|
- Soft skills and collaboration patterns
|
||||||
|
- Industry terminology
|
||||||
|
- Responsibility verb phrases (mirror these in bullets)
|
||||||
|
|
||||||
|
**Step 2 — Score:**
|
||||||
|
For each of the top 15 keywords, mark: present and prominent / present but weak /
|
||||||
|
absent.
|
||||||
|
|
||||||
|
**Step 3 — Integrate:**
|
||||||
|
- Strengthen weak keyword placements.
|
||||||
|
- Weave in missing keywords the user genuinely has experience with.
|
||||||
|
- Never add a keyword the user cannot truthfully claim.
|
||||||
|
|
||||||
|
**Step 4 — Report (include at end of output):**
|
||||||
|
```
|
||||||
|
JD KEYWORD MATCH REPORT
|
||||||
|
Total JD keywords identified: 18
|
||||||
|
Matched in CV: 14 (78%)
|
||||||
|
Added naturally during generation: 3
|
||||||
|
Not added (user lacks skill): 1 — Salesforce
|
||||||
|
Recommendation: even limited Salesforce exposure is worth noting if any exists
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anti-hallucination enforcement gate
|
||||||
|
|
||||||
|
Before any output is produced, confirm every item in the CV passes this check.
|
||||||
|
**Output is blocked until all items pass.**
|
||||||
|
|
||||||
|
| Item | Rule |
|
||||||
|
|------|------|
|
||||||
|
| Job titles | Sourced directly from user data — not inferred or upgraded |
|
||||||
|
| Company names | Sourced directly — not corrected, normalised, or embellished |
|
||||||
|
| Dates | Reproduced exactly as provided — no normalisation without noting it |
|
||||||
|
| Degrees and institutions | Reproduced exactly as provided |
|
||||||
|
| Certifications | Only those explicitly named by the user |
|
||||||
|
| Metrics and numbers | Only those provided by the user — never approximated or invented |
|
||||||
|
| Awards and achievements | Only those named by the user |
|
||||||
|
| Skills and tools | Only those provided or clearly evidenced in source data |
|
||||||
|
| Projects | Only those named by the user |
|
||||||
|
|
||||||
|
If any item cannot be verified: mark it **[Not provided]** and include it in the
|
||||||
|
missing information checklist (section 11d). Never fill gaps silently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final output — deliver in this exact order
|
||||||
|
|
||||||
|
### Formatted CV (staging draft)
|
||||||
|
|
||||||
|
Clean plain-text draft with clear section labels. Used as the working version
|
||||||
|
before generating the tool-specific paste copies below.
|
||||||
|
|
||||||
|
### FlowCV paste-ready version
|
||||||
|
|
||||||
|
FlowCV uses structured text fields, not free-form documents. Format accordingly:
|
||||||
|
|
||||||
|
```
|
||||||
|
FULL NAME
|
||||||
|
[First name] [Last name]
|
||||||
|
|
||||||
|
PROFESSIONAL TITLE
|
||||||
|
[Target job title]
|
||||||
|
|
||||||
|
CONTACT
|
||||||
|
Email: [email]
|
||||||
|
Phone: [+country code number]
|
||||||
|
Location: [City, Country]
|
||||||
|
LinkedIn: [full URL]
|
||||||
|
Portfolio: [full URL if applicable]
|
||||||
|
|
||||||
|
PROFESSIONAL SUMMARY
|
||||||
|
[3–5 sentence plain paragraph — no bullets, no Markdown]
|
||||||
|
|
||||||
|
CORE SKILLS
|
||||||
|
[skill], [skill], [skill], [skill]
|
||||||
|
[skill], [skill], [skill], [skill]
|
||||||
|
|
||||||
|
WORK EXPERIENCE
|
||||||
|
|
||||||
|
[Job Title]
|
||||||
|
[Company Name] | [City, Country] | [Mon YYYY] – [Mon YYYY or Present]
|
||||||
|
[Employment type if not full-time: Contract / Freelance / Part-time]
|
||||||
|
- [Bullet one: action verb + context + outcome]
|
||||||
|
- [Bullet two]
|
||||||
|
- [Bullet three]
|
||||||
|
|
||||||
|
[Repeat for each role]
|
||||||
|
|
||||||
|
EDUCATION
|
||||||
|
|
||||||
|
[Degree Name]
|
||||||
|
[Institution Name], [Country] | [YYYY] – [YYYY]
|
||||||
|
[Grade or classification if notable]
|
||||||
|
|
||||||
|
[Repeat for each qualification]
|
||||||
|
|
||||||
|
CERTIFICATIONS
|
||||||
|
[Certificate Name] — [Issuing Body] — [Month YYYY]
|
||||||
|
|
||||||
|
PROJECTS
|
||||||
|
|
||||||
|
[Project Name]
|
||||||
|
[Technologies: tool, tool, tool]
|
||||||
|
- [What it does / your role / outcome]
|
||||||
|
|
||||||
|
ACHIEVEMENTS
|
||||||
|
- [Achievement one]
|
||||||
|
- [Achievement two]
|
||||||
|
|
||||||
|
VOLUNTEER EXPERIENCE
|
||||||
|
[Role] — [Organisation] — [YYYY–YYYY]
|
||||||
|
- [One-line description]
|
||||||
|
|
||||||
|
LANGUAGES
|
||||||
|
[Language]: [Native / Fluent / Professional / Conversational / Basic]
|
||||||
|
|
||||||
|
ADDITIONAL INFORMATION
|
||||||
|
[Anything else: open-source, interests relevant to role]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Canva paste-ready version
|
||||||
|
|
||||||
|
Canva CV templates use individual text boxes per section. Provide each section as
|
||||||
|
a separate clearly labelled block, with no Markdown symbols.
|
||||||
|
|
||||||
|
```
|
||||||
|
--- PASTE INTO: Name field ---
|
||||||
|
[Full name]
|
||||||
|
|
||||||
|
--- PASTE INTO: Job title / headline field ---
|
||||||
|
[Target job title]
|
||||||
|
|
||||||
|
--- PASTE INTO: Contact block ---
|
||||||
|
[email] | [phone] | [city, country] | [LinkedIn URL]
|
||||||
|
|
||||||
|
--- PASTE INTO: Summary / About field ---
|
||||||
|
[3–5 sentence paragraph, plain text, no hyphens or bullets]
|
||||||
|
|
||||||
|
--- PASTE INTO: Skills field ---
|
||||||
|
[skill] | [skill] | [skill] | [skill] | [skill]
|
||||||
|
|
||||||
|
--- PASTE INTO: Experience entry 1 ---
|
||||||
|
[Job Title]
|
||||||
|
[Company] | [Location] | [Mon YYYY – Mon YYYY]
|
||||||
|
- [Bullet]
|
||||||
|
- [Bullet]
|
||||||
|
- [Bullet]
|
||||||
|
|
||||||
|
[Continue for each role as a separate block]
|
||||||
|
|
||||||
|
--- PASTE INTO: Education entry 1 ---
|
||||||
|
[Degree]
|
||||||
|
[Institution], [Country] | [YYYY – YYYY]
|
||||||
|
[Grade if notable]
|
||||||
|
|
||||||
|
--- PASTE INTO: Certifications ---
|
||||||
|
[Certificate] | [Issuer] | [YYYY]
|
||||||
|
|
||||||
|
--- PASTE INTO: Languages ---
|
||||||
|
[Language] ([Proficiency])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Missing information checklist
|
||||||
|
|
||||||
|
```
|
||||||
|
MISSING INFORMATION
|
||||||
|
[ ] Phone number
|
||||||
|
[ ] LinkedIn URL
|
||||||
|
[ ] Portfolio or GitHub URL
|
||||||
|
[ ] Measurable results for [Role] at [Company]
|
||||||
|
[ ] Certifications — do you hold any?
|
||||||
|
[ ] Languages — list any beyond English
|
||||||
|
[ ] Employment gap [Mon YYYY – Mon YYYY] — add a brief framing note
|
||||||
|
[ ] [Any other flagged item]
|
||||||
|
```
|
||||||
|
|
||||||
|
### CV flaw report (scored 0–100)
|
||||||
|
|
||||||
|
Run all checks. Display a scored report:
|
||||||
|
|
||||||
|
```
|
||||||
|
CV FLAW REPORT
|
||||||
|
──────────────────────────────────────
|
||||||
|
Score: [X]/100
|
||||||
|
|
||||||
|
PASS Truthfulness — all facts sourced from user data
|
||||||
|
PASS No hallucination — no fabricated details
|
||||||
|
PASS Tense correctness — past for completed, present for current
|
||||||
|
PASS ATS structure — single column, no tables or images
|
||||||
|
PASS Standard headings — all recognisable by parsers
|
||||||
|
PASS No forbidden characters — no ➤ ✓ ★
|
||||||
|
PASS Humanized — no banned phrases found
|
||||||
|
PASS Contact info in body (not header/footer)
|
||||||
|
FAIL [Check name] — [specific issue and location in CV]
|
||||||
|
──────────────────────────────────────
|
||||||
|
Deductions: -[N] per FAIL item
|
||||||
|
Final score: [X]/100
|
||||||
|
|
||||||
|
ISSUES TO FIX:
|
||||||
|
1. [Exact location] — [Exact problem] — [Suggested fix]
|
||||||
|
2. [Exact location] — [Exact problem] — [Suggested fix]
|
||||||
|
```
|
||||||
|
|
||||||
|
Score deductions: -10 per FAIL on truthfulness or hallucination;
|
||||||
|
-5 per FAIL on tense, ATS structure, or banned phrases;
|
||||||
|
-3 per FAIL on formatting issues.
|
||||||
|
|
||||||
|
### Improvement suggestions (3–7, specific and actionable)
|
||||||
|
|
||||||
|
- "Your summary does not state the target role. Open with your job title explicitly."
|
||||||
|
- "The [Company] role has no metrics. Even approximate scope (team size, users, budget range) strengthens credibility."
|
||||||
|
- "Skills section mixes expert and basic tools without distinction. Group into Proficient / Familiar."
|
||||||
|
- "Add a GitHub or portfolio URL — technical recruiters check it before the interview."
|
||||||
|
- "Three bullets begin with 'Responsible for' — replace with direct action verbs."
|
||||||
|
- "CV is [N] pages for [N] years of experience. Target is [N] pages; trim older roles to one line."
|
||||||
|
|
||||||
|
### Suggested file name
|
||||||
|
|
||||||
|
```
|
||||||
|
Suggested filename: [FirstName]_[LastName]_CV.docx
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cover letter companion (optional)
|
||||||
|
|
||||||
|
After the CV output, offer:
|
||||||
|
|
||||||
|
> "Would you like a tailored cover letter for this application?"
|
||||||
|
|
||||||
|
If yes, output as a **separate clearly labelled plain-text block** — not inline with the CV.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Opens with a specific hook — not "I am writing to apply for…"
|
||||||
|
- References company and role by name
|
||||||
|
- Bridges 2–3 strongest CV points to the JD's key requirements
|
||||||
|
- Closes with a clear call to action
|
||||||
|
- Matches tone of the target industry
|
||||||
|
- 3 paragraphs maximum, 250–350 words
|
||||||
|
- Does not repeat the CV verbatim
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- **No hallucination.** Never invent a title, company, date, degree, cert, skill, metric, or award.
|
||||||
|
- **No fake metrics.** If the user says "we grew a lot", ask for specifics — never insert a percentage.
|
||||||
|
- **Respect source truth.** "Junior Developer" stays "Junior Developer" — suggest a reframe if needed; never silently change it.
|
||||||
|
- **No silent changes.** If something is materially reworded, note the change.
|
||||||
|
- **One version at a time.** Complete the CV before offering variants.
|
||||||
|
- **Privacy.** Do not expose full home address, national ID, DOB, marital status, or religion unless the user's target market requires it.
|
||||||
|
- **No keyword stuffing.** Adding skills the user does not have is fraud. Flag gaps; never fabricate.
|
||||||
|
- **OCR warning.** Always display before continuing: "OCR was used — please verify the extracted text for accuracy."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Country and market conventions
|
||||||
|
|
||||||
|
| Market | Length | Photo | DOB | Marital status | References |
|
||||||
|
|--------|--------|-------|-----|----------------|------------|
|
||||||
|
| USA | 1–2 pages | No | No | No | "Available on request" |
|
||||||
|
| Canada | 1–2 pages | No | No | No | "Available on request" |
|
||||||
|
| UK | 2 pages | No | No | No | "Available on request" |
|
||||||
|
| Ireland | 2 pages | No | No | No | "Available on request" |
|
||||||
|
| Australia / NZ | 2–3 pages | No | No | No | "Available on request" |
|
||||||
|
| Germany / Austria / Switzerland | 2–3 pages | Yes (expected) | Yes | Sometimes | Listed or on request |
|
||||||
|
| France | 1–2 pages | Optional | No (illegal to require) | No | On request |
|
||||||
|
| Netherlands / Scandinavia | 1–2 pages | Optional | No | No | On request |
|
||||||
|
| Japan | 1–2 pages (rirekisho) | Yes | Yes | Yes | Listed |
|
||||||
|
| South Korea | 1–2 pages | Yes | Yes | Yes | Listed |
|
||||||
|
| China | 1–2 pages | Yes | Yes | Yes | Listed |
|
||||||
|
| India | 2–3 pages | Optional | Yes (common) | Sometimes | Listed |
|
||||||
|
| Nepal | 2–3 pages | Yes (common) | Yes | Sometimes | Listed |
|
||||||
|
| Bangladesh / Sri Lanka | 2–3 pages | Yes (common) | Yes | Sometimes | Listed |
|
||||||
|
| UAE / Gulf (GCC) | 2–3 pages | Yes (common) | Yes | Yes (sometimes) | Listed |
|
||||||
|
| Nigeria / East Africa | 2–3 pages | Yes (common) | Yes | Sometimes | Listed |
|
||||||
|
| South Africa | 3–5 pages | Optional | Yes (common) | No | Listed |
|
||||||
|
| Brazil | 1–2 pages | Optional | Yes (common) | No | On request |
|
||||||
|
| Academic (global) | No limit | Varies | Varies | No | Full list required |
|
||||||
|
| Executive / board (global) | 2–3 pages | No | No | No | On request |
|
||||||
|
|
||||||
|
Default when market is unknown: UK / international conventions (no photo, no DOB, 2 pages,
|
||||||
|
"Available on request").
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision tree
|
||||||
|
|
||||||
|
```
|
||||||
|
User invokes @cv-generator
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Source provided? --No--> Run questionnaire (Q1–Q20, one at a time)
|
||||||
|
|Yes
|
||||||
|
v
|
||||||
|
LinkedIn URL blocked? --Yes--> Ask for PDF export immediately; do not proceed empty
|
||||||
|
|No
|
||||||
|
v
|
||||||
|
Collect all sources --> merge and deduplicate (section 5)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Ask: Purpose? --> Detect or assume seniority (default: mid-level; tell the user)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Select format (section 3c)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Select section order (section 6)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
JD provided? --Yes--> Parse JD --> extract and score keywords (section 9)
|
||||||
|
|No |
|
||||||
|
v v
|
||||||
|
Write CV content Integrate keywords naturally
|
||||||
|
(sections 7–8) |
|
||||||
|
|<-------------------+
|
||||||
|
v
|
||||||
|
Run anti-hallucination gate (section 10) --> block output until all pass
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Run tense enforcement (section 7d) --> block output until all pass
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Run banned phrase scan (section 7c) --> fix any found
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Output in order:
|
||||||
|
Formatted CV (staging draft)
|
||||||
|
FlowCV paste-ready version
|
||||||
|
Canva paste-ready version
|
||||||
|
Missing information checklist
|
||||||
|
CV flaw report (scored)
|
||||||
|
Improve suggestions
|
||||||
|
Suggested file name
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Offer cover letter (section 12)
|
||||||
|
```
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# dbos-golang
|
||||||
|
|
||||||
|
> **Note:** `CLAUDE.md` is a symlink to this file.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
DBOS Go SDK for building reliable, fault-tolerant applications with durable workflows. Use this skill when writing Go code with DBOS, creating workflows and steps, using queues, using the DBOS Client from external applications, or building Go applications that need to be resilient to failures.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
dbos-golang/
|
||||||
|
SKILL.md # Main skill file - read this first
|
||||||
|
AGENTS.md # This navigation guide
|
||||||
|
CLAUDE.md # Symlink to AGENTS.md
|
||||||
|
references/ # Detailed reference files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Read `SKILL.md` for the main skill instructions
|
||||||
|
2. Browse `references/` for detailed documentation on specific topics
|
||||||
|
3. Reference files are loaded on-demand - read only what you need
|
||||||
|
|
||||||
|
## Reference Categories
|
||||||
|
|
||||||
|
| Priority | Category | Impact | Prefix |
|
||||||
|
|----------|----------|--------|--------|
|
||||||
|
| 1 | Lifecycle | CRITICAL | `lifecycle-` |
|
||||||
|
| 2 | Workflow | CRITICAL | `workflow-` |
|
||||||
|
| 3 | Step | HIGH | `step-` |
|
||||||
|
| 4 | Queue | HIGH | `queue-` |
|
||||||
|
| 5 | Communication | MEDIUM | `comm-` |
|
||||||
|
| 6 | Pattern | MEDIUM | `pattern-` |
|
||||||
|
| 7 | Testing | LOW-MEDIUM | `test-` |
|
||||||
|
| 8 | Client | MEDIUM | `client-` |
|
||||||
|
| 9 | Advanced | LOW | `advanced-` |
|
||||||
|
|
||||||
|
Reference files are named `{prefix}-{topic}.md` (e.g., `query-missing-indexes.md`).
|
||||||
|
|
||||||
|
## Available References
|
||||||
|
|
||||||
|
**Advanced** (`advanced-`):
|
||||||
|
- `references/advanced-patching.md`
|
||||||
|
- `references/advanced-versioning.md`
|
||||||
|
|
||||||
|
**Client** (`client-`):
|
||||||
|
- `references/client-enqueue.md`
|
||||||
|
- `references/client-setup.md`
|
||||||
|
|
||||||
|
**Communication** (`comm-`):
|
||||||
|
- `references/comm-events.md`
|
||||||
|
- `references/comm-messages.md`
|
||||||
|
- `references/comm-streaming.md`
|
||||||
|
|
||||||
|
**Lifecycle** (`lifecycle-`):
|
||||||
|
- `references/lifecycle-config.md`
|
||||||
|
|
||||||
|
**Pattern** (`pattern-`):
|
||||||
|
- `references/pattern-debouncing.md`
|
||||||
|
- `references/pattern-idempotency.md`
|
||||||
|
- `references/pattern-scheduled.md`
|
||||||
|
- `references/pattern-sleep.md`
|
||||||
|
|
||||||
|
**Queue** (`queue-`):
|
||||||
|
- `references/queue-basics.md`
|
||||||
|
- `references/queue-concurrency.md`
|
||||||
|
- `references/queue-deduplication.md`
|
||||||
|
- `references/queue-listening.md`
|
||||||
|
- `references/queue-partitioning.md`
|
||||||
|
- `references/queue-priority.md`
|
||||||
|
- `references/queue-rate-limiting.md`
|
||||||
|
|
||||||
|
**Step** (`step-`):
|
||||||
|
- `references/step-basics.md`
|
||||||
|
- `references/step-concurrency.md`
|
||||||
|
- `references/step-retries.md`
|
||||||
|
|
||||||
|
**Testing** (`test-`):
|
||||||
|
- `references/test-setup.md`
|
||||||
|
|
||||||
|
**Workflow** (`workflow-`):
|
||||||
|
- `references/workflow-background.md`
|
||||||
|
- `references/workflow-constraints.md`
|
||||||
|
- `references/workflow-control.md`
|
||||||
|
- `references/workflow-determinism.md`
|
||||||
|
- `references/workflow-introspection.md`
|
||||||
|
- `references/workflow-timeout.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*29 reference files across 9 categories*
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# dbos-golang
|
||||||
|
|
||||||
|
> **Note:** `CLAUDE.md` is a symlink to this file.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
DBOS Go SDK for building reliable, fault-tolerant applications with durable workflows. Use this skill when writing Go code with DBOS, creating workflows and steps, using queues, using the DBOS Client from external applications, or building Go applications that need to be resilient to failures.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
dbos-golang/
|
||||||
|
SKILL.md # Main skill file - read this first
|
||||||
|
AGENTS.md # This navigation guide
|
||||||
|
CLAUDE.md # Symlink to AGENTS.md
|
||||||
|
references/ # Detailed reference files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Read `SKILL.md` for the main skill instructions
|
||||||
|
2. Browse `references/` for detailed documentation on specific topics
|
||||||
|
3. Reference files are loaded on-demand - read only what you need
|
||||||
|
|
||||||
|
## Reference Categories
|
||||||
|
|
||||||
|
| Priority | Category | Impact | Prefix |
|
||||||
|
|----------|----------|--------|--------|
|
||||||
|
| 1 | Lifecycle | CRITICAL | `lifecycle-` |
|
||||||
|
| 2 | Workflow | CRITICAL | `workflow-` |
|
||||||
|
| 3 | Step | HIGH | `step-` |
|
||||||
|
| 4 | Queue | HIGH | `queue-` |
|
||||||
|
| 5 | Communication | MEDIUM | `comm-` |
|
||||||
|
| 6 | Pattern | MEDIUM | `pattern-` |
|
||||||
|
| 7 | Testing | LOW-MEDIUM | `test-` |
|
||||||
|
| 8 | Client | MEDIUM | `client-` |
|
||||||
|
| 9 | Advanced | LOW | `advanced-` |
|
||||||
|
|
||||||
|
Reference files are named `{prefix}-{topic}.md` (e.g., `query-missing-indexes.md`).
|
||||||
|
|
||||||
|
## Available References
|
||||||
|
|
||||||
|
**Advanced** (`advanced-`):
|
||||||
|
- `references/advanced-patching.md`
|
||||||
|
- `references/advanced-versioning.md`
|
||||||
|
|
||||||
|
**Client** (`client-`):
|
||||||
|
- `references/client-enqueue.md`
|
||||||
|
- `references/client-setup.md`
|
||||||
|
|
||||||
|
**Communication** (`comm-`):
|
||||||
|
- `references/comm-events.md`
|
||||||
|
- `references/comm-messages.md`
|
||||||
|
- `references/comm-streaming.md`
|
||||||
|
|
||||||
|
**Lifecycle** (`lifecycle-`):
|
||||||
|
- `references/lifecycle-config.md`
|
||||||
|
|
||||||
|
**Pattern** (`pattern-`):
|
||||||
|
- `references/pattern-debouncing.md`
|
||||||
|
- `references/pattern-idempotency.md`
|
||||||
|
- `references/pattern-scheduled.md`
|
||||||
|
- `references/pattern-sleep.md`
|
||||||
|
|
||||||
|
**Queue** (`queue-`):
|
||||||
|
- `references/queue-basics.md`
|
||||||
|
- `references/queue-concurrency.md`
|
||||||
|
- `references/queue-deduplication.md`
|
||||||
|
- `references/queue-listening.md`
|
||||||
|
- `references/queue-partitioning.md`
|
||||||
|
- `references/queue-priority.md`
|
||||||
|
- `references/queue-rate-limiting.md`
|
||||||
|
|
||||||
|
**Step** (`step-`):
|
||||||
|
- `references/step-basics.md`
|
||||||
|
- `references/step-concurrency.md`
|
||||||
|
- `references/step-retries.md`
|
||||||
|
|
||||||
|
**Testing** (`test-`):
|
||||||
|
- `references/test-setup.md`
|
||||||
|
|
||||||
|
**Workflow** (`workflow-`):
|
||||||
|
- `references/workflow-background.md`
|
||||||
|
- `references/workflow-constraints.md`
|
||||||
|
- `references/workflow-control.md`
|
||||||
|
- `references/workflow-determinism.md`
|
||||||
|
- `references/workflow-introspection.md`
|
||||||
|
- `references/workflow-timeout.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*29 reference files across 9 categories*
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
---
|
||||||
|
name: dbos-golang
|
||||||
|
description: "Guide for building reliable, fault-tolerant Go applications with DBOS durable workflows. Use when adding DBOS to existing Go code, creating workflows and steps, or using queues for concurrency control."
|
||||||
|
risk: safe
|
||||||
|
source: "https://docs.dbos.dev/"
|
||||||
|
date_added: "2026-02-27"
|
||||||
|
---
|
||||||
|
|
||||||
|
# DBOS Go Best Practices
|
||||||
|
|
||||||
|
Guide for building reliable, fault-tolerant Go applications with DBOS durable workflows.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
Reference these guidelines when:
|
||||||
|
- Adding DBOS to existing Go code
|
||||||
|
- Creating workflows and steps
|
||||||
|
- Using queues for concurrency control
|
||||||
|
- Implementing workflow communication (events, messages, streams)
|
||||||
|
- Configuring and launching DBOS applications
|
||||||
|
- Using the DBOS Client from external applications
|
||||||
|
- Testing DBOS applications
|
||||||
|
|
||||||
|
## Rule Categories by Priority
|
||||||
|
|
||||||
|
| Priority | Category | Impact | Prefix |
|
||||||
|
|----------|----------|--------|--------|
|
||||||
|
| 1 | Lifecycle | CRITICAL | `lifecycle-` |
|
||||||
|
| 2 | Workflow | CRITICAL | `workflow-` |
|
||||||
|
| 3 | Step | HIGH | `step-` |
|
||||||
|
| 4 | Queue | HIGH | `queue-` |
|
||||||
|
| 5 | Communication | MEDIUM | `comm-` |
|
||||||
|
| 6 | Pattern | MEDIUM | `pattern-` |
|
||||||
|
| 7 | Testing | LOW-MEDIUM | `test-` |
|
||||||
|
| 8 | Client | MEDIUM | `client-` |
|
||||||
|
| 9 | Advanced | LOW | `advanced-` |
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Install the DBOS Go module:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/dbos-inc/dbos-transact-golang/dbos@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### DBOS Configuration and Launch
|
||||||
|
|
||||||
|
A DBOS application MUST create a context, register workflows, and launch before running any workflows:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dbos-inc/dbos-transact-golang/dbos"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx, err := dbos.NewDBOSContext(context.Background(), dbos.Config{
|
||||||
|
AppName: "my-app",
|
||||||
|
DatabaseURL: os.Getenv("DBOS_SYSTEM_DATABASE_URL"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer dbos.Shutdown(ctx, 30*time.Second)
|
||||||
|
|
||||||
|
dbos.RegisterWorkflow(ctx, myWorkflow)
|
||||||
|
|
||||||
|
if err := dbos.Launch(ctx); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflow and Step Structure
|
||||||
|
|
||||||
|
Workflows are comprised of steps. Any function performing complex operations or accessing external services must be run as a step using `dbos.RunAsStep`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func fetchData(ctx context.Context) (string, error) {
|
||||||
|
resp, err := http.Get("https://api.example.com/data")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return string(body), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
result, err := dbos.RunAsStep(ctx, fetchData, dbos.WithStepName("fetchData"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Constraints
|
||||||
|
|
||||||
|
- Do NOT start or enqueue workflows from within steps
|
||||||
|
- Do NOT use uncontrolled goroutines to start workflows - use `dbos.RunWorkflow` with queues or `dbos.Go`/`dbos.Select` for concurrent steps
|
||||||
|
- Workflows MUST be deterministic - non-deterministic operations go in steps
|
||||||
|
- Do NOT modify global variables from workflows or steps
|
||||||
|
- All workflows and queues MUST be registered before calling `Launch()`
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
Read individual rule files for detailed explanations and examples:
|
||||||
|
|
||||||
|
```
|
||||||
|
references/lifecycle-config.md
|
||||||
|
references/workflow-determinism.md
|
||||||
|
references/queue-concurrency.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- https://docs.dbos.dev/
|
||||||
|
- https://github.com/dbos-inc/dbos-transact-golang
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Section Definitions
|
||||||
|
|
||||||
|
This file defines the rule categories for DBOS Go best practices. Rules are automatically assigned to sections based on their filename prefix.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Lifecycle (lifecycle)
|
||||||
|
**Impact:** CRITICAL
|
||||||
|
**Description:** DBOS configuration, initialization, and launch patterns. Foundation for all DBOS applications.
|
||||||
|
|
||||||
|
## 2. Workflow (workflow)
|
||||||
|
**Impact:** CRITICAL
|
||||||
|
**Description:** Workflow creation, determinism requirements, background execution, and workflow IDs.
|
||||||
|
|
||||||
|
## 3. Step (step)
|
||||||
|
**Impact:** HIGH
|
||||||
|
**Description:** Step creation, retries, concurrent steps with Go/Select, and when to use steps vs workflows.
|
||||||
|
|
||||||
|
## 4. Queue (queue)
|
||||||
|
**Impact:** HIGH
|
||||||
|
**Description:** Queue creation, concurrency limits, rate limiting, partitioning, and priority.
|
||||||
|
|
||||||
|
## 5. Communication (comm)
|
||||||
|
**Impact:** MEDIUM
|
||||||
|
**Description:** Workflow events, messages, and streaming for inter-workflow communication.
|
||||||
|
|
||||||
|
## 6. Pattern (pattern)
|
||||||
|
**Impact:** MEDIUM
|
||||||
|
**Description:** Common patterns including idempotency, scheduled workflows, debouncing, and durable sleep.
|
||||||
|
|
||||||
|
## 7. Testing (test)
|
||||||
|
**Impact:** LOW-MEDIUM
|
||||||
|
**Description:** Testing DBOS applications with Go's testing package, mocks, and integration test setup.
|
||||||
|
|
||||||
|
## 8. Client (client)
|
||||||
|
**Impact:** MEDIUM
|
||||||
|
**Description:** DBOS Client for interacting with DBOS from external applications.
|
||||||
|
|
||||||
|
## 9. Advanced (advanced)
|
||||||
|
**Impact:** LOW
|
||||||
|
**Description:** Workflow versioning, patching, and safe code upgrades.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
title: Use Patching for Safe Workflow Upgrades
|
||||||
|
impact: LOW
|
||||||
|
impactDescription: Safely deploy breaking workflow changes without disrupting in-progress workflows
|
||||||
|
tags: advanced, patching, upgrade, breaking-change
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use Patching for Safe Workflow Upgrades
|
||||||
|
|
||||||
|
Use `dbos.Patch` to safely deploy breaking changes to workflow code. Breaking changes alter which steps run or their order, which can cause recovery failures.
|
||||||
|
|
||||||
|
**Incorrect (breaking change without patching):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// BEFORE: original workflow
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
result, _ := dbos.RunAsStep(ctx, foo, dbos.WithStepName("foo"))
|
||||||
|
_, _ = dbos.RunAsStep(ctx, bar, dbos.WithStepName("bar"))
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AFTER: breaking change - recovery will fail for in-progress workflows!
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
result, _ := dbos.RunAsStep(ctx, baz, dbos.WithStepName("baz")) // Changed step
|
||||||
|
_, _ = dbos.RunAsStep(ctx, bar, dbos.WithStepName("bar"))
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (using patch):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
useBaz, err := dbos.Patch(ctx, "use-baz")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var result string
|
||||||
|
if useBaz {
|
||||||
|
result, _ = dbos.RunAsStep(ctx, baz, dbos.WithStepName("baz")) // New workflows
|
||||||
|
} else {
|
||||||
|
result, _ = dbos.RunAsStep(ctx, foo, dbos.WithStepName("foo")) // Old workflows
|
||||||
|
}
|
||||||
|
_, _ = dbos.RunAsStep(ctx, bar, dbos.WithStepName("bar"))
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`dbos.Patch` returns `true` for new workflows and `false` for workflows that started before the patch.
|
||||||
|
|
||||||
|
**Deprecating patches (after all old workflows complete):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
dbos.DeprecatePatch(ctx, "use-baz") // Always takes the new path
|
||||||
|
result, _ := dbos.RunAsStep(ctx, baz, dbos.WithStepName("baz"))
|
||||||
|
_, _ = dbos.RunAsStep(ctx, bar, dbos.WithStepName("bar"))
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Removing patches (after all workflows using DeprecatePatch complete):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
result, _ := dbos.RunAsStep(ctx, baz, dbos.WithStepName("baz"))
|
||||||
|
_, _ = dbos.RunAsStep(ctx, bar, dbos.WithStepName("bar"))
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Lifecycle: `Patch()` → deploy → wait for old workflows → `DeprecatePatch()` → deploy → wait → remove patch entirely.
|
||||||
|
|
||||||
|
**Required configuration** — patching must be explicitly enabled:
|
||||||
|
|
||||||
|
```go
|
||||||
|
ctx, _ := dbos.NewDBOSContext(context.Background(), dbos.Config{
|
||||||
|
AppName: "my-app",
|
||||||
|
DatabaseURL: os.Getenv("DBOS_SYSTEM_DATABASE_URL"),
|
||||||
|
EnablePatching: true, // Required for dbos.Patch and dbos.DeprecatePatch
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Without `EnablePatching: true`, calls to `dbos.Patch` and `dbos.DeprecatePatch` will fail.
|
||||||
|
|
||||||
|
Reference: [Patching](https://docs.dbos.dev/golang/tutorials/upgrading-workflows#patching)
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
title: Use Versioning for Blue-Green Deployments
|
||||||
|
impact: LOW
|
||||||
|
impactDescription: Enables safe deployment of new code versions alongside old ones
|
||||||
|
tags: advanced, versioning, blue-green, deployment
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use Versioning for Blue-Green Deployments
|
||||||
|
|
||||||
|
Set `ApplicationVersion` in configuration to tag workflows with a version. DBOS only recovers workflows matching the current application version, preventing code mismatches during recovery.
|
||||||
|
|
||||||
|
**Incorrect (deploying new code that breaks in-progress workflows):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
ctx, _ := dbos.NewDBOSContext(context.Background(), dbos.Config{
|
||||||
|
AppName: "my-app",
|
||||||
|
DatabaseURL: os.Getenv("DBOS_SYSTEM_DATABASE_URL"),
|
||||||
|
// No version set - version auto-computed from binary hash
|
||||||
|
// Old workflows will be recovered with new code, which may break
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (versioned deployment):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
ctx, _ := dbos.NewDBOSContext(context.Background(), dbos.Config{
|
||||||
|
AppName: "my-app",
|
||||||
|
DatabaseURL: os.Getenv("DBOS_SYSTEM_DATABASE_URL"),
|
||||||
|
ApplicationVersion: "2.0.0",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, the application version is automatically computed from a SHA-256 hash of the executable binary. Set it explicitly for more control.
|
||||||
|
|
||||||
|
**Blue-green deployment strategy:**
|
||||||
|
|
||||||
|
1. Deploy new version (v2) alongside old version (v1)
|
||||||
|
2. Direct new traffic to v2 processes
|
||||||
|
3. Let v1 processes "drain" (complete in-progress workflows)
|
||||||
|
4. Check for remaining v1 workflows:
|
||||||
|
|
||||||
|
```go
|
||||||
|
oldWorkflows, _ := dbos.ListWorkflows(ctx,
|
||||||
|
dbos.WithAppVersion("1.0.0"),
|
||||||
|
dbos.WithStatus([]dbos.WorkflowStatusType{dbos.WorkflowStatusPending}),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Once all v1 workflows are complete, retire v1 processes
|
||||||
|
|
||||||
|
**Fork to new version (for stuck workflows):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Fork a workflow from a failed step to run on the new version
|
||||||
|
handle, _ := dbos.ForkWorkflowstring
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference: [Versioning](https://docs.dbos.dev/golang/tutorials/upgrading-workflows#versioning)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
title: Enqueue Workflows from External Applications
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Enables external services to submit work to DBOS queues
|
||||||
|
tags: client, enqueue, external, queue
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enqueue Workflows from External Applications
|
||||||
|
|
||||||
|
Use `client.Enqueue()` to submit workflows from outside your DBOS application. Since the Client runs externally, workflow and queue metadata must be specified explicitly by name.
|
||||||
|
|
||||||
|
**Incorrect (trying to use RunWorkflow from external code):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// RunWorkflow requires a full DBOS context with registered workflows
|
||||||
|
dbos.RunWorkflow(ctx, processTask, "data", dbos.WithQueue("myQueue"))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (using Client.Enqueue):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
client, err := dbos.NewClient(context.Background(), dbos.ClientConfig{
|
||||||
|
DatabaseURL: os.Getenv("DBOS_SYSTEM_DATABASE_URL"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer client.Shutdown(10 * time.Second)
|
||||||
|
|
||||||
|
// Basic enqueue - specify workflow and queue by name
|
||||||
|
handle, err := client.Enqueue("task_queue", "processTask", "task-data")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the result
|
||||||
|
result, err := handle.GetResult()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Enqueue with options:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
handle, err := client.Enqueue("task_queue", "processTask", "task-data",
|
||||||
|
dbos.WithEnqueueWorkflowID("custom-id"),
|
||||||
|
dbos.WithEnqueueDeduplicationID("unique-id"),
|
||||||
|
dbos.WithEnqueuePriority(10),
|
||||||
|
dbos.WithEnqueueTimeout(5*time.Minute),
|
||||||
|
dbos.WithEnqueueQueuePartitionKey("user-123"),
|
||||||
|
dbos.WithEnqueueApplicationVersion("2.0.0"),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Enqueue options:
|
||||||
|
- `WithEnqueueWorkflowID`: Custom workflow ID
|
||||||
|
- `WithEnqueueDeduplicationID`: Prevent duplicate enqueues
|
||||||
|
- `WithEnqueuePriority`: Queue priority (lower = higher priority)
|
||||||
|
- `WithEnqueueTimeout`: Workflow timeout
|
||||||
|
- `WithEnqueueQueuePartitionKey`: Partition key for partitioned queues
|
||||||
|
- `WithEnqueueApplicationVersion`: Override application version
|
||||||
|
|
||||||
|
The workflow name must match the registered name or custom name set with `WithWorkflowName` during registration.
|
||||||
|
|
||||||
|
Always call `client.Shutdown()` when done.
|
||||||
|
|
||||||
|
Reference: [DBOS Client Enqueue](https://docs.dbos.dev/golang/reference/client#enqueue)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
title: Initialize Client for External Access
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Enables external applications to interact with DBOS workflows
|
||||||
|
tags: client, external, setup, initialization
|
||||||
|
---
|
||||||
|
|
||||||
|
## Initialize Client for External Access
|
||||||
|
|
||||||
|
Use `dbos.NewClient` to interact with DBOS from external applications like API servers, CLI tools, or separate services. The Client connects directly to the DBOS system database.
|
||||||
|
|
||||||
|
**Incorrect (using full DBOS context from an external app):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Full DBOS context requires Launch() - too heavy for external clients
|
||||||
|
ctx, _ := dbos.NewDBOSContext(context.Background(), config)
|
||||||
|
dbos.Launch(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (using Client):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
client, err := dbos.NewClient(context.Background(), dbos.ClientConfig{
|
||||||
|
DatabaseURL: os.Getenv("DBOS_SYSTEM_DATABASE_URL"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer client.Shutdown(10 * time.Second)
|
||||||
|
|
||||||
|
// Send a message to a workflow
|
||||||
|
err = client.Send(workflowID, "notification", "topic")
|
||||||
|
|
||||||
|
// Get an event from a workflow
|
||||||
|
event, err := client.GetEvent(workflowID, "status", 60*time.Second)
|
||||||
|
|
||||||
|
// Retrieve a workflow handle
|
||||||
|
handle, err := client.RetrieveWorkflow(workflowID)
|
||||||
|
result, err := handle.GetResult()
|
||||||
|
|
||||||
|
// List workflows
|
||||||
|
workflows, err := client.ListWorkflows(
|
||||||
|
dbos.WithStatus([]dbos.WorkflowStatusType{dbos.WorkflowStatusError}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Workflow management
|
||||||
|
err = client.CancelWorkflow(workflowID)
|
||||||
|
handle, err = client.ResumeWorkflow(workflowID)
|
||||||
|
|
||||||
|
// Read a stream
|
||||||
|
values, closed, err := client.ClientReadStream(workflowID, "results")
|
||||||
|
|
||||||
|
// Read a stream asynchronously
|
||||||
|
ch, err := client.ClientReadStreamAsync(workflowID, "results")
|
||||||
|
```
|
||||||
|
|
||||||
|
ClientConfig options:
|
||||||
|
- `DatabaseURL` (required unless `SystemDBPool` is set): PostgreSQL connection string
|
||||||
|
- `SystemDBPool`: Custom `*pgxpool.Pool`
|
||||||
|
- `DatabaseSchema`: Schema name (default: `"dbos"`)
|
||||||
|
- `Logger`: Custom `*slog.Logger`
|
||||||
|
|
||||||
|
Always call `client.Shutdown()` when done.
|
||||||
|
|
||||||
|
Reference: [DBOS Client](https://docs.dbos.dev/golang/reference/client)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
title: Use Events for Workflow Status Publishing
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: Enables real-time progress monitoring and interactive workflows
|
||||||
|
tags: communication, events, status, key-value
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use Events for Workflow Status Publishing
|
||||||
|
|
||||||
|
Workflows can publish events (key-value pairs) with `dbos.SetEvent`. Other code can read events with `dbos.GetEvent`. Events are persisted and useful for real-time progress monitoring.
|
||||||
|
|
||||||
|
**Incorrect (using external state for progress):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
var progress int // Global variable - not durable!
|
||||||
|
|
||||||
|
func processData(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
progress = 50 // Not persisted, lost on restart
|
||||||
|
return input, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (using events):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func processData(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
dbos.SetEvent(ctx, "status", "processing")
|
||||||
|
_, err := dbos.RunAsStep(ctx, stepOne, dbos.WithStepName("stepOne"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
dbos.SetEvent(ctx, "progress", 50)
|
||||||
|
_, err = dbos.RunAsStep(ctx, stepTwo, dbos.WithStepName("stepTwo"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
dbos.SetEvent(ctx, "progress", 100)
|
||||||
|
dbos.SetEvent(ctx, "status", "complete")
|
||||||
|
return "done", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read events from outside the workflow
|
||||||
|
status, err := dbos.GetEventstring
|
||||||
|
progress, err := dbos.GetEventint
|
||||||
|
```
|
||||||
|
|
||||||
|
Events are useful for interactive workflows. For example, a checkout workflow can publish a payment URL for the caller to redirect to:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func checkoutWorkflow(ctx dbos.DBOSContext, order Order) (string, error) {
|
||||||
|
paymentURL, err := dbos.RunAsStep(ctx, func(ctx context.Context) (string, error) {
|
||||||
|
return createPayment(order)
|
||||||
|
}, dbos.WithStepName("createPayment"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
dbos.SetEvent(ctx, "paymentURL", paymentURL)
|
||||||
|
// Continue processing...
|
||||||
|
return "success", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP handler starts workflow and reads the payment URL
|
||||||
|
handle, _ := dbos.RunWorkflow(ctx, checkoutWorkflow, order)
|
||||||
|
url, _ := dbos.GetEventstring, "paymentURL", 300*time.Second)
|
||||||
|
```
|
||||||
|
|
||||||
|
`GetEvent` blocks until the event is set or the timeout expires. It returns the zero value of the type if the timeout is reached.
|
||||||
|
|
||||||
|
Reference: [Workflow Events](https://docs.dbos.dev/golang/tutorials/workflow-communication#workflow-events)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
title: Use Messages for Workflow Notifications
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: Enables reliable inter-workflow and external-to-workflow communication
|
||||||
|
tags: communication, messages, send, recv, notification
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use Messages for Workflow Notifications
|
||||||
|
|
||||||
|
Use `dbos.Send` to send messages to a workflow and `dbos.Recv` to receive them. Messages are queued per topic and persisted for reliable delivery.
|
||||||
|
|
||||||
|
**Incorrect (using external messaging for workflow communication):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// External message queue is not integrated with workflow recovery
|
||||||
|
ch := make(chan string) // Not durable!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (using DBOS messages):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func checkoutWorkflow(ctx dbos.DBOSContext, orderID string) (string, error) {
|
||||||
|
// Wait for payment notification (timeout 120 seconds)
|
||||||
|
notification, err := dbos.Recvstring
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if notification == "paid" {
|
||||||
|
_, err = dbos.RunAsStep(ctx, func(ctx context.Context) (string, error) {
|
||||||
|
return fulfillOrder(orderID)
|
||||||
|
}, dbos.WithStepName("fulfillOrder"))
|
||||||
|
return "fulfilled", err
|
||||||
|
}
|
||||||
|
_, err = dbos.RunAsStep(ctx, func(ctx context.Context) (string, error) {
|
||||||
|
return cancelOrder(orderID)
|
||||||
|
}, dbos.WithStepName("cancelOrder"))
|
||||||
|
return "cancelled", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a message from a webhook handler
|
||||||
|
func paymentWebhook(ctx dbos.DBOSContext, workflowID, status string) error {
|
||||||
|
return dbos.Send(ctx, workflowID, status, "payment_status")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key behaviors:
|
||||||
|
- `Recv` waits for and consumes the next message for the specified topic
|
||||||
|
- Returns the zero value if the wait times out, with a `DBOSError` with code `TimeoutError`
|
||||||
|
- Messages without a topic can only be received by `Recv` without a topic
|
||||||
|
- Messages are queued per-topic (FIFO)
|
||||||
|
|
||||||
|
**Reliability guarantees:**
|
||||||
|
- All messages are persisted to the database
|
||||||
|
- Messages sent from workflows are delivered exactly-once
|
||||||
|
|
||||||
|
Reference: [Workflow Messaging and Notifications](https://docs.dbos.dev/golang/tutorials/workflow-communication#workflow-messaging-and-notifications)
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
title: Use Streams for Real-Time Data
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: Enables streaming results from long-running workflows
|
||||||
|
tags: communication, stream, real-time, channel
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use Streams for Real-Time Data
|
||||||
|
|
||||||
|
Workflows can stream data to clients in real-time using `dbos.WriteStream`, `dbos.CloseStream`, and `dbos.ReadStream`/`dbos.ReadStreamAsync`. Useful for LLM output streaming or progress reporting.
|
||||||
|
|
||||||
|
**Incorrect (accumulating results then returning at end):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func processWorkflow(ctx dbos.DBOSContext, items []string) ([]string, error) {
|
||||||
|
var results []string
|
||||||
|
for _, item := range items {
|
||||||
|
result, _ := dbos.RunAsStep(ctx, func(ctx context.Context) (string, error) {
|
||||||
|
return processItem(item)
|
||||||
|
}, dbos.WithStepName("process"))
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
return results, nil // Client must wait for entire workflow to complete
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (streaming results as they become available):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func processWorkflow(ctx dbos.DBOSContext, items []string) (string, error) {
|
||||||
|
for _, item := range items {
|
||||||
|
result, err := dbos.RunAsStep(ctx, func(ctx context.Context) (string, error) {
|
||||||
|
return processItem(item)
|
||||||
|
}, dbos.WithStepName("process"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
dbos.WriteStream(ctx, "results", result)
|
||||||
|
}
|
||||||
|
dbos.CloseStream(ctx, "results") // Signal completion
|
||||||
|
return "done", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the stream synchronously (blocks until closed)
|
||||||
|
handle, _ := dbos.RunWorkflow(ctx, processWorkflow, items)
|
||||||
|
values, closed, err := dbos.ReadStreamstring, "results")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Async stream reading with channels:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
ch, err := dbos.ReadStreamAsyncstring, "results")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
for sv := range ch {
|
||||||
|
if sv.Err != nil {
|
||||||
|
log.Fatal(sv.Err)
|
||||||
|
}
|
||||||
|
if sv.Closed {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Println("Received:", sv.Value)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key behaviors:
|
||||||
|
- A workflow may have any number of streams, each identified by a unique key
|
||||||
|
- Streams are immutable and append-only
|
||||||
|
- Writes from workflows happen exactly-once
|
||||||
|
- Streams are automatically closed when the workflow terminates
|
||||||
|
- `ReadStream` blocks until the workflow is inactive or the stream is closed
|
||||||
|
- `ReadStreamAsync` returns a channel of `StreamValue[R]` for non-blocking reads
|
||||||
|
|
||||||
|
Reference: [Workflow Streaming](https://docs.dbos.dev/golang/tutorials/workflow-communication#workflow-streaming)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
title: Configure and Launch DBOS Properly
|
||||||
|
impact: CRITICAL
|
||||||
|
impactDescription: Application won't function without proper setup
|
||||||
|
tags: configuration, launch, setup, initialization
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configure and Launch DBOS Properly
|
||||||
|
|
||||||
|
Every DBOS application must create a context, register workflows and queues, then launch before running any workflows.
|
||||||
|
|
||||||
|
**Incorrect (missing configuration or launch):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// No context or launch!
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
return input, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// This will fail - DBOS is not initialized or launched
|
||||||
|
dbos.RegisterWorkflow(nil, myWorkflow) // panic: ctx cannot be nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (create context, register, launch):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
return input, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx, err := dbos.NewDBOSContext(context.Background(), dbos.Config{
|
||||||
|
AppName: "my-app",
|
||||||
|
DatabaseURL: os.Getenv("DBOS_SYSTEM_DATABASE_URL"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer dbos.Shutdown(ctx, 30*time.Second)
|
||||||
|
|
||||||
|
dbos.RegisterWorkflow(ctx, myWorkflow)
|
||||||
|
|
||||||
|
if err := dbos.Launch(ctx); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
handle, err := dbos.RunWorkflow(ctx, myWorkflow, "hello")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
result, err := handle.GetResult()
|
||||||
|
fmt.Println(result) // "hello"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Config fields:
|
||||||
|
- `AppName` (required): Application identifier
|
||||||
|
- `DatabaseURL` (required unless `SystemDBPool` is set): PostgreSQL connection string
|
||||||
|
- `SystemDBPool`: Custom `*pgxpool.Pool` (takes precedence over `DatabaseURL`)
|
||||||
|
- `DatabaseSchema`: Schema name (default: `"dbos"`)
|
||||||
|
- `Logger`: Custom `*slog.Logger` (defaults to stdout)
|
||||||
|
- `AdminServer`: Enable HTTP admin server (default: `false`)
|
||||||
|
- `AdminServerPort`: Admin server port (default: `3001`)
|
||||||
|
- `ApplicationVersion`: App version (auto-computed from binary hash if not set)
|
||||||
|
- `ExecutorID`: Executor identifier (default: `"local"`)
|
||||||
|
- `EnablePatching`: Enable code patching system (default: `false`)
|
||||||
|
|
||||||
|
Reference: [Integrating DBOS](https://docs.dbos.dev/golang/integrating-dbos)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
title: Debounce Workflows to Prevent Wasted Work
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: Prevents redundant workflow executions during rapid triggers
|
||||||
|
tags: pattern, debounce, delay, efficiency
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debounce Workflows to Prevent Wasted Work
|
||||||
|
|
||||||
|
Use `dbos.NewDebouncer` to delay workflow execution until some time has passed since the last trigger. This prevents wasted work when a workflow is triggered multiple times in quick succession.
|
||||||
|
|
||||||
|
**Incorrect (executing on every trigger):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Every keystroke triggers a new workflow - wasteful!
|
||||||
|
func onInputChange(ctx dbos.DBOSContext, userInput string) {
|
||||||
|
dbos.RunWorkflow(ctx, processInput, userInput)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (using Debouncer):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Create debouncer before Launch()
|
||||||
|
debouncer := dbos.NewDebouncer(ctx, processInput,
|
||||||
|
dbos.WithDebouncerTimeout(120*time.Second), // Max wait: 2 minutes
|
||||||
|
)
|
||||||
|
|
||||||
|
func onInputChange(ctx dbos.DBOSContext, userID, userInput string) error {
|
||||||
|
// Delays execution by 60 seconds from the last call
|
||||||
|
// Uses the LAST set of inputs when finally executing
|
||||||
|
_, err := debouncer.Debounce(ctx, userID, 60*time.Second, userInput)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key behaviors:
|
||||||
|
- First argument to `Debounce` is the debounce key, grouping executions together (e.g., per user)
|
||||||
|
- Second argument is the delay duration from the last call
|
||||||
|
- `WithDebouncerTimeout` sets a max wait time since the first trigger
|
||||||
|
- When the workflow finally executes, it uses the **last** set of inputs
|
||||||
|
- After execution begins, the next `Debounce` call starts a new cycle
|
||||||
|
- Debouncers must be created **before** `Launch()`
|
||||||
|
|
||||||
|
Type signature: `Debouncer[P any, R any]` — the type parameters match the target workflow.
|
||||||
|
|
||||||
|
Reference: [Debouncing Workflows](https://docs.dbos.dev/golang/tutorials/workflow-tutorial#debouncing)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
title: Use Workflow IDs for Idempotency
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: Prevents duplicate side effects like double payments
|
||||||
|
tags: pattern, idempotency, workflow-id, deduplication
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use Workflow IDs for Idempotency
|
||||||
|
|
||||||
|
Assign a workflow ID to ensure a workflow executes only once, even if called multiple times. This prevents duplicate side effects like double payments.
|
||||||
|
|
||||||
|
**Incorrect (no idempotency):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func processPayment(ctx dbos.DBOSContext, orderID string) (string, error) {
|
||||||
|
_, err := dbos.RunAsStep(ctx, func(ctx context.Context) (string, error) {
|
||||||
|
return chargeCard(orderID)
|
||||||
|
}, dbos.WithStepName("chargeCard"))
|
||||||
|
return "charged", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple calls could charge the card multiple times!
|
||||||
|
dbos.RunWorkflow(ctx, processPayment, "order-123")
|
||||||
|
dbos.RunWorkflow(ctx, processPayment, "order-123") // Double charge!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (with workflow ID):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func processPayment(ctx dbos.DBOSContext, orderID string) (string, error) {
|
||||||
|
_, err := dbos.RunAsStep(ctx, func(ctx context.Context) (string, error) {
|
||||||
|
return chargeCard(orderID)
|
||||||
|
}, dbos.WithStepName("chargeCard"))
|
||||||
|
return "charged", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same workflow ID = only one execution
|
||||||
|
workflowID := fmt.Sprintf("payment-%s", orderID)
|
||||||
|
dbos.RunWorkflow(ctx, processPayment, "order-123",
|
||||||
|
dbos.WithWorkflowID(workflowID),
|
||||||
|
)
|
||||||
|
dbos.RunWorkflow(ctx, processPayment, "order-123",
|
||||||
|
dbos.WithWorkflowID(workflowID),
|
||||||
|
)
|
||||||
|
// Second call returns the result of the first execution
|
||||||
|
```
|
||||||
|
|
||||||
|
Access the current workflow ID inside a workflow:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
currentID, err := dbos.GetWorkflowID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
fmt.Printf("Running workflow: %s\n", currentID)
|
||||||
|
return input, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Workflow IDs must be **globally unique** for your application. If not set, a random UUID is generated.
|
||||||
|
|
||||||
|
Reference: [Workflow IDs and Idempotency](https://docs.dbos.dev/golang/tutorials/workflow-tutorial#workflow-ids-and-idempotency)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
title: Create Scheduled Workflows
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: Enables recurring tasks with exactly-once-per-interval guarantees
|
||||||
|
tags: pattern, scheduled, cron, recurring
|
||||||
|
---
|
||||||
|
|
||||||
|
## Create Scheduled Workflows
|
||||||
|
|
||||||
|
Use `dbos.WithSchedule` when registering a workflow to run it on a cron schedule. Each scheduled invocation runs exactly once per interval.
|
||||||
|
|
||||||
|
**Incorrect (manual scheduling with goroutine):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Manual scheduling is not durable and misses intervals during downtime
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
generateReport()
|
||||||
|
time.Sleep(60 * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (using WithSchedule):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Scheduled workflow must accept time.Time as input
|
||||||
|
func everyThirtySeconds(ctx dbos.DBOSContext, scheduledTime time.Time) (string, error) {
|
||||||
|
fmt.Println("Running scheduled task at:", scheduledTime)
|
||||||
|
return "done", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dailyReport(ctx dbos.DBOSContext, scheduledTime time.Time) (string, error) {
|
||||||
|
_, err := dbos.RunAsStep(ctx, func(ctx context.Context) (string, error) {
|
||||||
|
return generateReport()
|
||||||
|
}, dbos.WithStepName("generateReport"))
|
||||||
|
return "report generated", err
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx, _ := dbos.NewDBOSContext(context.Background(), config)
|
||||||
|
defer dbos.Shutdown(ctx, 30*time.Second)
|
||||||
|
|
||||||
|
dbos.RegisterWorkflow(ctx, everyThirtySeconds,
|
||||||
|
dbos.WithSchedule("*/30 * * * * *"),
|
||||||
|
)
|
||||||
|
dbos.RegisterWorkflow(ctx, dailyReport,
|
||||||
|
dbos.WithSchedule("0 0 9 * * *"), // 9 AM daily
|
||||||
|
)
|
||||||
|
|
||||||
|
dbos.Launch(ctx)
|
||||||
|
select {} // Block forever
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Scheduled workflows must accept exactly one parameter of type `time.Time` representing the scheduled execution time.
|
||||||
|
|
||||||
|
DBOS crontab uses 6 fields with second precision:
|
||||||
|
```text
|
||||||
|
┌────────────── second
|
||||||
|
│ ┌──────────── minute
|
||||||
|
│ │ ┌────────── hour
|
||||||
|
│ │ │ ┌──────── day of month
|
||||||
|
│ │ │ │ ┌────── month
|
||||||
|
│ │ │ │ │ ┌──── day of week
|
||||||
|
* * * * * *
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference: [Scheduled Workflows](https://docs.dbos.dev/golang/tutorials/workflow-tutorial#scheduled-workflows)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
title: Use Durable Sleep for Delayed Execution
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: Enables reliable scheduling across restarts
|
||||||
|
tags: pattern, sleep, delay, durable, schedule
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use Durable Sleep for Delayed Execution
|
||||||
|
|
||||||
|
Use `dbos.Sleep` for durable delays within workflows. The wakeup time is stored in the database, so the sleep survives restarts.
|
||||||
|
|
||||||
|
**Incorrect (non-durable sleep):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func delayedTask(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
// time.Sleep is not durable - lost on restart!
|
||||||
|
time.Sleep(60 * time.Second)
|
||||||
|
result, err := dbos.RunAsStep(ctx, doWork, dbos.WithStepName("doWork"))
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (durable sleep):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func delayedTask(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
// Durable sleep - survives restarts
|
||||||
|
_, err := dbos.Sleep(ctx, 60*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
result, err := dbos.RunAsStep(ctx, doWork, dbos.WithStepName("doWork"))
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`dbos.Sleep` takes a `time.Duration`. It returns the remaining sleep duration (zero if completed normally).
|
||||||
|
|
||||||
|
Use cases:
|
||||||
|
- Scheduling tasks to run in the future
|
||||||
|
- Implementing retry delays
|
||||||
|
- Delays spanning hours, days, or weeks
|
||||||
|
|
||||||
|
```go
|
||||||
|
func scheduledTask(ctx dbos.DBOSContext, task string) (string, error) {
|
||||||
|
// Sleep for one week
|
||||||
|
dbos.Sleep(ctx, 7*24*time.Hour)
|
||||||
|
return processTask(task)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference: [Durable Sleep](https://docs.dbos.dev/golang/tutorials/workflow-tutorial#durable-sleep)
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
title: Use Queues for Concurrent Workflows
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Queues provide managed concurrency and flow control
|
||||||
|
tags: queue, concurrency, enqueue, workflow
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use Queues for Concurrent Workflows
|
||||||
|
|
||||||
|
Queues run many workflows concurrently with managed flow control. Use them when you need to control how many workflows run at once.
|
||||||
|
|
||||||
|
**Incorrect (uncontrolled concurrency):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Starting many workflows without control - could overwhelm resources
|
||||||
|
for _, task := range tasks {
|
||||||
|
dbos.RunWorkflow(ctx, processTask, task)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (using a queue):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Create queue before Launch()
|
||||||
|
queue := dbos.NewWorkflowQueue(ctx, "task_queue")
|
||||||
|
|
||||||
|
func processAllTasks(ctx dbos.DBOSContext, tasks []string) ([]string, error) {
|
||||||
|
var handles []dbos.WorkflowHandle[string]
|
||||||
|
for _, task := range tasks {
|
||||||
|
handle, err := dbos.RunWorkflow(ctx, processTask, task,
|
||||||
|
dbos.WithQueue(queue.Name),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
handles = append(handles, handle)
|
||||||
|
}
|
||||||
|
// Wait for all tasks
|
||||||
|
var results []string
|
||||||
|
for _, h := range handles {
|
||||||
|
result, err := h.GetResult()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Queues process workflows in FIFO order. All queues must be created with `dbos.NewWorkflowQueue` before `Launch()`.
|
||||||
|
|
||||||
|
Reference: [DBOS Queues](https://docs.dbos.dev/golang/tutorials/queue-tutorial)
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
title: Control Queue Concurrency
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Prevents resource exhaustion with concurrent limits
|
||||||
|
tags: queue, concurrency, workerConcurrency, limits
|
||||||
|
---
|
||||||
|
|
||||||
|
## Control Queue Concurrency
|
||||||
|
|
||||||
|
Queues support worker-level and global concurrency limits to prevent resource exhaustion.
|
||||||
|
|
||||||
|
**Incorrect (no concurrency control):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
queue := dbos.NewWorkflowQueue(ctx, "heavy_tasks") // No limits - could exhaust memory
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (worker concurrency):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Each process runs at most 5 tasks from this queue
|
||||||
|
queue := dbos.NewWorkflowQueue(ctx, "heavy_tasks",
|
||||||
|
dbos.WithWorkerConcurrency(5),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (global concurrency):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// At most 10 tasks run across ALL processes
|
||||||
|
queue := dbos.NewWorkflowQueue(ctx, "limited_tasks",
|
||||||
|
dbos.WithGlobalConcurrency(10),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**In-order processing (sequential):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Only one task at a time - guarantees order
|
||||||
|
serialQueue := dbos.NewWorkflowQueue(ctx, "sequential_queue",
|
||||||
|
dbos.WithGlobalConcurrency(1),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Worker concurrency is recommended for most use cases. Take care with global concurrency as any `PENDING` workflow on the queue counts toward the limit, including workflows from previous application versions.
|
||||||
|
|
||||||
|
When using worker concurrency, each process must have a unique `ExecutorID` set in configuration (this is automatic with DBOS Conductor or Cloud).
|
||||||
|
|
||||||
|
Reference: [Managing Concurrency](https://docs.dbos.dev/golang/tutorials/queue-tutorial#managing-concurrency)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
title: Deduplicate Queued Workflows
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Prevents duplicate workflow executions
|
||||||
|
tags: queue, deduplication, idempotent, duplicate
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deduplicate Queued Workflows
|
||||||
|
|
||||||
|
Set a deduplication ID when enqueuing to prevent duplicate workflow executions. If a workflow with the same deduplication ID is already enqueued or executing, a `DBOSError` with code `QueueDeduplicated` is returned.
|
||||||
|
|
||||||
|
**Incorrect (no deduplication):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Multiple calls could enqueue duplicates
|
||||||
|
func handleClick(ctx dbos.DBOSContext, userID, task string) error {
|
||||||
|
_, err := dbos.RunWorkflow(ctx, processTask, task,
|
||||||
|
dbos.WithQueue(queue.Name),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (with deduplication):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func handleClick(ctx dbos.DBOSContext, userID, task string) error {
|
||||||
|
_, err := dbos.RunWorkflow(ctx, processTask, task,
|
||||||
|
dbos.WithQueue(queue.Name),
|
||||||
|
dbos.WithDeduplicationID(userID),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// Check if it was deduplicated
|
||||||
|
var dbosErr *dbos.DBOSError
|
||||||
|
if errors.As(err, &dbosErr) && dbosErr.Code == dbos.QueueDeduplicated {
|
||||||
|
fmt.Println("Task already in progress for user:", userID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Deduplication is per-queue. The deduplication ID is active while the workflow has status `ENQUEUED` or `PENDING`. Once the workflow completes, a new workflow with the same deduplication ID can be enqueued.
|
||||||
|
|
||||||
|
This is useful for:
|
||||||
|
- Ensuring one active task per user
|
||||||
|
- Preventing duplicate form submissions
|
||||||
|
- Idempotent event processing
|
||||||
|
|
||||||
|
Reference: [Deduplication](https://docs.dbos.dev/golang/tutorials/queue-tutorial#deduplication)
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
title: Control Which Queues a Worker Listens To
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Enables heterogeneous worker pools
|
||||||
|
tags: queue, listen, worker, process, configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
## Control Which Queues a Worker Listens To
|
||||||
|
|
||||||
|
Use `ListenQueues` to make a process only dequeue from specific queues. This enables heterogeneous worker pools.
|
||||||
|
|
||||||
|
**Incorrect (all workers process all queues):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
cpuQueue := dbos.NewWorkflowQueue(ctx, "cpu_queue")
|
||||||
|
gpuQueue := dbos.NewWorkflowQueue(ctx, "gpu_queue")
|
||||||
|
|
||||||
|
// Every worker processes both CPU and GPU tasks
|
||||||
|
// GPU tasks on CPU workers will fail or be slow!
|
||||||
|
dbos.Launch(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (selective queue listening):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
cpuQueue := dbos.NewWorkflowQueue(ctx, "cpu_queue")
|
||||||
|
gpuQueue := dbos.NewWorkflowQueue(ctx, "gpu_queue")
|
||||||
|
|
||||||
|
workerType := os.Getenv("WORKER_TYPE") // "cpu" or "gpu"
|
||||||
|
|
||||||
|
if workerType == "gpu" {
|
||||||
|
ctx.ListenQueues(ctx, gpuQueue)
|
||||||
|
} else if workerType == "cpu" {
|
||||||
|
ctx.ListenQueues(ctx, cpuQueue)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbos.Launch(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
`ListenQueues` only controls dequeuing. A CPU worker can still enqueue tasks onto the GPU queue:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// From a CPU worker, enqueue onto the GPU queue
|
||||||
|
dbos.RunWorkflow(ctx, gpuTask, "data",
|
||||||
|
dbos.WithQueue(gpuQueue.Name),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference: [Listening to Specific Queues](https://docs.dbos.dev/golang/tutorials/queue-tutorial#listening-to-specific-queues)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
title: Partition Queues for Per-Entity Limits
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Enables per-entity concurrency control
|
||||||
|
tags: queue, partition, per-user, dynamic
|
||||||
|
---
|
||||||
|
|
||||||
|
## Partition Queues for Per-Entity Limits
|
||||||
|
|
||||||
|
Partitioned queues apply flow control limits per partition key instead of the entire queue. Each partition acts as a dynamic "subqueue".
|
||||||
|
|
||||||
|
**Incorrect (global concurrency for per-user limits):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Global concurrency=1 blocks ALL users, not per-user
|
||||||
|
queue := dbos.NewWorkflowQueue(ctx, "tasks",
|
||||||
|
dbos.WithGlobalConcurrency(1),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (partitioned queue):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
queue := dbos.NewWorkflowQueue(ctx, "tasks",
|
||||||
|
dbos.WithPartitionQueue(),
|
||||||
|
dbos.WithGlobalConcurrency(1),
|
||||||
|
)
|
||||||
|
|
||||||
|
func onUserTask(ctx dbos.DBOSContext, userID, task string) error {
|
||||||
|
// Each user gets their own partition - at most 1 task per user
|
||||||
|
// but tasks from different users can run concurrently
|
||||||
|
_, err := dbos.RunWorkflow(ctx, processTask, task,
|
||||||
|
dbos.WithQueue(queue.Name),
|
||||||
|
dbos.WithQueuePartitionKey(userID),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When a queue has `WithPartitionQueue()` enabled, you **must** provide a `WithQueuePartitionKey()` when enqueuing. Partition keys and deduplication IDs cannot be used together.
|
||||||
|
|
||||||
|
Reference: [Partitioning Queues](https://docs.dbos.dev/golang/tutorials/queue-tutorial#partitioning-queues)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
title: Set Queue Priority for Workflows
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Prioritizes important workflows over lower-priority ones
|
||||||
|
tags: queue, priority, ordering, importance
|
||||||
|
---
|
||||||
|
|
||||||
|
## Set Queue Priority for Workflows
|
||||||
|
|
||||||
|
Enable priority on a queue to process higher-priority workflows first. Lower numbers indicate higher priority.
|
||||||
|
|
||||||
|
**Incorrect (no priority - FIFO only):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
queue := dbos.NewWorkflowQueue(ctx, "tasks")
|
||||||
|
// All tasks processed in FIFO order regardless of importance
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (priority-enabled queue):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
queue := dbos.NewWorkflowQueue(ctx, "tasks",
|
||||||
|
dbos.WithPriorityEnabled(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// High priority task (lower number = higher priority)
|
||||||
|
dbos.RunWorkflow(ctx, processTask, "urgent-task",
|
||||||
|
dbos.WithQueue(queue.Name),
|
||||||
|
dbos.WithPriority(1),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Low priority task
|
||||||
|
dbos.RunWorkflow(ctx, processTask, "background-task",
|
||||||
|
dbos.WithQueue(queue.Name),
|
||||||
|
dbos.WithPriority(100),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Priority rules:
|
||||||
|
- Range: `1` to `2,147,483,647`
|
||||||
|
- Lower number = higher priority
|
||||||
|
- Workflows **without** assigned priorities have the highest priority (run first)
|
||||||
|
- Workflows with the same priority are dequeued in FIFO order
|
||||||
|
|
||||||
|
Reference: [Priority](https://docs.dbos.dev/golang/tutorials/queue-tutorial#priority)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
title: Rate Limit Queue Execution
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Prevents overwhelming external APIs with too many requests
|
||||||
|
tags: queue, rate-limit, throttle, api
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rate Limit Queue Execution
|
||||||
|
|
||||||
|
Set rate limits on a queue to control how many workflows start in a given period. Rate limits are global across all DBOS processes.
|
||||||
|
|
||||||
|
**Incorrect (no rate limiting):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
queue := dbos.NewWorkflowQueue(ctx, "llm_tasks")
|
||||||
|
// Could send hundreds of requests per second to a rate-limited API
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (rate-limited queue):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
queue := dbos.NewWorkflowQueue(ctx, "llm_tasks",
|
||||||
|
dbos.WithRateLimiter(&dbos.RateLimiter{
|
||||||
|
Limit: 50,
|
||||||
|
Period: 30 * time.Second,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
This queue starts at most 50 workflows per 30 seconds.
|
||||||
|
|
||||||
|
**Combining rate limiting with concurrency:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// At most 5 concurrent and 50 per 30 seconds
|
||||||
|
queue := dbos.NewWorkflowQueue(ctx, "api_tasks",
|
||||||
|
dbos.WithWorkerConcurrency(5),
|
||||||
|
dbos.WithRateLimiter(&dbos.RateLimiter{
|
||||||
|
Limit: 50,
|
||||||
|
Period: 30 * time.Second,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Common use cases:
|
||||||
|
- LLM API rate limiting (OpenAI, Anthropic, etc.)
|
||||||
|
- Third-party API throttling
|
||||||
|
- Preventing database overload
|
||||||
|
|
||||||
|
Reference: [Rate Limiting](https://docs.dbos.dev/golang/tutorials/queue-tutorial#rate-limiting)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
title: Use Steps for External Operations
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Steps enable recovery by checkpointing results
|
||||||
|
tags: step, external, api, checkpoint
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use Steps for External Operations
|
||||||
|
|
||||||
|
Any function that performs complex operations, accesses external APIs, or has side effects should be a step. Step results are checkpointed, enabling workflow recovery.
|
||||||
|
|
||||||
|
**Incorrect (external call in workflow):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
// External API call directly in workflow - not checkpointed!
|
||||||
|
resp, err := http.Get("https://api.example.com/data")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return string(body), nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (external call in step using `dbos.RunAsStep`):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func fetchData(ctx context.Context) (string, error) {
|
||||||
|
resp, err := http.Get("https://api.example.com/data")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return string(body), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
data, err := dbos.RunAsStep(ctx, fetchData, dbos.WithStepName("fetchData"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`dbos.RunAsStep` can also accept an inline closure:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
data, err := dbos.RunAsStep(ctx, func(ctx context.Context) (string, error) {
|
||||||
|
resp, err := http.Get("https://api.example.com/data")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return string(body), nil
|
||||||
|
}, dbos.WithStepName("fetchData"))
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Step type signature: `type Step[R any] func(ctx context.Context) (R, error)`
|
||||||
|
|
||||||
|
Step requirements:
|
||||||
|
- The function must accept a `context.Context` parameter — use the one provided, not the workflow's context
|
||||||
|
- Inputs and outputs must be serializable to JSON
|
||||||
|
- Cannot start or enqueue workflows from within steps
|
||||||
|
- Calling a step from within another step makes the inner call part of the outer step's execution
|
||||||
|
|
||||||
|
When to use steps:
|
||||||
|
- API calls to external services
|
||||||
|
- File system operations
|
||||||
|
- Random number generation
|
||||||
|
- Getting current time
|
||||||
|
- Any non-deterministic operation
|
||||||
|
|
||||||
|
Reference: [DBOS Steps](https://docs.dbos.dev/golang/tutorials/step-tutorial)
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
title: Run Concurrent Steps with Go and Select
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Enables parallel execution of steps with durable checkpointing
|
||||||
|
tags: step, concurrency, goroutine, select, parallel
|
||||||
|
---
|
||||||
|
|
||||||
|
## Run Concurrent Steps with Go and Select
|
||||||
|
|
||||||
|
Use `dbos.Go` to run steps concurrently in goroutines and `dbos.Select` to durably select the first completed result. Both operations are checkpointed for recovery.
|
||||||
|
|
||||||
|
**Incorrect (raw goroutines without checkpointing):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
// Raw goroutines are not checkpointed - recovery breaks!
|
||||||
|
ch := make(chan string, 2)
|
||||||
|
go func() { ch <- callAPI1() }()
|
||||||
|
go func() { ch <- callAPI2() }()
|
||||||
|
return <-ch, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (using dbos.Go for concurrent steps):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
// Start steps concurrently
|
||||||
|
ch1, err := dbos.Go(ctx, func(ctx context.Context) (string, error) {
|
||||||
|
return callAPI1(ctx)
|
||||||
|
}, dbos.WithStepName("api1"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
ch2, err := dbos.Go(ctx, func(ctx context.Context) (string, error) {
|
||||||
|
return callAPI2(ctx)
|
||||||
|
}, dbos.WithStepName("api2"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the first result (durable select)
|
||||||
|
result, err := dbos.Select(ctx, []<-chan dbos.StepOutcome[string]{ch1, ch2})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Waiting for all concurrent steps:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) ([]string, error) {
|
||||||
|
ch1, _ := dbos.Go(ctx, step1, dbos.WithStepName("step1"))
|
||||||
|
ch2, _ := dbos.Go(ctx, step2, dbos.WithStepName("step2"))
|
||||||
|
ch3, _ := dbos.Go(ctx, step3, dbos.WithStepName("step3"))
|
||||||
|
|
||||||
|
// Collect all results
|
||||||
|
results := make([]string, 3)
|
||||||
|
for i, ch := range []<-chan dbos.StepOutcome[string]{ch1, ch2, ch3} {
|
||||||
|
outcome := <-ch
|
||||||
|
if outcome.Err != nil {
|
||||||
|
return nil, outcome.Err
|
||||||
|
}
|
||||||
|
results[i] = outcome.Result
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key behaviors:
|
||||||
|
- `dbos.Go` starts a step in a goroutine and returns a channel of `StepOutcome[R]`
|
||||||
|
- `dbos.Select` durably selects the first completed result and checkpoints which channel was selected
|
||||||
|
- On recovery, `Select` replays the same selection, maintaining determinism
|
||||||
|
- Steps started with `Go` follow the same retry and checkpointing rules as `RunAsStep`
|
||||||
|
|
||||||
|
Reference: [Concurrent Steps](https://docs.dbos.dev/golang/tutorials/workflow-tutorial#concurrent-steps)
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
title: Configure Step Retries for Transient Failures
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Automatic retries handle transient failures without manual code
|
||||||
|
tags: step, retry, exponential-backoff, resilience
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configure Step Retries for Transient Failures
|
||||||
|
|
||||||
|
Steps can automatically retry on failure with exponential backoff. This handles transient failures like network issues.
|
||||||
|
|
||||||
|
**Incorrect (manual retry logic):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func fetchData(ctx context.Context) (string, error) {
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 0; attempt < 3; attempt++ {
|
||||||
|
resp, err := http.Get("https://api.example.com")
|
||||||
|
if err == nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return string(body), nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
time.Sleep(time.Duration(math.Pow(2, float64(attempt))) * time.Second)
|
||||||
|
}
|
||||||
|
return "", lastErr
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (built-in retries with `dbos.RunAsStep`):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func fetchData(ctx context.Context) (string, error) {
|
||||||
|
resp, err := http.Get("https://api.example.com")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return string(body), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
data, err := dbos.RunAsStep(ctx, fetchData,
|
||||||
|
dbos.WithStepName("fetchData"),
|
||||||
|
dbos.WithStepMaxRetries(10),
|
||||||
|
dbos.WithBaseInterval(500*time.Millisecond),
|
||||||
|
dbos.WithBackoffFactor(2.0),
|
||||||
|
dbos.WithMaxInterval(5*time.Second),
|
||||||
|
)
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Retry parameters:
|
||||||
|
- `WithStepMaxRetries(n)`: Maximum retry attempts (default: `0` — no retries)
|
||||||
|
- `WithBaseInterval(d)`: Initial delay between retries (default: `100ms`)
|
||||||
|
- `WithBackoffFactor(f)`: Multiplier for exponential backoff (default: `2.0`)
|
||||||
|
- `WithMaxInterval(d)`: Maximum delay between retries (default: `5s`)
|
||||||
|
|
||||||
|
With defaults, retry delays are: 100ms, 200ms, 400ms, 800ms, 1.6s, 3.2s, 5s, 5s...
|
||||||
|
|
||||||
|
If all retries are exhausted, a `DBOSError` with code `MaxStepRetriesExceeded` is returned to the calling workflow.
|
||||||
|
|
||||||
|
Reference: [Configurable Retries](https://docs.dbos.dev/golang/tutorials/step-tutorial#configurable-retries)
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
title: Use Proper Test Setup for DBOS
|
||||||
|
impact: LOW-MEDIUM
|
||||||
|
impactDescription: Ensures consistent test results with proper DBOS lifecycle management
|
||||||
|
tags: testing, go-test, setup, integration, mock
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use Proper Test Setup for DBOS
|
||||||
|
|
||||||
|
DBOS applications can be tested with unit tests (mocking DBOSContext) or integration tests (real Postgres database).
|
||||||
|
|
||||||
|
**Incorrect (no lifecycle management between tests):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Tests share state - results are inconsistent!
|
||||||
|
func TestOne(t *testing.T) {
|
||||||
|
myWorkflow(ctx, "input")
|
||||||
|
}
|
||||||
|
func TestTwo(t *testing.T) {
|
||||||
|
// Previous test's state leaks into this test
|
||||||
|
myWorkflow(ctx, "input")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (unit testing with mocks):**
|
||||||
|
|
||||||
|
The `DBOSContext` interface is fully mockable. Use a mocking library like `testify/mock` or `mockery`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestWorkflow(t *testing.T) {
|
||||||
|
mockCtx := mocks.NewMockDBOSContext(t)
|
||||||
|
|
||||||
|
// Mock RunAsStep to return a canned value
|
||||||
|
mockCtx.On("RunAsStep", mockCtx, mock.Anything, mock.Anything).
|
||||||
|
Return("mock-result", nil)
|
||||||
|
|
||||||
|
result, err := myWorkflow(mockCtx, "input")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "expected", result)
|
||||||
|
|
||||||
|
mockCtx.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (integration testing with Postgres):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func setupDBOS(t *testing.T) dbos.DBOSContext {
|
||||||
|
t.Helper()
|
||||||
|
databaseURL := os.Getenv("DBOS_TEST_DATABASE_URL")
|
||||||
|
if databaseURL == "" {
|
||||||
|
t.Skip("DBOS_TEST_DATABASE_URL not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, err := dbos.NewDBOSContext(context.Background(), dbos.Config{
|
||||||
|
AppName: "test-" + t.Name(),
|
||||||
|
DatabaseURL: databaseURL,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dbos.RegisterWorkflow(ctx, myWorkflow)
|
||||||
|
|
||||||
|
err = dbos.Launch(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
dbos.Shutdown(ctx, 10*time.Second)
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorkflowIntegration(t *testing.T) {
|
||||||
|
ctx := setupDBOS(t)
|
||||||
|
|
||||||
|
handle, err := dbos.RunWorkflow(ctx, myWorkflow, "test-input")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
result, err := handle.GetResult()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "expected-output", result)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key points:
|
||||||
|
- Use `t.Cleanup` to ensure `Shutdown` is called after each test
|
||||||
|
- Use unique `AppName` per test to avoid collisions
|
||||||
|
- Mock `DBOSContext` for fast unit tests without Postgres
|
||||||
|
- Use real Postgres for integration tests that verify durable behavior
|
||||||
|
|
||||||
|
Reference: [Testing DBOS](https://docs.dbos.dev/golang/tutorials/testing)
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
title: Start Workflows in Background
|
||||||
|
impact: CRITICAL
|
||||||
|
impactDescription: Background workflows enable reliable async processing
|
||||||
|
tags: workflow, background, handle, async
|
||||||
|
---
|
||||||
|
|
||||||
|
## Start Workflows in Background
|
||||||
|
|
||||||
|
Use `dbos.RunWorkflow` to start a workflow and get a handle to track it. The workflow is guaranteed to run to completion even if the app is interrupted.
|
||||||
|
|
||||||
|
**Incorrect (no way to track background work):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func processData(ctx dbos.DBOSContext, data string) (string, error) {
|
||||||
|
// ...
|
||||||
|
return "processed: " + data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire and forget in a goroutine - no durability, no tracking
|
||||||
|
go func() {
|
||||||
|
processData(ctx, data)
|
||||||
|
}()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (using RunWorkflow):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func processData(ctx dbos.DBOSContext, data string) (string, error) {
|
||||||
|
return "processed: " + data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// ... setup and launch ...
|
||||||
|
|
||||||
|
// Start workflow, get handle
|
||||||
|
handle, err := dbos.RunWorkflow(ctx, processData, "input")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the workflow ID
|
||||||
|
fmt.Println(handle.GetWorkflowID())
|
||||||
|
|
||||||
|
// Wait for result
|
||||||
|
result, err := handle.GetResult()
|
||||||
|
|
||||||
|
// Check status
|
||||||
|
status, err := handle.GetStatus()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Retrieve a handle later by workflow ID:
|
||||||
|
|
||||||
|
```go
|
||||||
|
handle, err := dbos.RetrieveWorkflowstring
|
||||||
|
result, err := handle.GetResult()
|
||||||
|
```
|
||||||
|
|
||||||
|
`GetResult` supports options:
|
||||||
|
- `dbos.WithHandleTimeout(timeout)`: Return a timeout error if the workflow doesn't complete within the duration
|
||||||
|
- `dbos.WithHandlePollingInterval(interval)`: Control how often the database is polled for completion
|
||||||
|
|
||||||
|
Reference: [Workflows](https://docs.dbos.dev/golang/tutorials/workflow-tutorial)
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
title: Follow Workflow Constraints
|
||||||
|
impact: CRITICAL
|
||||||
|
impactDescription: Violating constraints breaks recovery and durability guarantees
|
||||||
|
tags: workflow, constraints, rules, best-practices
|
||||||
|
---
|
||||||
|
|
||||||
|
## Follow Workflow Constraints
|
||||||
|
|
||||||
|
Workflows have specific constraints to maintain durability guarantees. Violating them can break recovery.
|
||||||
|
|
||||||
|
**Incorrect (starting workflows from steps):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func myStep(ctx context.Context) (string, error) {
|
||||||
|
// Don't start workflows from steps!
|
||||||
|
// The step's context.Context does not support workflow operations
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
// Starting a child workflow inside a step breaks determinism
|
||||||
|
dbos.RunAsStep(ctx, func(ctx context.Context) (string, error) {
|
||||||
|
handle, _ := dbos.RunWorkflow(ctx.(dbos.DBOSContext), otherWorkflow, "data") // WRONG
|
||||||
|
return handle.GetWorkflowID(), nil
|
||||||
|
})
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (workflow operations only from workflows):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func fetchData(ctx context.Context) (string, error) {
|
||||||
|
// Steps only do external operations
|
||||||
|
resp, err := http.Get("https://api.example.com")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return string(body), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func myWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
data, err := dbos.RunAsStep(ctx, fetchData, dbos.WithStepName("fetchData"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// Start child workflows from the parent workflow
|
||||||
|
handle, err := dbos.RunWorkflow(ctx, otherWorkflow, data)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// Receive messages from the workflow
|
||||||
|
msg, err := dbos.Recvstring
|
||||||
|
// Set events from the workflow
|
||||||
|
dbos.SetEvent(ctx, "status", "done")
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Additional constraints:
|
||||||
|
- Don't modify global variables from workflows or steps
|
||||||
|
- All workflows and queues must be registered **before** `Launch()`
|
||||||
|
- Concurrent steps must start in deterministic order using `dbos.Go`/`dbos.Select`
|
||||||
|
|
||||||
|
Reference: [Workflow Guarantees](https://docs.dbos.dev/golang/tutorials/workflow-tutorial#workflow-guarantees)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
title: Cancel, Resume, and Fork Workflows
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: Enables operational control over long-running workflows
|
||||||
|
tags: workflow, cancel, resume, fork, management
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cancel, Resume, and Fork Workflows
|
||||||
|
|
||||||
|
DBOS provides functions to cancel, resume, and fork workflows for operational control.
|
||||||
|
|
||||||
|
**Incorrect (no way to handle stuck or failed workflows):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Workflow is stuck or failed - no recovery mechanism
|
||||||
|
handle, _ := dbos.RunWorkflow(ctx, processTask, "data")
|
||||||
|
// If the workflow fails, there's no way to retry or recover
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (using cancel, resume, and fork):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Cancel a workflow - stops at its next step
|
||||||
|
err := dbos.CancelWorkflow(ctx, workflowID)
|
||||||
|
|
||||||
|
// Resume from the last completed step
|
||||||
|
handle, err := dbos.ResumeWorkflowstring
|
||||||
|
result, err := handle.GetResult()
|
||||||
|
```
|
||||||
|
|
||||||
|
Cancellation sets the workflow status to `CANCELLED` and preempts execution at the beginning of the next step. Cancelling also cancels all child workflows.
|
||||||
|
|
||||||
|
Resume restarts a workflow from its last completed step. Use this for workflows that are cancelled or have exceeded their maximum recovery attempts. You can also use this to start an enqueued workflow immediately, bypassing its queue.
|
||||||
|
|
||||||
|
Fork a workflow from a specific step:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// List steps to find the right step ID
|
||||||
|
steps, err := dbos.GetWorkflowSteps(ctx, workflowID)
|
||||||
|
|
||||||
|
// Fork from a specific step
|
||||||
|
forkHandle, err := dbos.ForkWorkflowstring
|
||||||
|
result, err := forkHandle.GetResult()
|
||||||
|
```
|
||||||
|
|
||||||
|
Forking creates a new workflow with a new ID, copying the original workflow's inputs and step outputs up to the selected step.
|
||||||
|
|
||||||
|
Reference: [Workflow Management](https://docs.dbos.dev/golang/tutorials/workflow-management)
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
title: Keep Workflows Deterministic
|
||||||
|
impact: CRITICAL
|
||||||
|
impactDescription: Non-deterministic workflows cannot recover correctly
|
||||||
|
tags: workflow, determinism, recovery, reliability
|
||||||
|
---
|
||||||
|
|
||||||
|
## Keep Workflows Deterministic
|
||||||
|
|
||||||
|
Workflow functions must be deterministic: given the same inputs and step return values, they must invoke the same steps in the same order. Non-deterministic operations must be moved to steps.
|
||||||
|
|
||||||
|
**Incorrect (non-deterministic workflow):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func exampleWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
// Random value in workflow breaks recovery!
|
||||||
|
// On replay, rand.Intn returns a different value,
|
||||||
|
// so the workflow may take a different branch.
|
||||||
|
if rand.Intn(2) == 0 {
|
||||||
|
return stepOne(ctx)
|
||||||
|
}
|
||||||
|
return stepTwo(ctx)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (non-determinism in step):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
func exampleWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
|
||||||
|
// Step result is checkpointed - replay uses the saved value
|
||||||
|
choice, err := dbos.RunAsStep(ctx, func(ctx context.Context) (int, error) {
|
||||||
|
return rand.Intn(2), nil
|
||||||
|
}, dbos.WithStepName("generateChoice"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if choice == 0 {
|
||||||
|
return stepOne(ctx)
|
||||||
|
}
|
||||||
|
return stepTwo(ctx)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Non-deterministic operations that must be in steps:
|
||||||
|
- Random number generation
|
||||||
|
- Getting current time (`time.Now()`)
|
||||||
|
- Accessing external APIs (`http.Get`, etc.)
|
||||||
|
- Reading files
|
||||||
|
- Database queries
|
||||||
|
|
||||||
|
Reference: [Workflow Determinism](https://docs.dbos.dev/golang/tutorials/workflow-tutorial#determinism)
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
title: List and Inspect Workflows
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: Enables monitoring and debugging of workflow executions
|
||||||
|
tags: workflow, list, inspect, status, monitoring
|
||||||
|
---
|
||||||
|
|
||||||
|
## List and Inspect Workflows
|
||||||
|
|
||||||
|
Use `dbos.ListWorkflows` to query workflow executions by status, name, time range, and other criteria.
|
||||||
|
|
||||||
|
**Incorrect (no monitoring of workflow state):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Start workflow with no way to check on it later
|
||||||
|
dbos.RunWorkflow(ctx, processTask, "data")
|
||||||
|
// If something goes wrong, no way to find or debug it
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (listing and inspecting workflows):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// List workflows by status
|
||||||
|
erroredWorkflows, err := dbos.ListWorkflows(ctx,
|
||||||
|
dbos.WithStatus([]dbos.WorkflowStatusType{dbos.WorkflowStatusError}),
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, wf := range erroredWorkflows {
|
||||||
|
fmt.Printf("Workflow %s: %s - %v\n", wf.ID, wf.Name, wf.Error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
List workflows with multiple filters:
|
||||||
|
|
||||||
|
```go
|
||||||
|
workflows, err := dbos.ListWorkflows(ctx,
|
||||||
|
dbos.WithName("processOrder"),
|
||||||
|
dbos.WithStatus([]dbos.WorkflowStatusType{dbos.WorkflowStatusSuccess}),
|
||||||
|
dbos.WithLimit(100),
|
||||||
|
dbos.WithSortDesc(),
|
||||||
|
dbos.WithLoadOutput(true),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
List workflow steps:
|
||||||
|
|
||||||
|
```go
|
||||||
|
steps, err := dbos.GetWorkflowSteps(ctx, workflowID)
|
||||||
|
for _, step := range steps {
|
||||||
|
fmt.Printf("Step %d: %s\n", step.StepID, step.StepName)
|
||||||
|
if step.Error != nil {
|
||||||
|
fmt.Printf(" Error: %v\n", step.Error)
|
||||||
|
}
|
||||||
|
if step.ChildWorkflowID != "" {
|
||||||
|
fmt.Printf(" Child: %s\n", step.ChildWorkflowID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Workflow status values: `WorkflowStatusPending`, `WorkflowStatusEnqueued`, `WorkflowStatusSuccess`, `WorkflowStatusError`, `WorkflowStatusCancelled`, `WorkflowStatusMaxRecoveryAttemptsExceeded`
|
||||||
|
|
||||||
|
To optimize performance, avoid loading inputs/outputs when you don't need them (they are not loaded by default).
|
||||||
|
|
||||||
|
Reference: [Workflow Management](https://docs.dbos.dev/golang/tutorials/workflow-management#listing-workflows)
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
title: Set Workflow Timeouts
|
||||||
|
impact: CRITICAL
|
||||||
|
impactDescription: Prevents workflows from running indefinitely
|
||||||
|
tags: workflow, timeout, cancellation, duration
|
||||||
|
---
|
||||||
|
|
||||||
|
## Set Workflow Timeouts
|
||||||
|
|
||||||
|
Set a timeout for a workflow by using Go's `context.WithTimeout` or `dbos.WithTimeout` on the DBOS context. When the timeout expires, the workflow and all its children are cancelled.
|
||||||
|
|
||||||
|
**Incorrect (no timeout for potentially long workflow):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// No timeout - could run indefinitely
|
||||||
|
handle, err := dbos.RunWorkflow(ctx, processTask, "data")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (with timeout):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Create a context with a 5-minute timeout
|
||||||
|
timedCtx, cancel := dbos.WithTimeout(ctx, 5*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
handle, err := dbos.RunWorkflow(timedCtx, processTask, "data")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key timeout behaviors:
|
||||||
|
- Timeouts are **start-to-completion**: the timeout begins when the workflow starts execution, not when it's enqueued
|
||||||
|
- Timeouts are **durable**: they persist across restarts, so workflows can have very long timeouts (hours, days, weeks)
|
||||||
|
- Cancellation happens at the **beginning of the next step** - the current step completes first
|
||||||
|
- Cancelling a workflow also cancels all **child workflows**
|
||||||
|
|
||||||
|
Reference: [Workflow Timeouts](https://docs.dbos.dev/golang/tutorials/workflow-tutorial#workflow-timeouts)
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# dbos-typescript
|
||||||
|
|
||||||
|
> **Note:** `CLAUDE.md` is a symlink to this file.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
DBOS TypeScript SDK for building reliable, fault-tolerant applications with durable workflows. Use this skill when writing TypeScript code with DBOS, creating workflows and steps, using queues, using DBOSClient from external applications, or building applications that need to be resilient to failures.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
dbos-typescript/
|
||||||
|
SKILL.md # Main skill file - read this first
|
||||||
|
AGENTS.md # This navigation guide
|
||||||
|
CLAUDE.md # Symlink to AGENTS.md
|
||||||
|
references/ # Detailed reference files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Read `SKILL.md` for the main skill instructions
|
||||||
|
2. Browse `references/` for detailed documentation on specific topics
|
||||||
|
3. Reference files are loaded on-demand - read only what you need
|
||||||
|
|
||||||
|
## Reference Categories
|
||||||
|
|
||||||
|
| Priority | Category | Impact | Prefix |
|
||||||
|
|----------|----------|--------|--------|
|
||||||
|
| 1 | Lifecycle | CRITICAL | `lifecycle-` |
|
||||||
|
| 2 | Workflow | CRITICAL | `workflow-` |
|
||||||
|
| 3 | Step | HIGH | `step-` |
|
||||||
|
| 4 | Queue | HIGH | `queue-` |
|
||||||
|
| 5 | Communication | MEDIUM | `comm-` |
|
||||||
|
| 6 | Pattern | MEDIUM | `pattern-` |
|
||||||
|
| 7 | Testing | LOW-MEDIUM | `test-` |
|
||||||
|
| 8 | Client | MEDIUM | `client-` |
|
||||||
|
| 9 | Advanced | LOW | `advanced-` |
|
||||||
|
|
||||||
|
Reference files are named `{prefix}-{topic}.md` (e.g., `query-missing-indexes.md`).
|
||||||
|
|
||||||
|
## Available References
|
||||||
|
|
||||||
|
**Advanced** (`advanced-`):
|
||||||
|
- `references/advanced-patching.md`
|
||||||
|
- `references/advanced-versioning.md`
|
||||||
|
|
||||||
|
**Client** (`client-`):
|
||||||
|
- `references/client-enqueue.md`
|
||||||
|
- `references/client-setup.md`
|
||||||
|
|
||||||
|
**Communication** (`comm-`):
|
||||||
|
- `references/comm-events.md`
|
||||||
|
- `references/comm-messages.md`
|
||||||
|
- `references/comm-streaming.md`
|
||||||
|
|
||||||
|
**Lifecycle** (`lifecycle-`):
|
||||||
|
- `references/lifecycle-config.md`
|
||||||
|
- `references/lifecycle-express.md`
|
||||||
|
|
||||||
|
**Pattern** (`pattern-`):
|
||||||
|
- `references/pattern-classes.md`
|
||||||
|
- `references/pattern-debouncing.md`
|
||||||
|
- `references/pattern-idempotency.md`
|
||||||
|
- `references/pattern-scheduled.md`
|
||||||
|
- `references/pattern-sleep.md`
|
||||||
|
|
||||||
|
**Queue** (`queue-`):
|
||||||
|
- `references/queue-basics.md`
|
||||||
|
- `references/queue-concurrency.md`
|
||||||
|
- `references/queue-deduplication.md`
|
||||||
|
- `references/queue-listening.md`
|
||||||
|
- `references/queue-partitioning.md`
|
||||||
|
- `references/queue-priority.md`
|
||||||
|
- `references/queue-rate-limiting.md`
|
||||||
|
|
||||||
|
**Step** (`step-`):
|
||||||
|
- `references/step-basics.md`
|
||||||
|
- `references/step-retries.md`
|
||||||
|
- `references/step-transactions.md`
|
||||||
|
|
||||||
|
**Testing** (`test-`):
|
||||||
|
- `references/test-setup.md`
|
||||||
|
|
||||||
|
**Workflow** (`workflow-`):
|
||||||
|
- `references/workflow-background.md`
|
||||||
|
- `references/workflow-constraints.md`
|
||||||
|
- `references/workflow-control.md`
|
||||||
|
- `references/workflow-determinism.md`
|
||||||
|
- `references/workflow-introspection.md`
|
||||||
|
- `references/workflow-timeout.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*31 reference files across 9 categories*
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# dbos-typescript
|
||||||
|
|
||||||
|
> **Note:** `CLAUDE.md` is a symlink to this file.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
DBOS TypeScript SDK for building reliable, fault-tolerant applications with durable workflows. Use this skill when writing TypeScript code with DBOS, creating workflows and steps, using queues, using DBOSClient from external applications, or building applications that need to be resilient to failures.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
dbos-typescript/
|
||||||
|
SKILL.md # Main skill file - read this first
|
||||||
|
AGENTS.md # This navigation guide
|
||||||
|
CLAUDE.md # Symlink to AGENTS.md
|
||||||
|
references/ # Detailed reference files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Read `SKILL.md` for the main skill instructions
|
||||||
|
2. Browse `references/` for detailed documentation on specific topics
|
||||||
|
3. Reference files are loaded on-demand - read only what you need
|
||||||
|
|
||||||
|
## Reference Categories
|
||||||
|
|
||||||
|
| Priority | Category | Impact | Prefix |
|
||||||
|
|----------|----------|--------|--------|
|
||||||
|
| 1 | Lifecycle | CRITICAL | `lifecycle-` |
|
||||||
|
| 2 | Workflow | CRITICAL | `workflow-` |
|
||||||
|
| 3 | Step | HIGH | `step-` |
|
||||||
|
| 4 | Queue | HIGH | `queue-` |
|
||||||
|
| 5 | Communication | MEDIUM | `comm-` |
|
||||||
|
| 6 | Pattern | MEDIUM | `pattern-` |
|
||||||
|
| 7 | Testing | LOW-MEDIUM | `test-` |
|
||||||
|
| 8 | Client | MEDIUM | `client-` |
|
||||||
|
| 9 | Advanced | LOW | `advanced-` |
|
||||||
|
|
||||||
|
Reference files are named `{prefix}-{topic}.md` (e.g., `query-missing-indexes.md`).
|
||||||
|
|
||||||
|
## Available References
|
||||||
|
|
||||||
|
**Advanced** (`advanced-`):
|
||||||
|
- `references/advanced-patching.md`
|
||||||
|
- `references/advanced-versioning.md`
|
||||||
|
|
||||||
|
**Client** (`client-`):
|
||||||
|
- `references/client-enqueue.md`
|
||||||
|
- `references/client-setup.md`
|
||||||
|
|
||||||
|
**Communication** (`comm-`):
|
||||||
|
- `references/comm-events.md`
|
||||||
|
- `references/comm-messages.md`
|
||||||
|
- `references/comm-streaming.md`
|
||||||
|
|
||||||
|
**Lifecycle** (`lifecycle-`):
|
||||||
|
- `references/lifecycle-config.md`
|
||||||
|
- `references/lifecycle-express.md`
|
||||||
|
|
||||||
|
**Pattern** (`pattern-`):
|
||||||
|
- `references/pattern-classes.md`
|
||||||
|
- `references/pattern-debouncing.md`
|
||||||
|
- `references/pattern-idempotency.md`
|
||||||
|
- `references/pattern-scheduled.md`
|
||||||
|
- `references/pattern-sleep.md`
|
||||||
|
|
||||||
|
**Queue** (`queue-`):
|
||||||
|
- `references/queue-basics.md`
|
||||||
|
- `references/queue-concurrency.md`
|
||||||
|
- `references/queue-deduplication.md`
|
||||||
|
- `references/queue-listening.md`
|
||||||
|
- `references/queue-partitioning.md`
|
||||||
|
- `references/queue-priority.md`
|
||||||
|
- `references/queue-rate-limiting.md`
|
||||||
|
|
||||||
|
**Step** (`step-`):
|
||||||
|
- `references/step-basics.md`
|
||||||
|
- `references/step-retries.md`
|
||||||
|
- `references/step-transactions.md`
|
||||||
|
|
||||||
|
**Testing** (`test-`):
|
||||||
|
- `references/test-setup.md`
|
||||||
|
|
||||||
|
**Workflow** (`workflow-`):
|
||||||
|
- `references/workflow-background.md`
|
||||||
|
- `references/workflow-constraints.md`
|
||||||
|
- `references/workflow-control.md`
|
||||||
|
- `references/workflow-determinism.md`
|
||||||
|
- `references/workflow-introspection.md`
|
||||||
|
- `references/workflow-timeout.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*31 reference files across 9 categories*
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
---
|
||||||
|
name: dbos-typescript
|
||||||
|
description: "Guide for building reliable, fault-tolerant TypeScript applications with DBOS durable workflows. Use when adding DBOS to existing TypeScript code, creating workflows and steps, or using queues for concurrency control."
|
||||||
|
risk: safe
|
||||||
|
source: "https://docs.dbos.dev/"
|
||||||
|
date_added: "2026-02-27"
|
||||||
|
---
|
||||||
|
|
||||||
|
# DBOS TypeScript Best Practices
|
||||||
|
|
||||||
|
Guide for building reliable, fault-tolerant TypeScript applications with DBOS durable workflows.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
Reference these guidelines when:
|
||||||
|
- Adding DBOS to existing TypeScript code
|
||||||
|
- Creating workflows and steps
|
||||||
|
- Using queues for concurrency control
|
||||||
|
- Implementing workflow communication (events, messages, streams)
|
||||||
|
- Configuring and launching DBOS applications
|
||||||
|
- Using DBOSClient from external applications
|
||||||
|
- Testing DBOS applications
|
||||||
|
|
||||||
|
## Rule Categories by Priority
|
||||||
|
|
||||||
|
| Priority | Category | Impact | Prefix |
|
||||||
|
|----------|----------|--------|--------|
|
||||||
|
| 1 | Lifecycle | CRITICAL | `lifecycle-` |
|
||||||
|
| 2 | Workflow | CRITICAL | `workflow-` |
|
||||||
|
| 3 | Step | HIGH | `step-` |
|
||||||
|
| 4 | Queue | HIGH | `queue-` |
|
||||||
|
| 5 | Communication | MEDIUM | `comm-` |
|
||||||
|
| 6 | Pattern | MEDIUM | `pattern-` |
|
||||||
|
| 7 | Testing | LOW-MEDIUM | `test-` |
|
||||||
|
| 8 | Client | MEDIUM | `client-` |
|
||||||
|
| 9 | Advanced | LOW | `advanced-` |
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Always install the latest version of DBOS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @dbos-inc/dbos-sdk@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### DBOS Configuration and Launch
|
||||||
|
|
||||||
|
A DBOS application MUST configure and launch DBOS before running any workflows:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
DBOS.setConfig({
|
||||||
|
name: "my-app",
|
||||||
|
systemDatabaseUrl: process.env.DBOS_SYSTEM_DATABASE_URL,
|
||||||
|
});
|
||||||
|
await DBOS.launch();
|
||||||
|
await myWorkflow();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.log);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflow and Step Structure
|
||||||
|
|
||||||
|
Workflows are comprised of steps. Any function performing complex operations or accessing external services must be run as a step using `DBOS.runStep`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
||||||
|
|
||||||
|
async function fetchData() {
|
||||||
|
return await fetch("https://api.example.com").then(r => r.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function myWorkflowFn() {
|
||||||
|
const result = await DBOS.runStep(fetchData, { name: "fetchData" });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const myWorkflow = DBOS.registerWorkflow(myWorkflowFn);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Constraints
|
||||||
|
|
||||||
|
- Do NOT call, start, or enqueue workflows from within steps
|
||||||
|
- Do NOT use threads or uncontrolled concurrency to start workflows - use `DBOS.startWorkflow` or queues
|
||||||
|
- Workflows MUST be deterministic - non-deterministic operations go in steps
|
||||||
|
- Do NOT modify global variables from workflows or steps
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
Read individual rule files for detailed explanations and examples:
|
||||||
|
|
||||||
|
```
|
||||||
|
references/lifecycle-config.md
|
||||||
|
references/workflow-determinism.md
|
||||||
|
references/queue-concurrency.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- https://docs.dbos.dev/
|
||||||
|
- https://github.com/dbos-inc/dbos-transact-ts
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
- Use this skill only when the task clearly matches the scope described above.
|
||||||
|
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||||
|
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Section Definitions
|
||||||
|
|
||||||
|
This file defines the rule categories for DBOS TypeScript best practices. Rules are automatically assigned to sections based on their filename prefix.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Lifecycle (lifecycle)
|
||||||
|
**Impact:** CRITICAL
|
||||||
|
**Description:** DBOS configuration, initialization, and launch patterns. Foundation for all DBOS applications.
|
||||||
|
|
||||||
|
## 2. Workflow (workflow)
|
||||||
|
**Impact:** CRITICAL
|
||||||
|
**Description:** Workflow creation, determinism requirements, background execution, and workflow IDs.
|
||||||
|
|
||||||
|
## 3. Step (step)
|
||||||
|
**Impact:** HIGH
|
||||||
|
**Description:** Step creation, retries, transactions via datasources, and when to use steps vs workflows.
|
||||||
|
|
||||||
|
## 4. Queue (queue)
|
||||||
|
**Impact:** HIGH
|
||||||
|
**Description:** WorkflowQueue creation, concurrency limits, rate limiting, partitioning, and priority.
|
||||||
|
|
||||||
|
## 5. Communication (comm)
|
||||||
|
**Impact:** MEDIUM
|
||||||
|
**Description:** Workflow events, messages, and streaming for inter-workflow communication.
|
||||||
|
|
||||||
|
## 6. Pattern (pattern)
|
||||||
|
**Impact:** MEDIUM
|
||||||
|
**Description:** Common patterns including idempotency, scheduled workflows, debouncing, and class instances.
|
||||||
|
|
||||||
|
## 7. Testing (test)
|
||||||
|
**Impact:** LOW-MEDIUM
|
||||||
|
**Description:** Testing DBOS applications with Jest, mocking, and integration test setup.
|
||||||
|
|
||||||
|
## 8. Client (client)
|
||||||
|
**Impact:** MEDIUM
|
||||||
|
**Description:** DBOSClient for interacting with DBOS from external applications.
|
||||||
|
|
||||||
|
## 9. Advanced (advanced)
|
||||||
|
**Impact:** LOW
|
||||||
|
**Description:** Workflow versioning, patching, and safe code upgrades.
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
title: Use Patching for Safe Workflow Upgrades
|
||||||
|
impact: LOW
|
||||||
|
impactDescription: Safely deploy breaking workflow changes without disrupting in-progress workflows
|
||||||
|
tags: advanced, patching, upgrade, breaking-change
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use Patching for Safe Workflow Upgrades
|
||||||
|
|
||||||
|
Use `DBOS.patch()` to safely deploy breaking changes to workflow code. Breaking changes alter which steps run or their order, which can cause recovery failures.
|
||||||
|
|
||||||
|
**Incorrect (breaking change without patching):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BEFORE: original workflow
|
||||||
|
async function workflowFn() {
|
||||||
|
await foo();
|
||||||
|
await bar();
|
||||||
|
}
|
||||||
|
const workflow = DBOS.registerWorkflow(workflowFn);
|
||||||
|
|
||||||
|
// AFTER: breaking change - recovery will fail for in-progress workflows!
|
||||||
|
async function workflowFn() {
|
||||||
|
await baz(); // Changed step
|
||||||
|
await bar();
|
||||||
|
}
|
||||||
|
const workflow = DBOS.registerWorkflow(workflowFn);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (using patch):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function workflowFn() {
|
||||||
|
if (await DBOS.patch("use-baz")) {
|
||||||
|
await baz(); // New workflows run this
|
||||||
|
} else {
|
||||||
|
await foo(); // Old workflows continue with original code
|
||||||
|
}
|
||||||
|
await bar();
|
||||||
|
}
|
||||||
|
const workflow = DBOS.registerWorkflow(workflowFn);
|
||||||
|
```
|
||||||
|
|
||||||
|
`DBOS.patch()` returns `true` for new workflows and `false` for workflows that started before the patch.
|
||||||
|
|
||||||
|
**Deprecating patches (after all old workflows complete):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function workflowFn() {
|
||||||
|
if (await DBOS.deprecatePatch("use-baz")) { // Always returns true
|
||||||
|
await baz();
|
||||||
|
}
|
||||||
|
await bar();
|
||||||
|
}
|
||||||
|
const workflow = DBOS.registerWorkflow(workflowFn);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Removing patches (after all workflows using deprecatePatch complete):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function workflowFn() {
|
||||||
|
await baz();
|
||||||
|
await bar();
|
||||||
|
}
|
||||||
|
const workflow = DBOS.registerWorkflow(workflowFn);
|
||||||
|
```
|
||||||
|
|
||||||
|
Lifecycle: `patch()` → deploy → wait for old workflows → `deprecatePatch()` → deploy → wait → remove patch entirely.
|
||||||
|
|
||||||
|
Use `DBOS.listWorkflows` to check for active old workflows before deprecating or removing patches.
|
||||||
|
|
||||||
|
Reference: [Patching](https://docs.dbos.dev/typescript/tutorials/upgrading-workflows#patching)
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
title: Use Versioning for Blue-Green Deployments
|
||||||
|
impact: LOW
|
||||||
|
impactDescription: Enables safe deployment of new code versions alongside old ones
|
||||||
|
tags: advanced, versioning, blue-green, deployment
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use Versioning for Blue-Green Deployments
|
||||||
|
|
||||||
|
Set `applicationVersion` in configuration to tag workflows with a version. DBOS only recovers workflows matching the current application version, preventing code mismatches during recovery.
|
||||||
|
|
||||||
|
**Incorrect (deploying new code that breaks in-progress workflows):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
DBOS.setConfig({
|
||||||
|
name: "my-app",
|
||||||
|
systemDatabaseUrl: process.env.DBOS_SYSTEM_DATABASE_URL,
|
||||||
|
// No version set - all workflows recovered regardless of code version
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (versioned deployment):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
DBOS.setConfig({
|
||||||
|
name: "my-app",
|
||||||
|
systemDatabaseUrl: process.env.DBOS_SYSTEM_DATABASE_URL,
|
||||||
|
applicationVersion: "2.0.0",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, the application version is automatically computed from a hash of workflow source code. Set it explicitly for more control.
|
||||||
|
|
||||||
|
**Blue-green deployment strategy:**
|
||||||
|
|
||||||
|
1. Deploy new version (v2) alongside old version (v1)
|
||||||
|
2. Direct new traffic to v2 processes
|
||||||
|
3. Let v1 processes "drain" (complete in-progress workflows)
|
||||||
|
4. Check for remaining v1 workflows:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const oldWorkflows = await DBOS.listWorkflows({
|
||||||
|
applicationVersion: "1.0.0",
|
||||||
|
status: "PENDING",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Once all v1 workflows are complete, retire v1 processes
|
||||||
|
|
||||||
|
**Fork to new version (for stuck workflows):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Fork a workflow from a failed step to run on the new version
|
||||||
|
const handle = await DBOS.forkWorkflow<string>(
|
||||||
|
workflowID,
|
||||||
|
failedStepID,
|
||||||
|
{ applicationVersion: "2.0.0" }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference: [Versioning](https://docs.dbos.dev/typescript/tutorials/upgrading-workflows#versioning)
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
title: Enqueue Workflows from External Applications
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: Enables external services to submit work to DBOS queues
|
||||||
|
tags: client, enqueue, external, queue
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enqueue Workflows from External Applications
|
||||||
|
|
||||||
|
Use `client.enqueue()` to submit workflows from outside your DBOS application. Since `DBOSClient` runs externally, workflow and queue metadata must be specified explicitly.
|
||||||
|
|
||||||
|
**Incorrect (trying to use DBOS.startWorkflow from external code):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// DBOS.startWorkflow requires a full DBOS setup
|
||||||
|
await DBOS.startWorkflow(processTask, { queueName: "myQueue" })("data");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (using DBOSClient.enqueue):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { DBOSClient } from "@dbos-inc/dbos-sdk";
|
||||||
|
|
||||||
|
const client = await DBOSClient.create({
|
||||||
|
systemDatabaseUrl: process.env.DBOS_SYSTEM_DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Basic enqueue
|
||||||
|
const handle = await client.enqueue(
|
||||||
|
{
|
||||||
|
workflowName: "processTask",
|
||||||
|
queueName: "task_queue",
|
||||||
|
},
|
||||||
|
"task-data"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for the result
|
||||||
|
const result = await handle.getResult();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Type-safe enqueue:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Import or declare the workflow type
|
||||||
|
declare class Tasks {
|
||||||
|
static processTask(data: string): Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handle = await client.enqueue<typeof Tasks.processTask>(
|
||||||
|
{
|
||||||
|
workflowName: "processTask",
|
||||||
|
workflowClassName: "Tasks",
|
||||||
|
queueName: "task_queue",
|
||||||
|
},
|
||||||
|
"task-data"
|
||||||
|
);
|
||||||
|
|
||||||
|
// TypeScript infers the result type
|
||||||
|
const result = await handle.getResult(); // type: string
|
||||||
|
```
|
||||||
|
|
||||||
|
**Enqueue options:**
|
||||||
|
- `workflowName` (required): Name of the workflow function
|
||||||
|
- `queueName` (required): Name of the queue
|
||||||
|
- `workflowClassName`: Class name if the workflow is a class method
|
||||||
|
- `workflowConfigName`: Instance name if using `ConfiguredInstance`
|
||||||
|
- `workflowID`: Custom workflow ID
|
||||||
|
- `workflowTimeoutMS`: Timeout in milliseconds
|
||||||
|
- `deduplicationID`: Prevent duplicate enqueues
|
||||||
|
- `priority`: Queue priority (lower = higher priority)
|
||||||
|
- `queuePartitionKey`: Partition key for partitioned queues
|
||||||
|
|
||||||
|
Always call `client.destroy()` when done.
|
||||||
|
|
||||||
|
Reference: [DBOS Client Enqueue](https://docs.dbos.dev/typescript/reference/client#enqueue)
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
title: Initialize DBOSClient for External Access
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: Enables external applications to interact with DBOS workflows
|
||||||
|
tags: client, external, setup, initialization
|
||||||
|
---
|
||||||
|
|
||||||
|
## Initialize DBOSClient for External Access
|
||||||
|
|
||||||
|
Use `DBOSClient` to interact with DBOS from external applications like API servers, CLI tools, or separate services. `DBOSClient` connects directly to the DBOS system database.
|
||||||
|
|
||||||
|
**Incorrect (using DBOS directly from an external app):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// DBOS requires full setup with launch() - too heavy for external clients
|
||||||
|
DBOS.setConfig({ ... });
|
||||||
|
await DBOS.launch();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (using DBOSClient):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { DBOSClient } from "@dbos-inc/dbos-sdk";
|
||||||
|
|
||||||
|
const client = await DBOSClient.create({
|
||||||
|
systemDatabaseUrl: process.env.DBOS_SYSTEM_DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send a message to a workflow
|
||||||
|
await client.send(workflowID, "notification", "topic");
|
||||||
|
|
||||||
|
// Get an event from a workflow
|
||||||
|
const event = await client.getEvent<string>(workflowID, "status");
|
||||||
|
|
||||||
|
// Read a stream from a workflow
|
||||||
|
for await (const value of client.readStream(workflowID, "results")) {
|
||||||
|
console.log(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve a workflow handle
|
||||||
|
const handle = client.retrieveWorkflow<string>(workflowID);
|
||||||
|
const result = await handle.getResult();
|
||||||
|
|
||||||
|
// List workflows
|
||||||
|
const workflows = await client.listWorkflows({ status: "ERROR" });
|
||||||
|
|
||||||
|
// Workflow management
|
||||||
|
await client.cancelWorkflow(workflowID);
|
||||||
|
await client.resumeWorkflow(workflowID);
|
||||||
|
|
||||||
|
// Always destroy when done
|
||||||
|
await client.destroy();
|
||||||
|
```
|
||||||
|
|
||||||
|
Constructor options:
|
||||||
|
- `systemDatabaseUrl`: Connection string to the Postgres system database (required)
|
||||||
|
- `systemDatabasePool`: Optional custom `node-postgres` connection pool
|
||||||
|
- `serializer`: Optional custom serializer (must match the DBOS application's serializer)
|
||||||
|
|
||||||
|
Reference: [DBOS Client](https://docs.dbos.dev/typescript/reference/client)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
title: Use Events for Workflow Status Publishing
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: Enables real-time progress monitoring and interactive workflows
|
||||||
|
tags: communication, events, status, key-value
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use Events for Workflow Status Publishing
|
||||||
|
|
||||||
|
Workflows can publish events (key-value pairs) with `DBOS.setEvent`. Other code can read events with `DBOS.getEvent`. Events are persisted and useful for real-time progress monitoring.
|
||||||
|
|
||||||
|
**Incorrect (using external state for progress):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
let progress = 0; // Global variable - not durable!
|
||||||
|
|
||||||
|
async function processDataFn() {
|
||||||
|
progress = 50; // Not persisted, lost on restart
|
||||||
|
}
|
||||||
|
const processData = DBOS.registerWorkflow(processDataFn);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (using events):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function processDataFn() {
|
||||||
|
await DBOS.setEvent("status", "processing");
|
||||||
|
await DBOS.runStep(stepOne, { name: "stepOne" });
|
||||||
|
await DBOS.setEvent("progress", 50);
|
||||||
|
await DBOS.runStep(stepTwo, { name: "stepTwo" });
|
||||||
|
await DBOS.setEvent("progress", 100);
|
||||||
|
await DBOS.setEvent("status", "complete");
|
||||||
|
}
|
||||||
|
const processData = DBOS.registerWorkflow(processDataFn);
|
||||||
|
|
||||||
|
// Read events from outside the workflow
|
||||||
|
const status = await DBOS.getEvent<string>(workflowID, "status", 0);
|
||||||
|
const progress = await DBOS.getEvent<number>(workflowID, "progress", 0);
|
||||||
|
// Returns null if the event doesn't exist within the timeout (default 60s)
|
||||||
|
```
|
||||||
|
|
||||||
|
Events are useful for interactive workflows. For example, a checkout workflow can publish a payment URL for the caller to redirect to:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function checkoutWorkflowFn() {
|
||||||
|
const paymentURL = await DBOS.runStep(createPayment, { name: "createPayment" });
|
||||||
|
await DBOS.setEvent("paymentURL", paymentURL);
|
||||||
|
// Continue processing...
|
||||||
|
}
|
||||||
|
const checkoutWorkflow = DBOS.registerWorkflow(checkoutWorkflowFn);
|
||||||
|
|
||||||
|
// HTTP handler starts workflow and reads the payment URL
|
||||||
|
const handle = await DBOS.startWorkflow(checkoutWorkflow)();
|
||||||
|
const url = await DBOS.getEvent<string>(handle.workflowID, "paymentURL", 300);
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference: [Workflow Events](https://docs.dbos.dev/typescript/tutorials/workflow-communication#workflow-events)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user