@softspark/ai-toolkit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +412 -0
- package/CHANGELOG.md +68 -0
- package/LICENSE +21 -0
- package/README.md +632 -0
- package/action.yml +53 -0
- package/app/.claude-plugin/plugin.json +44 -0
- package/app/ARCHITECTURE.md +306 -0
- package/app/CLAUDE.md.template +23 -0
- package/app/agents/ai-engineer.md +128 -0
- package/app/agents/backend-specialist.md +193 -0
- package/app/agents/business-intelligence.md +54 -0
- package/app/agents/chaos-monkey.md +67 -0
- package/app/agents/chief-of-staff.md +51 -0
- package/app/agents/code-archaeologist.md +127 -0
- package/app/agents/code-reviewer.md +184 -0
- package/app/agents/command-expert.md +131 -0
- package/app/agents/data-analyst.md +205 -0
- package/app/agents/data-scientist.md +151 -0
- package/app/agents/database-architect.md +317 -0
- package/app/agents/debugger.md +238 -0
- package/app/agents/devops-implementer.md +194 -0
- package/app/agents/documenter.md +364 -0
- package/app/agents/explorer-agent.md +145 -0
- package/app/agents/fact-checker.md +172 -0
- package/app/agents/frontend-specialist.md +209 -0
- package/app/agents/game-developer.md +216 -0
- package/app/agents/incident-responder.md +226 -0
- package/app/agents/infrastructure-architect.md +127 -0
- package/app/agents/infrastructure-validator.md +247 -0
- package/app/agents/llm-ops-engineer.md +237 -0
- package/app/agents/mcp-expert.md +228 -0
- package/app/agents/mcp-server-architect.md +195 -0
- package/app/agents/mcp-testing-engineer.md +292 -0
- package/app/agents/meta-architect.md +58 -0
- package/app/agents/ml-engineer.md +136 -0
- package/app/agents/mobile-developer.md +190 -0
- package/app/agents/night-watchman.md +55 -0
- package/app/agents/nlp-engineer.md +154 -0
- package/app/agents/orchestrator.md +437 -0
- package/app/agents/performance-optimizer.md +254 -0
- package/app/agents/predictive-analyst.md +57 -0
- package/app/agents/product-manager.md +194 -0
- package/app/agents/project-planner.md +287 -0
- package/app/agents/prompt-engineer.md +103 -0
- package/app/agents/qa-automation-engineer.md +182 -0
- package/app/agents/rag-engineer.md +201 -0
- package/app/agents/research-synthesizer.md +138 -0
- package/app/agents/search-specialist.md +101 -0
- package/app/agents/security-architect.md +62 -0
- package/app/agents/security-auditor.md +293 -0
- package/app/agents/seo-specialist.md +111 -0
- package/app/agents/system-governor.md +57 -0
- package/app/agents/tech-lead.md +62 -0
- package/app/agents/technical-researcher.md +103 -0
- package/app/agents/test-engineer.md +264 -0
- package/app/constitution.md +38 -0
- package/app/hooks/_profile-check.sh +11 -0
- package/app/hooks/guard-destructive.sh +74 -0
- package/app/hooks/guard-path.sh +73 -0
- package/app/hooks/post-tool-use.sh +35 -0
- package/app/hooks/pre-compact.sh +31 -0
- package/app/hooks/quality-check.sh +22 -0
- package/app/hooks/quality-gate.sh +49 -0
- package/app/hooks/save-session.sh +24 -0
- package/app/hooks/session-end.sh +37 -0
- package/app/hooks/session-start.sh +29 -0
- package/app/hooks/subagent-start.sh +16 -0
- package/app/hooks/subagent-stop.sh +16 -0
- package/app/hooks/track-usage.sh +50 -0
- package/app/hooks/user-prompt-submit.sh +25 -0
- package/app/hooks.json +178 -0
- package/app/mcp-defaults.json +23 -0
- package/app/output-styles/golden-rules.md +43 -0
- package/app/plugins/README.md +19 -0
- package/app/plugins/csharp-pack/README.md +11 -0
- package/app/plugins/csharp-pack/plugin.json +18 -0
- package/app/plugins/enterprise-pack/README.md +16 -0
- package/app/plugins/enterprise-pack/hooks/output-style.sh +6 -0
- package/app/plugins/enterprise-pack/hooks/status-line.sh +8 -0
- package/app/plugins/enterprise-pack/plugin.json +24 -0
- package/app/plugins/frontend-pack/README.md +14 -0
- package/app/plugins/frontend-pack/plugin.json +22 -0
- package/app/plugins/java-pack/README.md +11 -0
- package/app/plugins/java-pack/plugin.json +18 -0
- package/app/plugins/kotlin-pack/README.md +11 -0
- package/app/plugins/kotlin-pack/plugin.json +18 -0
- package/app/plugins/memory-pack/README.md +24 -0
- package/app/plugins/memory-pack/hooks/observation-capture.sh +67 -0
- package/app/plugins/memory-pack/hooks/session-summary.sh +71 -0
- package/app/plugins/memory-pack/plugin.json +22 -0
- package/app/plugins/memory-pack/scripts/init_db.py +81 -0
- package/app/plugins/memory-pack/scripts/strip_private.py +22 -0
- package/app/plugins/memory-pack/skills/mem-search/SKILL.md +70 -0
- package/app/plugins/research-pack/README.md +14 -0
- package/app/plugins/research-pack/plugin.json +22 -0
- package/app/plugins/ruby-pack/README.md +11 -0
- package/app/plugins/ruby-pack/plugin.json +18 -0
- package/app/plugins/rust-pack/README.md +11 -0
- package/app/plugins/rust-pack/plugin.json +18 -0
- package/app/plugins/security-pack/README.md +15 -0
- package/app/plugins/security-pack/plugin.json +23 -0
- package/app/plugins/swift-pack/README.md +11 -0
- package/app/plugins/swift-pack/plugin.json +18 -0
- package/app/rules/claude-toolkit-rules.md +21 -0
- package/app/rules/git-conventions.md +5 -0
- package/app/rules/quality-gates.md +10 -0
- package/app/skills/_lib/__init__.py +1 -0
- package/app/skills/_lib/detect_utils.py +150 -0
- package/app/skills/agent-creator/SKILL.md +82 -0
- package/app/skills/analyze/SKILL.md +92 -0
- package/app/skills/analyze/scripts/complexity.py +165 -0
- package/app/skills/api-patterns/SKILL.md +305 -0
- package/app/skills/app-builder/SKILL.md +187 -0
- package/app/skills/architecture-audit/SKILL.md +141 -0
- package/app/skills/architecture-decision/SKILL.md +55 -0
- package/app/skills/architecture-decision/templates/adr-template.md +36 -0
- package/app/skills/biz-scan/SKILL.md +30 -0
- package/app/skills/briefing/SKILL.md +27 -0
- package/app/skills/build/SKILL.md +97 -0
- package/app/skills/build/scripts/detect-build.py +151 -0
- package/app/skills/chaos/SKILL.md +32 -0
- package/app/skills/ci/SKILL.md +77 -0
- package/app/skills/ci/scripts/ci-detect.py +135 -0
- package/app/skills/ci/templates/github-actions-node.yml +38 -0
- package/app/skills/ci/templates/github-actions-python.yml +42 -0
- package/app/skills/ci-cd-patterns/SKILL.md +299 -0
- package/app/skills/clean-code/SKILL.md +110 -0
- package/app/skills/clean-code/reference/dart.md +18 -0
- package/app/skills/clean-code/reference/go.md +23 -0
- package/app/skills/clean-code/reference/php.md +32 -0
- package/app/skills/clean-code/reference/python.md +180 -0
- package/app/skills/clean-code/reference/typescript.md +26 -0
- package/app/skills/command-creator/SKILL.md +83 -0
- package/app/skills/commit/SKILL.md +98 -0
- package/app/skills/commit/scripts/pre-commit-check.py +87 -0
- package/app/skills/commit/templates/conventional-commit.md +52 -0
- package/app/skills/csharp-patterns/SKILL.md +450 -0
- package/app/skills/database-patterns/SKILL.md +297 -0
- package/app/skills/debug/SKILL.md +154 -0
- package/app/skills/debug/scripts/error-parser.py +187 -0
- package/app/skills/debugging-tactics/SKILL.md +136 -0
- package/app/skills/deploy/SKILL.md +130 -0
- package/app/skills/deploy/scripts/pre_deploy_check.py +171 -0
- package/app/skills/deploy/templates/deployment-checklist.md +31 -0
- package/app/skills/design-an-interface/SKILL.md +105 -0
- package/app/skills/design-engineering/SKILL.md +260 -0
- package/app/skills/docker-devops/SKILL.md +303 -0
- package/app/skills/docs/SKILL.md +145 -0
- package/app/skills/docs/scripts/doc-inventory.py +176 -0
- package/app/skills/docs/templates/adr-template.md +36 -0
- package/app/skills/docs/templates/readme-template.md +67 -0
- package/app/skills/documentation-standards/SKILL.md +191 -0
- package/app/skills/ecommerce-patterns/SKILL.md +209 -0
- package/app/skills/evaluate/SKILL.md +132 -0
- package/app/skills/evolve/SKILL.md +27 -0
- package/app/skills/explain/SKILL.md +54 -0
- package/app/skills/explain/scripts/dependency-graph.py +215 -0
- package/app/skills/explore/SKILL.md +112 -0
- package/app/skills/explore/scripts/visualize.py +117 -0
- package/app/skills/fix/SKILL.md +78 -0
- package/app/skills/fix/scripts/error-classifier.py +191 -0
- package/app/skills/flutter-patterns/SKILL.md +254 -0
- package/app/skills/git-mastery/SKILL.md +70 -0
- package/app/skills/grill-me/SKILL.md +38 -0
- package/app/skills/health/SKILL.md +91 -0
- package/app/skills/health/scripts/health_check.py +162 -0
- package/app/skills/hive-mind/SKILL.md +56 -0
- package/app/skills/hook-creator/SKILL.md +107 -0
- package/app/skills/index/SKILL.md +74 -0
- package/app/skills/instinct-review/SKILL.md +77 -0
- package/app/skills/java-patterns/SKILL.md +442 -0
- package/app/skills/kotlin-patterns/SKILL.md +446 -0
- package/app/skills/lint/SKILL.md +103 -0
- package/app/skills/lint/scripts/detect-linters.py +112 -0
- package/app/skills/mcp-patterns/SKILL.md +270 -0
- package/app/skills/mem-search/SKILL.md +70 -0
- package/app/skills/migrate/SKILL.md +90 -0
- package/app/skills/migrate/scripts/migration-status.py +195 -0
- package/app/skills/migration-patterns/SKILL.md +260 -0
- package/app/skills/night-watch/SKILL.md +28 -0
- package/app/skills/observability-patterns/SKILL.md +203 -0
- package/app/skills/onboard/SKILL.md +76 -0
- package/app/skills/orchestrate/SKILL.md +86 -0
- package/app/skills/panic/SKILL.md +30 -0
- package/app/skills/performance-profiling/SKILL.md +59 -0
- package/app/skills/plan/SKILL.md +110 -0
- package/app/skills/plan/templates/plan-template.md +40 -0
- package/app/skills/plan-writing/SKILL.md +201 -0
- package/app/skills/plugin-creator/SKILL.md +78 -0
- package/app/skills/pr/SKILL.md +129 -0
- package/app/skills/pr/scripts/pr-summary.py +175 -0
- package/app/skills/prd-to-issues/SKILL.md +108 -0
- package/app/skills/prd-to-plan/SKILL.md +120 -0
- package/app/skills/predict/SKILL.md +30 -0
- package/app/skills/qa-session/SKILL.md +110 -0
- package/app/skills/rag-patterns/SKILL.md +203 -0
- package/app/skills/refactor/SKILL.md +124 -0
- package/app/skills/refactor/scripts/refactor-scan.py +210 -0
- package/app/skills/refactor-plan/SKILL.md +112 -0
- package/app/skills/repeat/SKILL.md +149 -0
- package/app/skills/research-mastery/SKILL.md +56 -0
- package/app/skills/review/SKILL.md +141 -0
- package/app/skills/review/scripts/diff-analyzer.py +170 -0
- package/app/skills/rollback/SKILL.md +87 -0
- package/app/skills/rollback/scripts/rollback_info.py +149 -0
- package/app/skills/ruby-patterns/SKILL.md +454 -0
- package/app/skills/rust-patterns/SKILL.md +446 -0
- package/app/skills/search/SKILL.md +64 -0
- package/app/skills/security-patterns/SKILL.md +91 -0
- package/app/skills/security-patterns/reference/authentication.md +37 -0
- package/app/skills/security-patterns/reference/authorization.md +22 -0
- package/app/skills/security-patterns/reference/input-validation.md +30 -0
- package/app/skills/security-patterns/reference/oauth-csrf-audit.md +131 -0
- package/app/skills/skill-creator/SKILL.md +154 -0
- package/app/skills/skill-creator/templates/dashboard/index.html +130 -0
- package/app/skills/skill-creator/templates/reasoning-engine/assets/example.json +12 -0
- package/app/skills/skill-creator/templates/reasoning-engine/search.py +110 -0
- package/app/skills/subagent-development/SKILL.md +225 -0
- package/app/skills/subagent-development/reference/code-quality-reviewer-prompt.md +145 -0
- package/app/skills/subagent-development/reference/implementer-prompt.md +118 -0
- package/app/skills/subagent-development/reference/spec-reviewer-prompt.md +100 -0
- package/app/skills/swarm/SKILL.md +81 -0
- package/app/skills/swift-patterns/SKILL.md +500 -0
- package/app/skills/tdd/SKILL.md +174 -0
- package/app/skills/tdd/reference/deep-modules.md +32 -0
- package/app/skills/tdd/reference/interface-design.md +32 -0
- package/app/skills/tdd/reference/mocking.md +52 -0
- package/app/skills/tdd/reference/refactoring.md +10 -0
- package/app/skills/tdd/reference/tests.md +59 -0
- package/app/skills/teams/SKILL.md +101 -0
- package/app/skills/test/SKILL.md +107 -0
- package/app/skills/test/scripts/detect-runner.py +113 -0
- package/app/skills/testing-patterns/SKILL.md +73 -0
- package/app/skills/testing-patterns/reference/flutter-testing.md +33 -0
- package/app/skills/testing-patterns/reference/go-testing.md +52 -0
- package/app/skills/testing-patterns/reference/php-phpunit.md +39 -0
- package/app/skills/testing-patterns/reference/python-pytest.md +228 -0
- package/app/skills/testing-patterns/reference/typescript-vitest.md +50 -0
- package/app/skills/triage-issue/SKILL.md +120 -0
- package/app/skills/typescript-patterns/SKILL.md +256 -0
- package/app/skills/ubiquitous-language/SKILL.md +74 -0
- package/app/skills/verification-before-completion/SKILL.md +108 -0
- package/app/skills/workflow/SKILL.md +250 -0
- package/app/skills/write-a-prd/SKILL.md +129 -0
- package/app/skills/write-a-prd/reference/visual-companion.md +78 -0
- package/app/skills/write-a-prd/scripts/frame-template.html +111 -0
- package/app/skills/write-a-prd/scripts/visual-server.cjs +79 -0
- package/app/templates/skill/generator/SKILL.md.template +40 -0
- package/app/templates/skill/knowledge/SKILL.md.template +52 -0
- package/app/templates/skill/linter/SKILL.md.template +34 -0
- package/app/templates/skill/reviewer/SKILL.md.template +51 -0
- package/app/templates/skill/workflow/SKILL.md.template +49 -0
- package/benchmarks/README.md +111 -0
- package/benchmarks/ecosystem-dashboard.json +148 -0
- package/benchmarks/ecosystem-harvest.json +148 -0
- package/benchmarks/results.json +38 -0
- package/benchmarks/run.py +351 -0
- package/bin/ai-toolkit.js +345 -0
- package/kb/best-practices/README.md +11 -0
- package/kb/howto/README.md +11 -0
- package/kb/procedures/maintenance-sop.md +306 -0
- package/kb/reference/agents-catalog.md +124 -0
- package/kb/reference/anti-pattern-registry-format.md +221 -0
- package/kb/reference/architecture-overview.md +232 -0
- package/kb/reference/benchmark-config.md +62 -0
- package/kb/reference/ci-integration.md +66 -0
- package/kb/reference/claude-ecosystem-benchmark-snapshot.md +80 -0
- package/kb/reference/claude-ecosystem-expansion-foundations.md +102 -0
- package/kb/reference/commands-catalog.md +21 -0
- package/kb/reference/distribution-model.md +63 -0
- package/kb/reference/global-install-model.md +56 -0
- package/kb/reference/hierarchical-override-pattern.md +200 -0
- package/kb/reference/hooks-catalog.md +306 -0
- package/kb/reference/integrations.md +88 -0
- package/kb/reference/language-packs.md +52 -0
- package/kb/reference/merge-friendly-install-model.md +58 -0
- package/kb/reference/plugin-pack-conventions.md +151 -0
- package/kb/reference/quick-wins-implementation-summary.md +70 -0
- package/kb/reference/skill-templates.md +50 -0
- package/kb/reference/skills-catalog.md +215 -0
- package/kb/reference/skills-unification.md +57 -0
- package/kb/reference/stats.md +69 -0
- package/kb/reference/sync.md +76 -0
- package/kb/troubleshooting/README.md +11 -0
- package/llms-full.txt +3068 -0
- package/llms.txt +39 -0
- package/package.json +75 -0
- package/scripts/_common.py +160 -0
- package/scripts/add_rule.py +50 -0
- package/scripts/benchmark_config.py +127 -0
- package/scripts/benchmark_ecosystem.py +288 -0
- package/scripts/check_deps.py +260 -0
- package/scripts/create_skill.py +118 -0
- package/scripts/doctor.py +504 -0
- package/scripts/eject.py +113 -0
- package/scripts/emission.py +256 -0
- package/scripts/evaluate_skills.py +260 -0
- package/scripts/frontmatter.py +58 -0
- package/scripts/generate_agents_md.py +91 -0
- package/scripts/generate_aider_conf.py +51 -0
- package/scripts/generate_cline.py +35 -0
- package/scripts/generate_copilot.py +30 -0
- package/scripts/generate_cursor_rules.py +35 -0
- package/scripts/generate_gemini.py +28 -0
- package/scripts/generate_llms_txt.py +164 -0
- package/scripts/generate_roo_modes.py +80 -0
- package/scripts/generate_windsurf.py +35 -0
- package/scripts/generator_base.py +140 -0
- package/scripts/harvest_ecosystem.py +50 -0
- package/scripts/inject_rule_cli.py +101 -0
- package/scripts/inject_section_cli.py +47 -0
- package/scripts/injection.py +180 -0
- package/scripts/install.py +236 -0
- package/scripts/install_git_hooks.py +71 -0
- package/scripts/install_steps/__init__.py +5 -0
- package/scripts/install_steps/ai_tools.py +261 -0
- package/scripts/install_steps/hooks.py +90 -0
- package/scripts/install_steps/markers.py +79 -0
- package/scripts/install_steps/symlinks.py +87 -0
- package/scripts/merge-hooks.py +192 -0
- package/scripts/plugin.py +642 -0
- package/scripts/plugin_schema.py +138 -0
- package/scripts/remove_rule.py +58 -0
- package/scripts/stats.py +81 -0
- package/scripts/sync.py +215 -0
- package/scripts/uninstall.py +292 -0
- package/scripts/validate.py +700 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Gather rollback context: git state, migrations, docker services.
|
|
3
|
+
|
|
4
|
+
Detects the current git state, identifies the migration tool in use,
|
|
5
|
+
and lists running Docker Compose services. Returns a JSON object with
|
|
6
|
+
``git``, ``migrations``, and ``docker`` sections.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
python3 rollback_info.py
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _run(cmd: list[str]) -> subprocess.CompletedProcess[str]:
|
|
24
|
+
"""Run a command, capturing output as text. Never raises."""
|
|
25
|
+
return subprocess.run(
|
|
26
|
+
cmd,
|
|
27
|
+
capture_output=True,
|
|
28
|
+
text=True,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _git(args: list[str], default: str = "unknown") -> str:
|
|
33
|
+
"""Run a git sub-command and return stripped stdout, or *default*."""
|
|
34
|
+
result = _run(["git"] + args)
|
|
35
|
+
return result.stdout.strip() if result.returncode == 0 and result.stdout.strip() else default
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _git_info() -> dict[str, Any]:
|
|
39
|
+
"""Collect git state information."""
|
|
40
|
+
current_commit = _git(["rev-parse", "HEAD"])
|
|
41
|
+
previous_commit = _git(["rev-parse", "HEAD~1"])
|
|
42
|
+
last_msg = _git(["log", "-1", "--pretty=%s"])
|
|
43
|
+
branch = _git(["branch", "--show-current"])
|
|
44
|
+
|
|
45
|
+
porcelain = _run(["git", "status", "--porcelain"])
|
|
46
|
+
uncommitted = bool(porcelain.stdout.strip()) if porcelain.returncode == 0 else False
|
|
47
|
+
|
|
48
|
+
# Determine main branch name
|
|
49
|
+
main_branch = "main"
|
|
50
|
+
verify = _run(["git", "rev-parse", "--verify", "origin/main"])
|
|
51
|
+
if verify.returncode != 0:
|
|
52
|
+
main_branch = "master"
|
|
53
|
+
|
|
54
|
+
ahead_result = _run(["git", "rev-list", "--count", f"origin/{main_branch}..HEAD"])
|
|
55
|
+
try:
|
|
56
|
+
ahead = int(ahead_result.stdout.strip())
|
|
57
|
+
except (ValueError, TypeError):
|
|
58
|
+
ahead = 0
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
"branch": branch,
|
|
62
|
+
"current_commit": current_commit,
|
|
63
|
+
"previous_commit": previous_commit,
|
|
64
|
+
"last_commit_message": last_msg,
|
|
65
|
+
f"commits_ahead_of_{main_branch}": ahead,
|
|
66
|
+
"uncommitted_changes": uncommitted,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _migration_info() -> dict[str, Any]:
|
|
71
|
+
"""Detect migration tool and build rollback command."""
|
|
72
|
+
tool = "none"
|
|
73
|
+
pending: str = "unknown"
|
|
74
|
+
rollback_cmd: str | None = None
|
|
75
|
+
|
|
76
|
+
if Path("alembic.ini").exists():
|
|
77
|
+
tool = "alembic"
|
|
78
|
+
rollback_cmd = "alembic downgrade -1"
|
|
79
|
+
if shutil.which("alembic") is not None:
|
|
80
|
+
result = _run(["alembic", "heads"])
|
|
81
|
+
if result.returncode == 0:
|
|
82
|
+
head_count = len(
|
|
83
|
+
[line for line in result.stdout.strip().splitlines() if line.strip()]
|
|
84
|
+
)
|
|
85
|
+
pending = f"{head_count} pending head(s)"
|
|
86
|
+
elif Path("prisma/schema.prisma").exists():
|
|
87
|
+
tool = "prisma"
|
|
88
|
+
rollback_cmd = "npx prisma migrate resolve --rolled-back <name>"
|
|
89
|
+
elif Path("database/migrations").is_dir() and Path("artisan").exists():
|
|
90
|
+
tool = "laravel"
|
|
91
|
+
rollback_cmd = "php artisan migrate:rollback --step=1"
|
|
92
|
+
elif Path("manage.py").exists():
|
|
93
|
+
tool = "django"
|
|
94
|
+
rollback_cmd = "python manage.py migrate <app> <previous_migration>"
|
|
95
|
+
elif Path("drizzle.config.ts").exists() or Path("drizzle.config.js").exists():
|
|
96
|
+
tool = "drizzle"
|
|
97
|
+
rollback_cmd = "manual rollback required"
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
"tool": tool,
|
|
101
|
+
"pending": pending,
|
|
102
|
+
"rollback_command": rollback_cmd,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _docker_info() -> dict[str, Any]:
|
|
107
|
+
"""Detect Docker Compose services."""
|
|
108
|
+
services: list[dict[str, str]] = []
|
|
109
|
+
running = False
|
|
110
|
+
|
|
111
|
+
has_compose = (
|
|
112
|
+
Path("docker-compose.yml").exists()
|
|
113
|
+
or Path("compose.yml").exists()
|
|
114
|
+
)
|
|
115
|
+
if not (shutil.which("docker") and has_compose):
|
|
116
|
+
return {"running": running, "services": services}
|
|
117
|
+
|
|
118
|
+
result = _run([
|
|
119
|
+
"docker", "compose", "ps",
|
|
120
|
+
"--format", "{{.Service}}:{{.Image}}:{{.Status}}",
|
|
121
|
+
])
|
|
122
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
123
|
+
running = True
|
|
124
|
+
for line in result.stdout.strip().splitlines():
|
|
125
|
+
parts = line.split(":", 2)
|
|
126
|
+
if len(parts) >= 2:
|
|
127
|
+
services.append({
|
|
128
|
+
"service": parts[0],
|
|
129
|
+
"image": parts[1],
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
return {"running": running, "services": services}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main() -> None:
|
|
136
|
+
"""Entry point: gather rollback context and print JSON to stdout."""
|
|
137
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
138
|
+
|
|
139
|
+
output: dict[str, Any] = {
|
|
140
|
+
"timestamp": now,
|
|
141
|
+
"git": _git_info(),
|
|
142
|
+
"migrations": _migration_info(),
|
|
143
|
+
"docker": _docker_info(),
|
|
144
|
+
}
|
|
145
|
+
print(json.dumps(output, indent=2))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
main()
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ruby-patterns
|
|
3
|
+
description: "Loaded when user asks about Ruby development patterns"
|
|
4
|
+
effort: medium
|
|
5
|
+
user-invocable: false
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Ruby Patterns
|
|
9
|
+
|
|
10
|
+
## Project Structure
|
|
11
|
+
|
|
12
|
+
### Gem Layout
|
|
13
|
+
```
|
|
14
|
+
my_gem/
|
|
15
|
+
├── lib/
|
|
16
|
+
│ ├── my_gem.rb # Entry point, require sub-files
|
|
17
|
+
│ └── my_gem/
|
|
18
|
+
│ ├── version.rb
|
|
19
|
+
│ ├── configuration.rb
|
|
20
|
+
│ ├── client.rb
|
|
21
|
+
│ └── errors.rb
|
|
22
|
+
├── spec/
|
|
23
|
+
│ ├── spec_helper.rb
|
|
24
|
+
│ ├── my_gem/
|
|
25
|
+
│ │ └── client_spec.rb
|
|
26
|
+
│ └── fixtures/
|
|
27
|
+
├── bin/
|
|
28
|
+
│ └── console # IRB with gem loaded
|
|
29
|
+
├── sig/ # RBS type signatures
|
|
30
|
+
├── Gemfile
|
|
31
|
+
├── Rakefile
|
|
32
|
+
├── my_gem.gemspec
|
|
33
|
+
├── .rubocop.yml
|
|
34
|
+
└── .ruby-version
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Rails Standard Structure
|
|
38
|
+
```
|
|
39
|
+
app/
|
|
40
|
+
├── controllers/
|
|
41
|
+
│ ├── application_controller.rb
|
|
42
|
+
│ └── api/v1/
|
|
43
|
+
│ └── users_controller.rb
|
|
44
|
+
├── models/
|
|
45
|
+
│ ├── application_record.rb
|
|
46
|
+
│ ├── user.rb
|
|
47
|
+
│ └── concerns/
|
|
48
|
+
│ └── searchable.rb
|
|
49
|
+
├── services/
|
|
50
|
+
│ └── users/
|
|
51
|
+
│ ├── create_service.rb
|
|
52
|
+
│ └── import_service.rb
|
|
53
|
+
├── jobs/
|
|
54
|
+
│ └── user_sync_job.rb
|
|
55
|
+
├── mailers/
|
|
56
|
+
├── serializers/
|
|
57
|
+
│ └── user_serializer.rb
|
|
58
|
+
└── views/
|
|
59
|
+
config/
|
|
60
|
+
├── routes.rb
|
|
61
|
+
├── database.yml
|
|
62
|
+
├── initializers/
|
|
63
|
+
│ ├── sidekiq.rb
|
|
64
|
+
│ └── cors.rb
|
|
65
|
+
└── environments/
|
|
66
|
+
db/
|
|
67
|
+
├── migrate/
|
|
68
|
+
├── schema.rb
|
|
69
|
+
└── seeds.rb
|
|
70
|
+
spec/
|
|
71
|
+
├── rails_helper.rb
|
|
72
|
+
├── spec_helper.rb
|
|
73
|
+
├── models/
|
|
74
|
+
├── requests/
|
|
75
|
+
├── services/
|
|
76
|
+
├── factories/
|
|
77
|
+
│ └── users.rb
|
|
78
|
+
└── support/
|
|
79
|
+
└── shared_examples/
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Gemfile Best Practices
|
|
83
|
+
```ruby
|
|
84
|
+
source "https://rubygems.org"
|
|
85
|
+
|
|
86
|
+
ruby "~> 3.3"
|
|
87
|
+
|
|
88
|
+
gem "rails", "~> 7.2"
|
|
89
|
+
gem "pg"
|
|
90
|
+
gem "puma", ">= 6.0"
|
|
91
|
+
gem "sidekiq", "~> 7.0"
|
|
92
|
+
gem "redis", ">= 5.0"
|
|
93
|
+
|
|
94
|
+
group :development, :test do
|
|
95
|
+
gem "rspec-rails"
|
|
96
|
+
gem "factory_bot_rails"
|
|
97
|
+
gem "faker"
|
|
98
|
+
gem "debug"
|
|
99
|
+
gem "rubocop-rails", require: false
|
|
100
|
+
gem "rubocop-rspec", require: false
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
group :test do
|
|
104
|
+
gem "shoulda-matchers"
|
|
105
|
+
gem "webmock"
|
|
106
|
+
gem "vcr"
|
|
107
|
+
gem "simplecov", require: false
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Idioms / Code Style
|
|
114
|
+
|
|
115
|
+
### Blocks, Procs, and Lambdas
|
|
116
|
+
```ruby
|
|
117
|
+
# Block -- yielded to, not stored
|
|
118
|
+
def with_retry(attempts: 3)
|
|
119
|
+
attempts.times do |i|
|
|
120
|
+
return yield
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
raise if i == attempts - 1
|
|
123
|
+
sleep(2**i)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
with_retry { http_client.get("/data") }
|
|
128
|
+
|
|
129
|
+
# Proc -- flexible arity, returns from enclosing method
|
|
130
|
+
validator = Proc.new { |val| val.to_s.strip.length > 0 }
|
|
131
|
+
|
|
132
|
+
# Lambda -- strict arity, returns from itself
|
|
133
|
+
transform = ->(x) { x.to_s.downcase.strip }
|
|
134
|
+
words = ["Hello ", " WORLD"].map(&transform)
|
|
135
|
+
|
|
136
|
+
# Method reference
|
|
137
|
+
names = users.map(&:name)
|
|
138
|
+
valid = values.select(&method(:valid?))
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Modules and Mixins
|
|
142
|
+
```ruby
|
|
143
|
+
# Concern pattern (Rails)
|
|
144
|
+
module Searchable
|
|
145
|
+
extend ActiveSupport::Concern
|
|
146
|
+
|
|
147
|
+
included do
|
|
148
|
+
scope :search, ->(query) {
|
|
149
|
+
where("name ILIKE ?", "%#{sanitize_sql_like(query)}%")
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
class_methods do
|
|
154
|
+
def searchable_columns
|
|
155
|
+
%i[name email]
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Pure Ruby mixin
|
|
161
|
+
module Loggable
|
|
162
|
+
def logger
|
|
163
|
+
@logger ||= Logger.new($stdout, progname: self.class.name)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def log_info(msg) = logger.info(msg)
|
|
167
|
+
def log_error(msg) = logger.error(msg)
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### method_missing with respond_to_missing?
|
|
172
|
+
```ruby
|
|
173
|
+
class Config
|
|
174
|
+
def initialize(data = {})
|
|
175
|
+
@data = data
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def method_missing(name, *args)
|
|
179
|
+
key = name.to_s.chomp("=").to_sym
|
|
180
|
+
if name.to_s.end_with?("=")
|
|
181
|
+
@data[key] = args.first
|
|
182
|
+
elsif @data.key?(key)
|
|
183
|
+
@data[key]
|
|
184
|
+
else
|
|
185
|
+
super
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def respond_to_missing?(name, include_private = false)
|
|
190
|
+
@data.key?(name.to_s.chomp("=").to_sym) || super
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Frozen String Literals
|
|
196
|
+
```ruby
|
|
197
|
+
# frozen_string_literal: true
|
|
198
|
+
|
|
199
|
+
# Add to every file. Prevents accidental mutation, improves memory.
|
|
200
|
+
# Enforce via RuboCop: Style/FrozenStringLiteralComment
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Pattern Matching (Ruby 3+)
|
|
204
|
+
```ruby
|
|
205
|
+
case response
|
|
206
|
+
in { status: 200, body: { data: Array => items } }
|
|
207
|
+
process_items(items)
|
|
208
|
+
in { status: 200, body: { data: Hash => item } }
|
|
209
|
+
process_item(item)
|
|
210
|
+
in { status: 404 }
|
|
211
|
+
raise NotFoundError
|
|
212
|
+
in { status: (500..) }
|
|
213
|
+
raise ServerError, response[:body]
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Find pattern
|
|
217
|
+
case users
|
|
218
|
+
in [*, { role: "admin", name: String => admin_name }, *]
|
|
219
|
+
puts "Found admin: #{admin_name}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Pin operator
|
|
223
|
+
expected_status = 200
|
|
224
|
+
case response
|
|
225
|
+
in { status: ^expected_status }
|
|
226
|
+
handle_success(response)
|
|
227
|
+
end
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Enumerable Idioms
|
|
231
|
+
```ruby
|
|
232
|
+
# Chaining
|
|
233
|
+
active_emails = users
|
|
234
|
+
.select(&:active?)
|
|
235
|
+
.reject { |u| u.email.nil? }
|
|
236
|
+
.map(&:email)
|
|
237
|
+
.uniq
|
|
238
|
+
.sort
|
|
239
|
+
|
|
240
|
+
# Grouping and tallying
|
|
241
|
+
users.group_by(&:role) # => { "admin" => [...], "user" => [...] }
|
|
242
|
+
users.tally_by(&:role) # => { "admin" => 3, "user" => 15 }
|
|
243
|
+
orders.sum(&:total)
|
|
244
|
+
scores.filter_map { |s| s.value if s.valid? }
|
|
245
|
+
|
|
246
|
+
# each_with_object over inject for building hashes
|
|
247
|
+
users.each_with_object({}) do |user, memo|
|
|
248
|
+
memo[user.id] = user.name
|
|
249
|
+
end
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Error Handling
|
|
255
|
+
|
|
256
|
+
### begin/rescue/ensure
|
|
257
|
+
```ruby
|
|
258
|
+
def fetch_user(id)
|
|
259
|
+
user = api_client.get("/users/#{id}")
|
|
260
|
+
User.new(user)
|
|
261
|
+
rescue Faraday::TimeoutError => e
|
|
262
|
+
logger.warn("Timeout fetching user #{id}: #{e.message}")
|
|
263
|
+
nil
|
|
264
|
+
rescue Faraday::ClientError => e
|
|
265
|
+
raise if e.response_status != 404
|
|
266
|
+
nil
|
|
267
|
+
rescue StandardError => e
|
|
268
|
+
logger.error("Unexpected error: #{e.class} - #{e.message}")
|
|
269
|
+
raise
|
|
270
|
+
ensure
|
|
271
|
+
api_client.close if api_client
|
|
272
|
+
end
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Custom Exceptions
|
|
276
|
+
```ruby
|
|
277
|
+
module MyApp
|
|
278
|
+
class Error < StandardError; end
|
|
279
|
+
|
|
280
|
+
class NotFoundError < Error
|
|
281
|
+
attr_reader :resource, :id
|
|
282
|
+
|
|
283
|
+
def initialize(resource:, id:)
|
|
284
|
+
@resource = resource
|
|
285
|
+
@id = id
|
|
286
|
+
super("#{resource} not found: #{id}")
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
class ValidationError < Error
|
|
291
|
+
attr_reader :errors
|
|
292
|
+
|
|
293
|
+
def initialize(errors)
|
|
294
|
+
@errors = errors
|
|
295
|
+
super(errors.join(", "))
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
class RateLimitError < Error
|
|
300
|
+
attr_reader :retry_after
|
|
301
|
+
|
|
302
|
+
def initialize(retry_after:)
|
|
303
|
+
@retry_after = retry_after
|
|
304
|
+
super("Rate limited. Retry after #{retry_after}s")
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Retry with Backoff
|
|
311
|
+
```ruby
|
|
312
|
+
def with_retries(max: 3, base_delay: 0.5, errors: [StandardError])
|
|
313
|
+
attempts = 0
|
|
314
|
+
begin
|
|
315
|
+
attempts += 1
|
|
316
|
+
yield
|
|
317
|
+
rescue *errors => e
|
|
318
|
+
raise if attempts >= max
|
|
319
|
+
delay = base_delay * (2**(attempts - 1)) + rand(0.0..0.5)
|
|
320
|
+
sleep(delay)
|
|
321
|
+
retry
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
with_retries(max: 5, errors: [Net::OpenTimeout, Faraday::TimeoutError]) do
|
|
326
|
+
api_client.post("/webhook", payload)
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Dry::Monads (Railway-oriented)
|
|
331
|
+
```ruby
|
|
332
|
+
require "dry/monads"
|
|
333
|
+
|
|
334
|
+
class CreateUser
|
|
335
|
+
include Dry::Monads[:result, :do]
|
|
336
|
+
|
|
337
|
+
def call(params)
|
|
338
|
+
values = yield validate(params)
|
|
339
|
+
user = yield persist(values)
|
|
340
|
+
yield send_welcome_email(user)
|
|
341
|
+
Success(user)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
private
|
|
345
|
+
|
|
346
|
+
def validate(params)
|
|
347
|
+
result = UserContract.new.call(params)
|
|
348
|
+
result.success? ? Success(result.to_h) : Failure(result.errors.to_h)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def persist(values)
|
|
352
|
+
user = User.create(values)
|
|
353
|
+
user.persisted? ? Success(user) : Failure(user.errors.full_messages)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def send_welcome_email(user)
|
|
357
|
+
UserMailer.welcome(user).deliver_later
|
|
358
|
+
Success(user)
|
|
359
|
+
rescue StandardError => e
|
|
360
|
+
# Non-critical -- log and continue
|
|
361
|
+
Rails.logger.error("Welcome email failed: #{e.message}")
|
|
362
|
+
Success(user)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Testing Patterns
|
|
370
|
+
|
|
371
|
+
### RSpec Structure
|
|
372
|
+
```ruby
|
|
373
|
+
RSpec.describe UserService, "#create" do
|
|
374
|
+
subject(:result) { described_class.new(repo: repo).create(params) }
|
|
375
|
+
|
|
376
|
+
let(:repo) { instance_double(UserRepository) }
|
|
377
|
+
let(:params) { { name: "Ada", email: "ada@example.com" } }
|
|
378
|
+
|
|
379
|
+
context "when params are valid" do
|
|
380
|
+
before do
|
|
381
|
+
allow(repo).to receive(:save).and_return(build(:user, **params))
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
it "returns the created user" do
|
|
385
|
+
expect(result).to be_a(User)
|
|
386
|
+
expect(result.name).to eq("Ada")
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
it "persists via repository" do
|
|
390
|
+
result
|
|
391
|
+
expect(repo).to have_received(:save).with(hash_including(name: "Ada"))
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
context "when email is taken" do
|
|
396
|
+
before do
|
|
397
|
+
allow(repo).to receive(:save).and_raise(ActiveRecord::RecordNotUnique)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
it "raises a duplicate error" do
|
|
401
|
+
expect { result }.to raise_error(UserService::DuplicateEmail)
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### FactoryBot
|
|
408
|
+
```ruby
|
|
409
|
+
FactoryBot.define do
|
|
410
|
+
factory :user do
|
|
411
|
+
name { Faker::Name.name }
|
|
412
|
+
email { Faker::Internet.unique.email }
|
|
413
|
+
role { :user }
|
|
414
|
+
|
|
415
|
+
trait :admin do
|
|
416
|
+
role { :admin }
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
trait :with_orders do
|
|
420
|
+
transient do
|
|
421
|
+
order_count { 3 }
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
after(:create) do |user, ctx|
|
|
425
|
+
create_list(:order, ctx.order_count, user: user)
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Usage
|
|
432
|
+
create(:user, :admin)
|
|
433
|
+
create(:user, :with_orders, order_count: 5)
|
|
434
|
+
build_stubbed(:user) # No DB hit
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### VCR / WebMock
|
|
438
|
+
```ruby
|
|
439
|
+
# spec/support/vcr.rb
|
|
440
|
+
VCR.configure do |c|
|
|
441
|
+
c.cassette_library_dir = "spec/fixtures/cassettes"
|
|
442
|
+
c.hook_into :webmock
|
|
443
|
+
c.filter_sensitive_data("<API_KEY>") { ENV.fetch("API_KEY") }
|
|
444
|
+
c.default_cassette_options = { record: :once, decode_compressed_response: true }
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
RSpec.describe GitHubClient do
|
|
448
|
+
it "fetches repositories", vcr: { cassette_name: "github/repos" } do
|
|
449
|
+
repos = described_class.new.repos("rails")
|
|
450
|
+
expect(repos).not_to be_empty
|
|
451
|
+
expect(repos.first).to respond_to(:name)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
```
|