mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-30 21:45:13 +02:00
feat(pi): curated pi/core skills+prompts profile, CI load test, and 2.2.2 release sync (#3264)
Adds a curated, Pi-native, skills+prompts-only profile at pi/core/ for downstream packagers that mirror GitHub Releases. - manifests/pi-core.json: explicit include lists, per-item exclusion reasons, curation rules, safety allowlists; every root skill and command must be classified. - scripts/build-pi-core.js regenerates pi/core deterministically (package.json from VERSION, LICENSE, README.md, CURATION.md, skills/, commands/); --check fails CI on drift. council is renamed ecc-council inside pi/core only. - Safety checks: no callable endpoints outside the allowlist, no npx/curl|sh/pip install, no secrets, no absolute home paths, no symlinks, valid frontmatter, no duplicate names. - CI: build + drift check and an offline Pi CLI load test of pi/core. - Release: VERSION, package.json and pi/core/package.json at 2.2.2, CHANGELOG, tag-triggered release verification, and a two-week cadence in CONTRIBUTING.md. pi/core: 123 of 293 skills and 24 of 94 commands; 35,006 characters of skill description text.
This commit is contained in:
@@ -239,6 +239,44 @@ jobs:
|
||||
run: node scripts/ci/validate-no-personal-paths.js
|
||||
continue-on-error: false
|
||||
|
||||
pi-core:
|
||||
name: Pi Core Profile
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
# Rebuild runs frontmatter validation and the pi/core safety checks
|
||||
# (no callable endpoints, no runtime downloads, no secrets, no absolute
|
||||
# home paths, no symlinks, no duplicate skill names); the diff check
|
||||
# proves the committed profile is up to date.
|
||||
- name: Rebuild pi/core and verify it is up to date
|
||||
run: |
|
||||
node scripts/build-pi-core.js
|
||||
git diff --exit-code pi/core
|
||||
|
||||
- name: Install Pi coding agent CLI
|
||||
run: npm install --global --ignore-scripts --no-audit --no-fund @mariozechner/pi-coding-agent@0.73.1
|
||||
|
||||
# --extension only loads a package's pi.extensions entries; pi/core is a
|
||||
# skills+prompts-only package, so the equivalent --skill/--prompt-template
|
||||
# resource flags are used. The load-test script additionally asserts via
|
||||
# RPC that every curated command actually loaded.
|
||||
- name: Offline load test with the Pi CLI
|
||||
run: |
|
||||
PI_OFFLINE=1 pi --offline --mode rpc --no-session --no-context-files --no-extensions \
|
||||
--skill pi/core/skills --prompt-template pi/core/commands </dev/null >/dev/null
|
||||
node scripts/ci/pi-core-load-test.js
|
||||
|
||||
python-tests:
|
||||
name: Python Lint, Type Check & Test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -84,6 +84,22 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Verify VERSION matches tag
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
TAG_VERSION="${TAG_NAME#v}"
|
||||
FILE_VERSION=$(tr -d '[:space:]' < VERSION)
|
||||
if [ "$TAG_VERSION" != "$FILE_VERSION" ]; then
|
||||
echo "::error::Tag version ($TAG_VERSION) does not match VERSION ($FILE_VERSION)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# pi/core/package.json derives its version from VERSION, so a current
|
||||
# profile also proves pi/core is in sync with the tag.
|
||||
- name: Verify pi/core profile is current
|
||||
run: node scripts/build-pi-core.js --check
|
||||
|
||||
- name: Verify release metadata stays in sync
|
||||
run: node tests/plugin-manifest.test.js
|
||||
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
## 2.2.2 - 2026-09-15
|
||||
|
||||
### Added
|
||||
|
||||
#### Pi core profile
|
||||
|
||||
- Add `pi/core/`, a curated Pi-native skills+prompts-only profile for downstream packagers that mirror GitHub Releases: 123 portable engineering skills and 24 pure prompt-workflow commands, no extensions, no hooks, no runtime downloads, and no network or SaaS dependencies. The profile is generated deterministically from the explicit include/exclude lists in `manifests/pi-core.json` by `scripts/build-pi-core.js` and committed so release tarballs contain it verbatim; `pi/core/CURATION.md` lists every excluded skill and command with its reason.
|
||||
- The build fails on safety violations: non-allowlisted URL hosts, pipe-to-shell or fetch-and-run download forms, secrets or tokens, absolute per-user home paths, symlinks, invalid SKILL.md frontmatter, and duplicate skill names. The `council` skill ships as `ecc-council` inside pi/core to avoid catalog name clashes.
|
||||
- CI rebuilds pi/core and verifies it is committed up to date, then installs the Pi coding agent CLI and proves the profile loads fully offline (`PI_OFFLINE=1`), asserting every curated command is actually registered. The release workflow verifies VERSION matches the tag and that pi/core is current.
|
||||
|
||||
### Fixed
|
||||
|
||||
#### Packaging
|
||||
|
||||
@@ -14,6 +14,7 @@ Thanks for wanting to contribute! This repo is a community resource for Claude C
|
||||
- [MCP and documentation (e.g. Context7)](#mcp-and-documentation-eg-context7)
|
||||
- [Cross-Harness and Translations](#cross-harness-and-translations)
|
||||
- [Pull Request Process](#pull-request-process)
|
||||
- [Releases](#releases)
|
||||
|
||||
---
|
||||
|
||||
@@ -484,6 +485,27 @@ Run `npm test` locally. It is the same gauntlet CI runs, and it catches almost e
|
||||
|
||||
---
|
||||
|
||||
## Releases
|
||||
|
||||
Releases are cut on a regular cadence, roughly every two weeks, plus out-of-band
|
||||
patches for security or data-loss fixes.
|
||||
|
||||
1. Sync the version everywhere: `VERSION`, `package.json`, and (via
|
||||
`node scripts/build-pi-core.js`) `pi/core/package.json`.
|
||||
2. Update `CHANGELOG.md` and write reviewed release notes at
|
||||
`docs/releases/<version>/release-notes.md` (required by the release workflow).
|
||||
3. Tag `vX.Y.Z` on `main` and push the tag. The `release.yml` workflow verifies
|
||||
the tag is exactly on `origin/main`, checks the VERSION/pi/core sync, tests
|
||||
the exact packed artifact on Linux, macOS, and Windows, publishes to npm with
|
||||
provenance, verifies registry bytes, and creates the GitHub Release from the
|
||||
reviewed notes.
|
||||
|
||||
Tags and GitHub Releases are immutable once published: downstream packagers poll
|
||||
`/releases`, download `archive/refs/tags/vX.Y.Z.tar.gz`, and pin its sha256.
|
||||
Never move or re-tag a published version; ship a new patch version instead.
|
||||
|
||||
---
|
||||
|
||||
## Guidelines
|
||||
|
||||
### Do
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# ECC 2.2.2
|
||||
|
||||
ECC 2.2.2 adds `pi/core`, a curated Pi-native skills+prompts-only profile built for
|
||||
downstream packagers that mirror GitHub Releases, plus a set of packaging, memory,
|
||||
hooks, and Windows compatibility fixes.
|
||||
|
||||
## Pi core profile
|
||||
|
||||
`pi/core/` is a self-contained package (`ecc-pi-core`) that mirrors of the release
|
||||
tarball can copy directly: no build step, no extensions, no hooks, no runtime
|
||||
downloads, and no network or SaaS dependencies.
|
||||
|
||||
- 123 curated skills (language and framework patterns, testing/TDD, code review,
|
||||
non-offensive security review, planning, refactoring, docs, and git/PR workflows)
|
||||
and 24 prompt commands that are pure prompt workflows.
|
||||
- Generated deterministically from the explicit include/exclude lists in
|
||||
`manifests/pi-core.json` by `scripts/build-pi-core.js` and committed, so the
|
||||
release tarball contains it verbatim. `pi/core/CURATION.md` lists every excluded
|
||||
skill and command with its reason; the `council` skill ships as `ecc-council`
|
||||
inside pi/core to avoid catalog name clashes.
|
||||
- The build fails on safety violations: non-allowlisted URL hosts, pipe-to-shell
|
||||
or fetch-and-run download forms, secrets or tokens, absolute per-user home
|
||||
paths, symlinks, invalid SKILL.md frontmatter, and duplicate skill names.
|
||||
- CI rebuilds pi/core on every PR and verifies it is up to date, then installs
|
||||
the Pi coding agent CLI and proves the profile loads fully offline
|
||||
(`PI_OFFLINE=1`), asserting that every curated command actually registers.
|
||||
- The release workflow additionally verifies that `VERSION` matches the tag and
|
||||
that pi/core is current before publishing.
|
||||
|
||||
Downstream consumption: poll `/releases`, download
|
||||
`archive/refs/tags/vX.Y.Z.tar.gz`, pin its sha256, copy `pi/core/`, and load it
|
||||
offline with `pi --offline --skill pi/core/skills --prompt-template pi/core/commands`.
|
||||
|
||||
## Packaging
|
||||
|
||||
- The compiled OpenCode payload is explicitly included in the npm package, and
|
||||
packing is verified from a clean state with lifecycle scripts enabled.
|
||||
|
||||
## Memory and MCP
|
||||
|
||||
- Incomplete memory reads are distinguished from missing records, and directory
|
||||
traversal failures are classified.
|
||||
- The reserved `_meta` parameter is accepted on memory MCP ping requests.
|
||||
|
||||
## Hooks and Windows compatibility
|
||||
|
||||
- `hooks.json` stays within Claude Code's schema; stable hook metadata moved into
|
||||
a validated sidecar.
|
||||
- The no-verify guard handles stuck optional values and long-option prefixes.
|
||||
- Windows linter paths and ESLint 9 are supported, and settings updates tolerate
|
||||
missing Windows device IDs while retaining full-precision inode checks.
|
||||
|
||||
## Workflow guidance, catalog, and dependency security
|
||||
|
||||
- Epic sync filters issues by label; dependency bumps no longer document
|
||||
auto-merge; naming and Boolean guidance is language-neutral; the `prp-pr`
|
||||
command alias is distinguished; Rails skill discovery, invoice tax calculation
|
||||
order, and framework documentation were corrected; Serply and Squish catalog
|
||||
entries were removed.
|
||||
- `lru` updated to 0.18.2 (RUSTSEC-2026-0253) and `js-yaml` to 4.3.2
|
||||
(GHSA-2883-xcg3-v3hh).
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,482 @@
|
||||
{
|
||||
"description": "Curated Pi-native profile for ECC: portable engineering skills and pure prompt workflow commands, regenerated into pi/core/ by scripts/build-pi-core.js.",
|
||||
"profile": {
|
||||
"dir": "pi/core",
|
||||
"packageName": "ecc-pi-core",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"pi-package",
|
||||
"skills"
|
||||
]
|
||||
},
|
||||
"curationRules": {
|
||||
"include": [
|
||||
"language/framework skills",
|
||||
"testing/TDD",
|
||||
"code review",
|
||||
"security review (non-offensive)",
|
||||
"planning",
|
||||
"refactoring",
|
||||
"docs",
|
||||
"git/PR workflows",
|
||||
"prompt commands that are pure prompt workflows"
|
||||
],
|
||||
"exclude": [
|
||||
"makes network calls, needs API keys, or downloads at runtime (npx, pip install, curl|sh)",
|
||||
"wraps third-party SaaS (exa, context7, fal, videodb, x-api, social/marketing/SEO/outreach)",
|
||||
"integrates commercial products (ECC Tools, ecc.tools, AgentShield, billing/ops skills)",
|
||||
"depends on Claude-Code-only mechanics (hooks, agent rosters/orch-*, continuous-learning, loops, hookify, cost tracking, session save/resume, gateguard)",
|
||||
"venture-specific (ito-*, prediction-market-*, hermes-*, nasiko-*, openclaw-*, x402)",
|
||||
"niche domain pack (healthcare/HIPAA, supply chain/logistics, homelab, scientific, visa/legal docs) or offensive security (bug bounty)"
|
||||
]
|
||||
},
|
||||
"skills": {
|
||||
"include": [
|
||||
"accessibility",
|
||||
"agent-architecture-audit",
|
||||
"agent-eval",
|
||||
"agent-harness-construction",
|
||||
"agent-introspection-debugging",
|
||||
"agent-self-evaluation",
|
||||
"agentic-engineering",
|
||||
"ai-first-engineering",
|
||||
"android-clean-architecture",
|
||||
"angular-developer",
|
||||
"api-connector-builder",
|
||||
"api-design",
|
||||
"architecture-decision-records",
|
||||
"backend-patterns",
|
||||
"benchmark",
|
||||
"benchmark-optimization-loop",
|
||||
"blueprint",
|
||||
"bun-runtime",
|
||||
"click-path-audit",
|
||||
"clickhouse-io",
|
||||
"code-tour",
|
||||
"codebase-onboarding",
|
||||
"coding-standards",
|
||||
"compose-multiplatform-patterns",
|
||||
"content-hash-cache-pattern",
|
||||
"contract-first",
|
||||
"cost-aware-llm-pipeline",
|
||||
"council",
|
||||
"cpp-coding-standards",
|
||||
"cpp-testing",
|
||||
"csharp-testing",
|
||||
"dart-flutter-patterns",
|
||||
"dashboard-builder",
|
||||
"data-throughput-accelerator",
|
||||
"database-migrations",
|
||||
"deployment-patterns",
|
||||
"design-system",
|
||||
"dev-team",
|
||||
"django-celery",
|
||||
"django-patterns",
|
||||
"django-security",
|
||||
"django-tdd",
|
||||
"django-verification",
|
||||
"docker-patterns",
|
||||
"dotnet-patterns",
|
||||
"e2e-testing",
|
||||
"error-handling",
|
||||
"fastapi-patterns",
|
||||
"flutter-dart-code-review",
|
||||
"foundation-models-on-device",
|
||||
"frontend-a11y",
|
||||
"frontend-patterns",
|
||||
"fsharp-testing",
|
||||
"git-workflow",
|
||||
"golang-patterns",
|
||||
"golang-testing",
|
||||
"hexagonal-architecture",
|
||||
"inherit-legacy-style",
|
||||
"intent-driven-development",
|
||||
"java-coding-standards",
|
||||
"jpa-patterns",
|
||||
"kotlin-coroutines-flows",
|
||||
"kotlin-exposed-patterns",
|
||||
"kotlin-ktor-patterns",
|
||||
"kotlin-patterns",
|
||||
"kotlin-testing",
|
||||
"kubernetes-patterns",
|
||||
"laravel-patterns",
|
||||
"laravel-security",
|
||||
"laravel-tdd",
|
||||
"laravel-verification",
|
||||
"latency-critical-systems",
|
||||
"liquid-glass-design",
|
||||
"living-docs-governance",
|
||||
"make-interfaces-feel-better",
|
||||
"mcp-server-patterns",
|
||||
"ml-adoption-playbook",
|
||||
"mle-workflow",
|
||||
"motion-advanced",
|
||||
"motion-foundations",
|
||||
"motion-patterns",
|
||||
"mysql-patterns",
|
||||
"nestjs-patterns",
|
||||
"nextjs-turbopack",
|
||||
"nuxt4-patterns",
|
||||
"parallel-execution-optimizer",
|
||||
"perl-patterns",
|
||||
"perl-security",
|
||||
"perl-testing",
|
||||
"postgres-patterns",
|
||||
"prisma-patterns",
|
||||
"product-capability",
|
||||
"product-lens",
|
||||
"production-audit",
|
||||
"python-patterns",
|
||||
"python-testing",
|
||||
"pytorch-patterns",
|
||||
"quarkus-patterns",
|
||||
"quarkus-security",
|
||||
"quarkus-tdd",
|
||||
"quarkus-verification",
|
||||
"rails-patterns",
|
||||
"react-native-patterns",
|
||||
"react-patterns",
|
||||
"react-performance",
|
||||
"react-testing",
|
||||
"redis-patterns",
|
||||
"regex-vs-llm-structured-text",
|
||||
"rust-patterns",
|
||||
"rust-testing",
|
||||
"security-review",
|
||||
"springboot-patterns",
|
||||
"springboot-security",
|
||||
"springboot-tdd",
|
||||
"springboot-verification",
|
||||
"swift-actor-persistence",
|
||||
"swift-concurrency-6-2",
|
||||
"swift-protocol-di-testing",
|
||||
"swiftui-patterns",
|
||||
"tdd-workflow",
|
||||
"verification-loop",
|
||||
"vite-patterns",
|
||||
"vue-patterns"
|
||||
],
|
||||
"rename": {
|
||||
"council": "ecc-council"
|
||||
},
|
||||
"exclude": {
|
||||
"agent-payment-x402": "venture-specific x402 payments; wallet and network runtime",
|
||||
"agent-sort": "ECC install planner over the full ECC catalog; not portable",
|
||||
"agentic-os": "Claude-Code-only persistent OS mechanics (slash commands, memory, schedules)",
|
||||
"ai-regression-testing": "workflow creates Claude Code custom slash commands (.claude/commands)",
|
||||
"article-writing": "content/marketing writing, not engineering",
|
||||
"automation-audit-ops": "ECC ops audit of hooks/connectors/MCP surfaces",
|
||||
"autonomous-agent-harness": "Claude-Code-only autonomous harness (hooks, scheduling, computer use)",
|
||||
"autonomous-loops": "Claude-Code-only autonomous loops",
|
||||
"benchmark-methodology": "competitive/marketing benchmarking",
|
||||
"blender-motion-state-inspection": "niche 3D/Blender domain pack",
|
||||
"brand-discovery": "brand/marketing content",
|
||||
"brand-voice": "marketing/outreach voice profiling",
|
||||
"browser-qa": "requires a browser automation MCP server at runtime",
|
||||
"canary-watch": "makes network calls to deployed URLs",
|
||||
"carrier-relationship-management": "supply chain/logistics domain pack",
|
||||
"cisco-ios-patterns": "network-device ops niche domain",
|
||||
"ck": "Claude-Code-only persistent memory commands",
|
||||
"claude-devfleet": "multi-agent orchestration via external DevFleet product",
|
||||
"codehealth-mcp": "wraps CodeScene MCP SaaS",
|
||||
"competitive-platform-analysis": "competitive/marketing analysis",
|
||||
"competitive-report-structure": "competitive/marketing reporting",
|
||||
"config-gc": "Claude-Code-only config garbage collection (~/.claude)",
|
||||
"configure-ecc": "ECC-specific setup wizard",
|
||||
"connections-optimizer": "social/outreach (X and LinkedIn)",
|
||||
"content-engine": "social/marketing content system",
|
||||
"context-budget": "Claude-Code-only context window audit",
|
||||
"continuous-agent-loop": "continuous agent loops",
|
||||
"continuous-learning": "Claude-Code-only hooks-based continuous learning (deprecated)",
|
||||
"continuous-learning-v2": "Claude-Code-only hooks-based continuous learning",
|
||||
"cost-tracking": "Claude Code cost tracking",
|
||||
"council-multi-model": "requires external Codex CLI at runtime",
|
||||
"counterparty-channel-discipline": "agent messaging ops policy",
|
||||
"crosspost": "social/marketing distribution",
|
||||
"customer-billing-ops": "billing/ops skill over connected billing tools",
|
||||
"customs-trade-compliance": "customs/trade niche domain",
|
||||
"data-scraper-agent": "scheduled network scraping agent",
|
||||
"deep-research": "requires firecrawl and exa SaaS MCP tools",
|
||||
"defi-amm-security": "crypto/DeFi niche domain",
|
||||
"delivery-gate": "Claude-Code-only stop hook",
|
||||
"dmux-workflows": "multi-agent orchestration via dmux",
|
||||
"documentation-lookup": "wraps Context7 SaaS MCP",
|
||||
"dynamic-workflow-mode": "Claude dynamic workflow mode mechanics",
|
||||
"ecc-guide": "ECC repository self-reference",
|
||||
"ecc-recipes": "ECC command catalog self-reference",
|
||||
"ecc-tools-cost-audit": "ECC Tools commercial billing/ops",
|
||||
"email-ops": "mailbox ops skill",
|
||||
"energy-procurement": "energy procurement niche domain",
|
||||
"enterprise-agent-ops": "agent runtime ops, not portable engineering",
|
||||
"esign-field-placement": "niche e-sign browser automation",
|
||||
"eval-harness": "reads and writes .claude/evals (Claude-Code-only)",
|
||||
"evm-token-decimals": "crypto/EVM niche domain",
|
||||
"exa-search": "wraps Exa SaaS",
|
||||
"fal-ai-media": "wraps fal.ai SaaS",
|
||||
"finance-billing-ops": "billing/ops skill",
|
||||
"flox-environments": "runtime installer flow (curl|sh) for Flox",
|
||||
"frontend-design-direction": "ECC-specific design direction",
|
||||
"frontend-slides": "presentation/content production, not engineering",
|
||||
"gan-style-harness": "Claude-Code-only generator/evaluator harness",
|
||||
"gateguard": "Claude-Code-only PreToolUse gate",
|
||||
"generating-python-installer": "niche Windows installer packaging with runtime downloads",
|
||||
"github-ops": "makes GitHub API calls via gh at runtime",
|
||||
"google-workspace-ops": "wraps Google Workspace SaaS",
|
||||
"growth-log": "ECC learning-log workflow",
|
||||
"healthcare-cdss-patterns": "healthcare niche domain pack",
|
||||
"healthcare-emr-patterns": "healthcare niche domain pack",
|
||||
"healthcare-eval-harness": "healthcare niche domain pack",
|
||||
"healthcare-phi-compliance": "healthcare/PHI niche domain pack",
|
||||
"hermes-imports": "venture-specific hermes-*",
|
||||
"hipaa-compliance": "HIPAA niche domain pack",
|
||||
"homelab-network-readiness": "homelab niche domain pack",
|
||||
"homelab-network-setup": "homelab niche domain pack",
|
||||
"homelab-pihole-dns": "homelab niche domain pack",
|
||||
"homelab-vlan-segmentation": "homelab niche domain pack",
|
||||
"homelab-wireguard-vpn": "homelab niche domain pack",
|
||||
"hookify-rules": "hookify (Claude-Code-only hooks)",
|
||||
"i18n-sync": "built around a third-party npm CLI (locakit) with external source links; not self-contained",
|
||||
"inventory-demand-planning": "supply chain niche domain",
|
||||
"investor-materials": "fundraising/marketing content",
|
||||
"investor-outreach": "fundraising outreach",
|
||||
"ios-icon-gen": "Iconify API network calls at runtime",
|
||||
"iterative-retrieval": "multi-agent subagent context mechanics; links to external social post",
|
||||
"ito-baskets": "venture-specific ito-*",
|
||||
"ito-compute": "venture-specific ito-*",
|
||||
"ito-inference": "venture-specific ito-*",
|
||||
"ito-training": "venture-specific ito-*",
|
||||
"jira-integration": "wraps Jira SaaS API",
|
||||
"knowledge-ops": "ops skill over MCP memory and vector stores",
|
||||
"laravel-plugin-discovery": "wraps LaraPlugins.io SaaS MCP",
|
||||
"lead-intelligence": "sales outreach pipeline",
|
||||
"llm-trading-agent-security": "trading-agent niche domain",
|
||||
"logistics-exception-management": "logistics niche domain",
|
||||
"loop-design-check": "agent loop design mechanics",
|
||||
"mailtrap-email-integration": "wraps Mailtrap SaaS API",
|
||||
"manim-video": "niche video production; pip installs at runtime",
|
||||
"market-research": "web research over network sources",
|
||||
"marketing-campaign": "marketing",
|
||||
"master-agreement-generator": "legal docs niche",
|
||||
"messages-ops": "messaging ops skill",
|
||||
"nanoclaw-repl": "ECC product-specific REPL",
|
||||
"nasiko-control-plane": "venture-specific nasiko-*",
|
||||
"netmiko-ssh-automation": "network-device ops niche; SSH at runtime",
|
||||
"network-bgp-diagnostics": "network ops niche domain",
|
||||
"network-config-validation": "network ops niche domain",
|
||||
"network-interface-health": "network ops niche domain",
|
||||
"nodejs-keccak256": "crypto/EVM niche domain",
|
||||
"nutrient-document-processing": "wraps Nutrient DWS SaaS API",
|
||||
"openclaw-persona-forge": "venture-specific openclaw-*",
|
||||
"opensource-pipeline": "multi-agent roster pipeline",
|
||||
"operator-approval-loop": "ECC ops approval contract",
|
||||
"orch-add-feature": "Claude-Code-only orch-* orchestration",
|
||||
"orch-build-mvp": "Claude-Code-only orch-* orchestration",
|
||||
"orch-change-feature": "Claude-Code-only orch-* orchestration",
|
||||
"orch-fix-defect": "Claude-Code-only orch-* orchestration",
|
||||
"orch-pipeline": "Claude-Code-only orch-* orchestration",
|
||||
"orch-refine-code": "Claude-Code-only orch-* orchestration",
|
||||
"plan-canvas": "Claude-Code-only plan canvas server",
|
||||
"plan-orchestrate": "ECC orchestration prompt generator over the full catalog",
|
||||
"plankton-code-quality": "Claude-Code-only write-time hooks",
|
||||
"prediction-market-oracle-research": "venture-specific prediction-market-*",
|
||||
"prediction-market-risk-review": "venture-specific prediction-market-*",
|
||||
"production-scheduling": "manufacturing niche domain",
|
||||
"project-flow-ops": "ops over GitHub/Linear SaaS",
|
||||
"prompt-optimizer": "ECC command/agent catalog self-reference",
|
||||
"quality-nonconformance": "regulated manufacturing niche",
|
||||
"ralphinho-rfc-pipeline": "multi-agent DAG orchestration",
|
||||
"recsys-pipeline-architect": "installs upstream package via npx skills add",
|
||||
"recursive-decision-ledger": "recursive prompting loops",
|
||||
"remotion-video-creation": "video/media production niche",
|
||||
"repo-scan": "downloads an external skill at runtime",
|
||||
"research-ops": "ECC ops research workflow with network enrichment",
|
||||
"returns-reverse-logistics": "logistics niche domain",
|
||||
"rules-distill": "Claude-Code-only rules distillation mechanics",
|
||||
"safety-guard": "Claude-Code-only PreToolUse hooks",
|
||||
"santa-method": "multi-agent adversarial review roster",
|
||||
"scientific-db-pubmed-database": "scientific niche domain pack",
|
||||
"scientific-db-uspto-database": "scientific niche domain pack",
|
||||
"scientific-pkg-gget": "scientific niche domain pack",
|
||||
"scientific-thinking-literature-review": "scientific niche domain pack",
|
||||
"scientific-thinking-scholar-evaluation": "scientific niche domain pack",
|
||||
"search-first": "network searches (npm/PyPI/GitHub) at runtime",
|
||||
"security-bounty-hunter": "offensive security (bug bounty)",
|
||||
"security-scan": "wraps AgentShield commercial product",
|
||||
"seo": "SEO/marketing",
|
||||
"skill-comply": "runs agent rosters for compliance checks",
|
||||
"skill-scout": "network searches of skill marketplaces",
|
||||
"skill-stocktake": "subagent-based Claude skill audit",
|
||||
"social-graph-ranker": "social graph (X and LinkedIn)",
|
||||
"social-publisher": "wraps SocialClaw SaaS",
|
||||
"strategic-compact": "Claude Code session compaction mechanics",
|
||||
"taste": "media/creative-direction niche pack",
|
||||
"taste-application": "media generation via fal.ai SaaS",
|
||||
"taste-distillation": "media analysis paired with fal.ai SaaS",
|
||||
"tasteforge-video": "media generation workflow over fal.ai SaaS",
|
||||
"team-agent-orchestration": "agent squad orchestration",
|
||||
"team-builder": "Claude agents roster picker",
|
||||
"terminal-opener": "ECC harness utility, not engineering content",
|
||||
"terminal-ops": "ECC ops workflow",
|
||||
"tinystruct-patterns": "niche single-framework pack",
|
||||
"token-budget-advisor": "session/token mechanics",
|
||||
"ui-demo": "Playwright video recording with runtime installs",
|
||||
"ui-to-vue": "runs an npx converter package at runtime",
|
||||
"uncloud": "niche cluster ops",
|
||||
"unified-memory": "ECC memory vault (session save/resume family)",
|
||||
"unified-notifications-ops": "ECC notifications ops",
|
||||
"video-editing": "media production niche",
|
||||
"videodb": "wraps VideoDB SaaS",
|
||||
"visa-doc-translate": "visa/legal docs niche; OCR network calls",
|
||||
"windows-desktop-e2e": "niche Windows desktop automation; pip installs at runtime",
|
||||
"workspace-surface-audit": "ECC harness surface audit",
|
||||
"x-api": "wraps X/Twitter SaaS API"
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
"include": [
|
||||
"aside.md",
|
||||
"build-fix.md",
|
||||
"code-review.md",
|
||||
"cpp-test.md",
|
||||
"fastapi-review.md",
|
||||
"feature-dev.md",
|
||||
"flutter-test.md",
|
||||
"go-test.md",
|
||||
"gradle-build.md",
|
||||
"kotlin-test.md",
|
||||
"plan-prd.md",
|
||||
"plan.md",
|
||||
"pr.md",
|
||||
"prp-commit.md",
|
||||
"prp-implement.md",
|
||||
"prp-plan.md",
|
||||
"prp-pr.md",
|
||||
"prp-prd.md",
|
||||
"react-test.md",
|
||||
"refactor-clean.md",
|
||||
"rust-test.md",
|
||||
"test-coverage.md",
|
||||
"update-codemaps.md",
|
||||
"update-docs.md"
|
||||
],
|
||||
"exclude": {
|
||||
"auto-update.md": "ECC self-update/reinstall",
|
||||
"checkpoint.md": "writes .claude/checkpoints.log (Claude-Code-only)",
|
||||
"cost-report.md": "Claude Code cost tracking",
|
||||
"cpp-build.md": "invokes ECC agent roster (cpp-build-resolver)",
|
||||
"cpp-review.md": "invokes ECC agent roster (cpp-reviewer)",
|
||||
"ecc-guide.md": "ECC repository self-reference",
|
||||
"epic-claim.md": "GitHub epic ops over ECC coordination state; network",
|
||||
"epic-decompose.md": "GitHub epic ops over ECC coordination state; network",
|
||||
"epic-publish.md": "GitHub epic ops over ECC coordination state; network",
|
||||
"epic-review.md": "GitHub epic ops over ECC coordination state; network",
|
||||
"epic-sync.md": "GitHub epic ops over ECC coordination state; network",
|
||||
"epic-unblock.md": "GitHub epic ops over ECC coordination state; network",
|
||||
"epic-validate.md": "GitHub epic ops over ECC coordination state; network",
|
||||
"evolve.md": "instincts/continuous-learning mechanics",
|
||||
"flutter-build.md": "invokes ECC agent roster (dart-build-resolver)",
|
||||
"flutter-review.md": "invokes ECC agent roster (flutter-reviewer)",
|
||||
"gan-build.md": "GAN generator/evaluator loop harness",
|
||||
"gan-design.md": "GAN generator/evaluator loop harness",
|
||||
"go-build.md": "invokes ECC agent roster (go-build-resolver)",
|
||||
"go-review.md": "invokes ECC agent roster (go-reviewer)",
|
||||
"harness-audit.md": "ECC harness audit",
|
||||
"hookify-configure.md": "hookify (Claude-Code-only hooks)",
|
||||
"hookify-help.md": "hookify (Claude-Code-only hooks)",
|
||||
"hookify-list.md": "hookify (Claude-Code-only hooks)",
|
||||
"hookify.md": "hookify (Claude-Code-only hooks)",
|
||||
"instinct-export.md": "instincts (continuous-learning)",
|
||||
"instinct-import.md": "instincts (continuous-learning)",
|
||||
"instinct-status.md": "instincts (continuous-learning)",
|
||||
"jira.md": "wraps Jira SaaS API",
|
||||
"kotlin-build.md": "invokes ECC agent roster (kotlin-build-resolver)",
|
||||
"kotlin-review.md": "invokes ECC agent roster (kotlin-reviewer)",
|
||||
"learn-eval.md": "continuous-learning session extraction",
|
||||
"learn.md": "continuous-learning session extraction",
|
||||
"loop-start.md": "autonomous loops",
|
||||
"loop-status.md": "autonomous loops",
|
||||
"marketing-campaign.md": "marketing",
|
||||
"model-route.md": "ECC model routing/cost mechanics",
|
||||
"multi-backend.md": "multi-model orchestration",
|
||||
"multi-execute.md": "multi-model orchestration",
|
||||
"multi-frontend.md": "multi-model orchestration",
|
||||
"multi-plan.md": "multi-model orchestration",
|
||||
"multi-workflow.md": "multi-model orchestration",
|
||||
"orch-add-feature.md": "Claude-Code-only orch-* orchestration",
|
||||
"orch-build-mvp.md": "Claude-Code-only orch-* orchestration",
|
||||
"orch-change-feature.md": "Claude-Code-only orch-* orchestration",
|
||||
"orch-fix-defect.md": "Claude-Code-only orch-* orchestration",
|
||||
"orch-refine-code.md": "Claude-Code-only orch-* orchestration",
|
||||
"orch-review.md": "Claude-Code-only orch-* orchestration",
|
||||
"plan-canvas.md": "Claude-Code-only plan canvas server",
|
||||
"pm2.md": "PM2 runtime process manager ops",
|
||||
"project-init.md": "ECC install-manifest onboarding plan",
|
||||
"projects.md": "instincts (continuous-learning)",
|
||||
"promote.md": "instincts (continuous-learning)",
|
||||
"prune.md": "instincts (continuous-learning)",
|
||||
"python-review.md": "invokes ECC agent roster (python-reviewer)",
|
||||
"quality-gate.md": "drives the ECC PostToolUse formatter hook script",
|
||||
"react-build.md": "invokes ECC agent roster (react-build-resolver)",
|
||||
"react-review.md": "invokes ECC agent roster (react-reviewer)",
|
||||
"resume-session.md": "Claude Code session save/resume (~/.claude/session-data)",
|
||||
"review-pr.md": "invokes ECC agent roster (specialized review agents)",
|
||||
"rust-build.md": "invokes ECC agent roster (rust-build-resolver)",
|
||||
"rust-review.md": "invokes ECC agent roster (rust-reviewer)",
|
||||
"santa-loop.md": "multi-agent adversarial review loop",
|
||||
"save-session.md": "Claude Code session save/resume (~/.claude/session-data)",
|
||||
"security-scan.md": "wraps AgentShield commercial product",
|
||||
"sessions.md": "Claude Code session management",
|
||||
"setup-pm.md": "runs an ECC repo script (scripts/setup-package-manager.js)",
|
||||
"skill-create.md": "Claude Code skill authoring plus instincts",
|
||||
"skill-health.md": "ECC skill analytics dashboard",
|
||||
"vue-review.md": "invokes ECC agent roster (vue-reviewer)"
|
||||
}
|
||||
},
|
||||
"safety": {
|
||||
"semantics": "URLs: documentation hosts in urlAllowlistHosts, placeholder hosts (example.com/org/net and subdomains), and non-FQDN internal hostnames (localhost, docker service names) are allowed; everything else fails. Runtime downloads: pipe-to-shell (curl|sh, wget|sh) and fetch-and-run npx forms (-y/--yes, pkg@version, create-*, degit, \"skills add\") fail. Local-first npx <tool> and standard project dependency installation (pip install <dep>) are the reader's own project workflow and are allowed; skills whose own operation downloads tooling are excluded above. Absolute per-user home paths fail; portable tilde references are allowed.",
|
||||
"urlAllowlistHosts": [
|
||||
"aka.ms",
|
||||
"angular.dev",
|
||||
"aws.amazon.com",
|
||||
"bloclibrary.dev",
|
||||
"cli.github.com",
|
||||
"developer.android.com",
|
||||
"developer.apple.com",
|
||||
"developers.cloudflare.com",
|
||||
"dart.dev",
|
||||
"docs.flutter.dev",
|
||||
"external-secrets.io",
|
||||
"github.com",
|
||||
"isocpp.github.io",
|
||||
"kotlinlang.org",
|
||||
"modelcontextprotocol.io",
|
||||
"nextjs.org",
|
||||
"owasp.org",
|
||||
"portswigger.net",
|
||||
"pub.dev",
|
||||
"riverpod.dev",
|
||||
"supabase.com",
|
||||
"vite.dev",
|
||||
"www.cisecurity.org",
|
||||
"www.speedscope.app",
|
||||
"www.terraform.io",
|
||||
"www.w3.org"
|
||||
],
|
||||
"defaultAllowedHosts": [
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
"[::1]",
|
||||
"0.0.0.0",
|
||||
"example.com",
|
||||
"example.org",
|
||||
"example.net"
|
||||
],
|
||||
"scanAllowlist": [
|
||||
{
|
||||
"path": "skills/tdd-workflow/SKILL.md",
|
||||
"contains": "curl ... | sh` must be rejected",
|
||||
"reason": "anti-pattern warning telling reviewers to reject pipe-to-shell; not an instruction"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
# Curation
|
||||
|
||||
pi/core includes 123 of 293 skills and 24 of 94 commands from the root of ECC.
|
||||
Everything excluded is listed here with its reason.
|
||||
|
||||
## Rules
|
||||
|
||||
Include: language/framework skills; testing/TDD; code review; security review (non-offensive); planning; refactoring; docs; git/PR workflows; prompt commands that are pure prompt workflows.
|
||||
|
||||
Exclude anything that:
|
||||
- makes network calls, needs API keys, or downloads at runtime (npx, pip install, curl|sh)
|
||||
- wraps third-party SaaS (exa, context7, fal, videodb, x-api, social/marketing/SEO/outreach)
|
||||
- integrates commercial products (ECC Tools, ecc.tools, AgentShield, billing/ops skills)
|
||||
- depends on Claude-Code-only mechanics (hooks, agent rosters/orch-*, continuous-learning, loops, hookify, cost tracking, session save/resume, gateguard)
|
||||
- venture-specific (ito-*, prediction-market-*, hermes-*, nasiko-*, openclaw-*, x402)
|
||||
- niche domain pack (healthcare/HIPAA, supply chain/logistics, homelab, scientific, visa/legal docs) or offensive security (bug bounty)
|
||||
|
||||
## Excluded skills
|
||||
|
||||
| Skill | Reason |
|
||||
|---|---|
|
||||
| `agent-payment-x402` | venture-specific x402 payments; wallet and network runtime |
|
||||
| `agent-sort` | ECC install planner over the full ECC catalog; not portable |
|
||||
| `agentic-os` | Claude-Code-only persistent OS mechanics (slash commands, memory, schedules) |
|
||||
| `ai-regression-testing` | workflow creates Claude Code custom slash commands (.claude/commands) |
|
||||
| `article-writing` | content/marketing writing, not engineering |
|
||||
| `automation-audit-ops` | ECC ops audit of hooks/connectors/MCP surfaces |
|
||||
| `autonomous-agent-harness` | Claude-Code-only autonomous harness (hooks, scheduling, computer use) |
|
||||
| `autonomous-loops` | Claude-Code-only autonomous loops |
|
||||
| `benchmark-methodology` | competitive/marketing benchmarking |
|
||||
| `blender-motion-state-inspection` | niche 3D/Blender domain pack |
|
||||
| `brand-discovery` | brand/marketing content |
|
||||
| `brand-voice` | marketing/outreach voice profiling |
|
||||
| `browser-qa` | requires a browser automation MCP server at runtime |
|
||||
| `canary-watch` | makes network calls to deployed URLs |
|
||||
| `carrier-relationship-management` | supply chain/logistics domain pack |
|
||||
| `cisco-ios-patterns` | network-device ops niche domain |
|
||||
| `ck` | Claude-Code-only persistent memory commands |
|
||||
| `claude-devfleet` | multi-agent orchestration via external DevFleet product |
|
||||
| `codehealth-mcp` | wraps CodeScene MCP SaaS |
|
||||
| `competitive-platform-analysis` | competitive/marketing analysis |
|
||||
| `competitive-report-structure` | competitive/marketing reporting |
|
||||
| `config-gc` | Claude-Code-only config garbage collection (~/.claude) |
|
||||
| `configure-ecc` | ECC-specific setup wizard |
|
||||
| `connections-optimizer` | social/outreach (X and LinkedIn) |
|
||||
| `content-engine` | social/marketing content system |
|
||||
| `context-budget` | Claude-Code-only context window audit |
|
||||
| `continuous-agent-loop` | continuous agent loops |
|
||||
| `continuous-learning` | Claude-Code-only hooks-based continuous learning (deprecated) |
|
||||
| `continuous-learning-v2` | Claude-Code-only hooks-based continuous learning |
|
||||
| `cost-tracking` | Claude Code cost tracking |
|
||||
| `council-multi-model` | requires external Codex CLI at runtime |
|
||||
| `counterparty-channel-discipline` | agent messaging ops policy |
|
||||
| `crosspost` | social/marketing distribution |
|
||||
| `customer-billing-ops` | billing/ops skill over connected billing tools |
|
||||
| `customs-trade-compliance` | customs/trade niche domain |
|
||||
| `data-scraper-agent` | scheduled network scraping agent |
|
||||
| `deep-research` | requires firecrawl and exa SaaS MCP tools |
|
||||
| `defi-amm-security` | crypto/DeFi niche domain |
|
||||
| `delivery-gate` | Claude-Code-only stop hook |
|
||||
| `dmux-workflows` | multi-agent orchestration via dmux |
|
||||
| `documentation-lookup` | wraps Context7 SaaS MCP |
|
||||
| `dynamic-workflow-mode` | Claude dynamic workflow mode mechanics |
|
||||
| `ecc-guide` | ECC repository self-reference |
|
||||
| `ecc-recipes` | ECC command catalog self-reference |
|
||||
| `ecc-tools-cost-audit` | ECC Tools commercial billing/ops |
|
||||
| `email-ops` | mailbox ops skill |
|
||||
| `energy-procurement` | energy procurement niche domain |
|
||||
| `enterprise-agent-ops` | agent runtime ops, not portable engineering |
|
||||
| `esign-field-placement` | niche e-sign browser automation |
|
||||
| `eval-harness` | reads and writes .claude/evals (Claude-Code-only) |
|
||||
| `evm-token-decimals` | crypto/EVM niche domain |
|
||||
| `exa-search` | wraps Exa SaaS |
|
||||
| `fal-ai-media` | wraps fal.ai SaaS |
|
||||
| `finance-billing-ops` | billing/ops skill |
|
||||
| `flox-environments` | runtime installer flow (curl\|sh) for Flox |
|
||||
| `frontend-design-direction` | ECC-specific design direction |
|
||||
| `frontend-slides` | presentation/content production, not engineering |
|
||||
| `gan-style-harness` | Claude-Code-only generator/evaluator harness |
|
||||
| `gateguard` | Claude-Code-only PreToolUse gate |
|
||||
| `generating-python-installer` | niche Windows installer packaging with runtime downloads |
|
||||
| `github-ops` | makes GitHub API calls via gh at runtime |
|
||||
| `google-workspace-ops` | wraps Google Workspace SaaS |
|
||||
| `growth-log` | ECC learning-log workflow |
|
||||
| `healthcare-cdss-patterns` | healthcare niche domain pack |
|
||||
| `healthcare-emr-patterns` | healthcare niche domain pack |
|
||||
| `healthcare-eval-harness` | healthcare niche domain pack |
|
||||
| `healthcare-phi-compliance` | healthcare/PHI niche domain pack |
|
||||
| `hermes-imports` | venture-specific hermes-* |
|
||||
| `hipaa-compliance` | HIPAA niche domain pack |
|
||||
| `homelab-network-readiness` | homelab niche domain pack |
|
||||
| `homelab-network-setup` | homelab niche domain pack |
|
||||
| `homelab-pihole-dns` | homelab niche domain pack |
|
||||
| `homelab-vlan-segmentation` | homelab niche domain pack |
|
||||
| `homelab-wireguard-vpn` | homelab niche domain pack |
|
||||
| `hookify-rules` | hookify (Claude-Code-only hooks) |
|
||||
| `i18n-sync` | built around a third-party npm CLI (locakit) with external source links; not self-contained |
|
||||
| `inventory-demand-planning` | supply chain niche domain |
|
||||
| `investor-materials` | fundraising/marketing content |
|
||||
| `investor-outreach` | fundraising outreach |
|
||||
| `ios-icon-gen` | Iconify API network calls at runtime |
|
||||
| `iterative-retrieval` | multi-agent subagent context mechanics; links to external social post |
|
||||
| `ito-baskets` | venture-specific ito-* |
|
||||
| `ito-compute` | venture-specific ito-* |
|
||||
| `ito-inference` | venture-specific ito-* |
|
||||
| `ito-training` | venture-specific ito-* |
|
||||
| `jira-integration` | wraps Jira SaaS API |
|
||||
| `knowledge-ops` | ops skill over MCP memory and vector stores |
|
||||
| `laravel-plugin-discovery` | wraps LaraPlugins.io SaaS MCP |
|
||||
| `lead-intelligence` | sales outreach pipeline |
|
||||
| `llm-trading-agent-security` | trading-agent niche domain |
|
||||
| `logistics-exception-management` | logistics niche domain |
|
||||
| `loop-design-check` | agent loop design mechanics |
|
||||
| `mailtrap-email-integration` | wraps Mailtrap SaaS API |
|
||||
| `manim-video` | niche video production; pip installs at runtime |
|
||||
| `market-research` | web research over network sources |
|
||||
| `marketing-campaign` | marketing |
|
||||
| `master-agreement-generator` | legal docs niche |
|
||||
| `messages-ops` | messaging ops skill |
|
||||
| `nanoclaw-repl` | ECC product-specific REPL |
|
||||
| `nasiko-control-plane` | venture-specific nasiko-* |
|
||||
| `netmiko-ssh-automation` | network-device ops niche; SSH at runtime |
|
||||
| `network-bgp-diagnostics` | network ops niche domain |
|
||||
| `network-config-validation` | network ops niche domain |
|
||||
| `network-interface-health` | network ops niche domain |
|
||||
| `nodejs-keccak256` | crypto/EVM niche domain |
|
||||
| `nutrient-document-processing` | wraps Nutrient DWS SaaS API |
|
||||
| `openclaw-persona-forge` | venture-specific openclaw-* |
|
||||
| `opensource-pipeline` | multi-agent roster pipeline |
|
||||
| `operator-approval-loop` | ECC ops approval contract |
|
||||
| `orch-add-feature` | Claude-Code-only orch-* orchestration |
|
||||
| `orch-build-mvp` | Claude-Code-only orch-* orchestration |
|
||||
| `orch-change-feature` | Claude-Code-only orch-* orchestration |
|
||||
| `orch-fix-defect` | Claude-Code-only orch-* orchestration |
|
||||
| `orch-pipeline` | Claude-Code-only orch-* orchestration |
|
||||
| `orch-refine-code` | Claude-Code-only orch-* orchestration |
|
||||
| `plan-canvas` | Claude-Code-only plan canvas server |
|
||||
| `plan-orchestrate` | ECC orchestration prompt generator over the full catalog |
|
||||
| `plankton-code-quality` | Claude-Code-only write-time hooks |
|
||||
| `prediction-market-oracle-research` | venture-specific prediction-market-* |
|
||||
| `prediction-market-risk-review` | venture-specific prediction-market-* |
|
||||
| `production-scheduling` | manufacturing niche domain |
|
||||
| `project-flow-ops` | ops over GitHub/Linear SaaS |
|
||||
| `prompt-optimizer` | ECC command/agent catalog self-reference |
|
||||
| `quality-nonconformance` | regulated manufacturing niche |
|
||||
| `ralphinho-rfc-pipeline` | multi-agent DAG orchestration |
|
||||
| `recsys-pipeline-architect` | installs upstream package via npx skills add |
|
||||
| `recursive-decision-ledger` | recursive prompting loops |
|
||||
| `remotion-video-creation` | video/media production niche |
|
||||
| `repo-scan` | downloads an external skill at runtime |
|
||||
| `research-ops` | ECC ops research workflow with network enrichment |
|
||||
| `returns-reverse-logistics` | logistics niche domain |
|
||||
| `rules-distill` | Claude-Code-only rules distillation mechanics |
|
||||
| `safety-guard` | Claude-Code-only PreToolUse hooks |
|
||||
| `santa-method` | multi-agent adversarial review roster |
|
||||
| `scientific-db-pubmed-database` | scientific niche domain pack |
|
||||
| `scientific-db-uspto-database` | scientific niche domain pack |
|
||||
| `scientific-pkg-gget` | scientific niche domain pack |
|
||||
| `scientific-thinking-literature-review` | scientific niche domain pack |
|
||||
| `scientific-thinking-scholar-evaluation` | scientific niche domain pack |
|
||||
| `search-first` | network searches (npm/PyPI/GitHub) at runtime |
|
||||
| `security-bounty-hunter` | offensive security (bug bounty) |
|
||||
| `security-scan` | wraps AgentShield commercial product |
|
||||
| `seo` | SEO/marketing |
|
||||
| `skill-comply` | runs agent rosters for compliance checks |
|
||||
| `skill-scout` | network searches of skill marketplaces |
|
||||
| `skill-stocktake` | subagent-based Claude skill audit |
|
||||
| `social-graph-ranker` | social graph (X and LinkedIn) |
|
||||
| `social-publisher` | wraps SocialClaw SaaS |
|
||||
| `strategic-compact` | Claude Code session compaction mechanics |
|
||||
| `taste` | media/creative-direction niche pack |
|
||||
| `taste-application` | media generation via fal.ai SaaS |
|
||||
| `taste-distillation` | media analysis paired with fal.ai SaaS |
|
||||
| `tasteforge-video` | media generation workflow over fal.ai SaaS |
|
||||
| `team-agent-orchestration` | agent squad orchestration |
|
||||
| `team-builder` | Claude agents roster picker |
|
||||
| `terminal-opener` | ECC harness utility, not engineering content |
|
||||
| `terminal-ops` | ECC ops workflow |
|
||||
| `tinystruct-patterns` | niche single-framework pack |
|
||||
| `token-budget-advisor` | session/token mechanics |
|
||||
| `ui-demo` | Playwright video recording with runtime installs |
|
||||
| `ui-to-vue` | runs an npx converter package at runtime |
|
||||
| `uncloud` | niche cluster ops |
|
||||
| `unified-memory` | ECC memory vault (session save/resume family) |
|
||||
| `unified-notifications-ops` | ECC notifications ops |
|
||||
| `video-editing` | media production niche |
|
||||
| `videodb` | wraps VideoDB SaaS |
|
||||
| `visa-doc-translate` | visa/legal docs niche; OCR network calls |
|
||||
| `windows-desktop-e2e` | niche Windows desktop automation; pip installs at runtime |
|
||||
| `workspace-surface-audit` | ECC harness surface audit |
|
||||
| `x-api` | wraps X/Twitter SaaS API |
|
||||
|
||||
## Excluded commands
|
||||
|
||||
| Command | Reason |
|
||||
|---|---|
|
||||
| `auto-update` | ECC self-update/reinstall |
|
||||
| `checkpoint` | writes .claude/checkpoints.log (Claude-Code-only) |
|
||||
| `cost-report` | Claude Code cost tracking |
|
||||
| `cpp-build` | invokes ECC agent roster (cpp-build-resolver) |
|
||||
| `cpp-review` | invokes ECC agent roster (cpp-reviewer) |
|
||||
| `ecc-guide` | ECC repository self-reference |
|
||||
| `epic-claim` | GitHub epic ops over ECC coordination state; network |
|
||||
| `epic-decompose` | GitHub epic ops over ECC coordination state; network |
|
||||
| `epic-publish` | GitHub epic ops over ECC coordination state; network |
|
||||
| `epic-review` | GitHub epic ops over ECC coordination state; network |
|
||||
| `epic-sync` | GitHub epic ops over ECC coordination state; network |
|
||||
| `epic-unblock` | GitHub epic ops over ECC coordination state; network |
|
||||
| `epic-validate` | GitHub epic ops over ECC coordination state; network |
|
||||
| `evolve` | instincts/continuous-learning mechanics |
|
||||
| `flutter-build` | invokes ECC agent roster (dart-build-resolver) |
|
||||
| `flutter-review` | invokes ECC agent roster (flutter-reviewer) |
|
||||
| `gan-build` | GAN generator/evaluator loop harness |
|
||||
| `gan-design` | GAN generator/evaluator loop harness |
|
||||
| `go-build` | invokes ECC agent roster (go-build-resolver) |
|
||||
| `go-review` | invokes ECC agent roster (go-reviewer) |
|
||||
| `harness-audit` | ECC harness audit |
|
||||
| `hookify-configure` | hookify (Claude-Code-only hooks) |
|
||||
| `hookify-help` | hookify (Claude-Code-only hooks) |
|
||||
| `hookify-list` | hookify (Claude-Code-only hooks) |
|
||||
| `hookify` | hookify (Claude-Code-only hooks) |
|
||||
| `instinct-export` | instincts (continuous-learning) |
|
||||
| `instinct-import` | instincts (continuous-learning) |
|
||||
| `instinct-status` | instincts (continuous-learning) |
|
||||
| `jira` | wraps Jira SaaS API |
|
||||
| `kotlin-build` | invokes ECC agent roster (kotlin-build-resolver) |
|
||||
| `kotlin-review` | invokes ECC agent roster (kotlin-reviewer) |
|
||||
| `learn-eval` | continuous-learning session extraction |
|
||||
| `learn` | continuous-learning session extraction |
|
||||
| `loop-start` | autonomous loops |
|
||||
| `loop-status` | autonomous loops |
|
||||
| `marketing-campaign` | marketing |
|
||||
| `model-route` | ECC model routing/cost mechanics |
|
||||
| `multi-backend` | multi-model orchestration |
|
||||
| `multi-execute` | multi-model orchestration |
|
||||
| `multi-frontend` | multi-model orchestration |
|
||||
| `multi-plan` | multi-model orchestration |
|
||||
| `multi-workflow` | multi-model orchestration |
|
||||
| `orch-add-feature` | Claude-Code-only orch-* orchestration |
|
||||
| `orch-build-mvp` | Claude-Code-only orch-* orchestration |
|
||||
| `orch-change-feature` | Claude-Code-only orch-* orchestration |
|
||||
| `orch-fix-defect` | Claude-Code-only orch-* orchestration |
|
||||
| `orch-refine-code` | Claude-Code-only orch-* orchestration |
|
||||
| `orch-review` | Claude-Code-only orch-* orchestration |
|
||||
| `plan-canvas` | Claude-Code-only plan canvas server |
|
||||
| `pm2` | PM2 runtime process manager ops |
|
||||
| `project-init` | ECC install-manifest onboarding plan |
|
||||
| `projects` | instincts (continuous-learning) |
|
||||
| `promote` | instincts (continuous-learning) |
|
||||
| `prune` | instincts (continuous-learning) |
|
||||
| `python-review` | invokes ECC agent roster (python-reviewer) |
|
||||
| `quality-gate` | drives the ECC PostToolUse formatter hook script |
|
||||
| `react-build` | invokes ECC agent roster (react-build-resolver) |
|
||||
| `react-review` | invokes ECC agent roster (react-reviewer) |
|
||||
| `resume-session` | Claude Code session save/resume (~/.claude/session-data) |
|
||||
| `review-pr` | invokes ECC agent roster (specialized review agents) |
|
||||
| `rust-build` | invokes ECC agent roster (rust-build-resolver) |
|
||||
| `rust-review` | invokes ECC agent roster (rust-reviewer) |
|
||||
| `santa-loop` | multi-agent adversarial review loop |
|
||||
| `save-session` | Claude Code session save/resume (~/.claude/session-data) |
|
||||
| `security-scan` | wraps AgentShield commercial product |
|
||||
| `sessions` | Claude Code session management |
|
||||
| `setup-pm` | runs an ECC repo script (scripts/setup-package-manager.js) |
|
||||
| `skill-create` | Claude Code skill authoring plus instincts |
|
||||
| `skill-health` | ECC skill analytics dashboard |
|
||||
| `vue-review` | invokes ECC agent roster (vue-reviewer) |
|
||||
|
||||
## Renames
|
||||
|
||||
- `council` is shipped as `ecc-council` inside pi/core (the root skill keeps its original name).
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Affaan Mustafa
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,40 @@
|
||||
# ecc-pi-core
|
||||
|
||||
A curated, Pi-native profile of ECC (Everything Claude Code): 123 portable
|
||||
engineering skills and 24 pure prompt-workflow commands, with no extensions,
|
||||
no hooks, no runtime downloads, and no network or SaaS dependencies.
|
||||
|
||||
## Contents
|
||||
|
||||
- `skills/` - language, framework, testing/TDD, code review, security review,
|
||||
planning, refactoring, docs, and git/PR workflow skills.
|
||||
- `commands/` - prompt commands that are pure prompt workflows.
|
||||
- `CURATION.md` - every excluded skill and command with its reason.
|
||||
|
||||
## Use
|
||||
|
||||
Copy this directory into your project (or pin a release tarball) and load it with the
|
||||
Pi coding agent:
|
||||
|
||||
```sh
|
||||
pi --no-extensions --extension pi/core
|
||||
```
|
||||
|
||||
Offline load test (as run in CI):
|
||||
|
||||
```sh
|
||||
PI_OFFLINE=1 pi --offline --mode rpc --no-session --no-context-files --no-extensions \
|
||||
--extension pi/core </dev/null >/dev/null
|
||||
```
|
||||
|
||||
## Regenerate
|
||||
|
||||
`pi/core` is generated from `manifests/pi-core.json` and committed so release
|
||||
tarballs contain it verbatim. After changing the manifest or any included source
|
||||
content, run:
|
||||
|
||||
```sh
|
||||
node scripts/build-pi-core.js
|
||||
```
|
||||
|
||||
and commit the result. CI verifies the committed profile is up to date.
|
||||
@@ -0,0 +1,164 @@
|
||||
---
|
||||
description: Answer a quick side question without interrupting or losing context from the current task. Resume work automatically after answering.
|
||||
---
|
||||
|
||||
# Aside Command
|
||||
|
||||
Ask a question mid-task and get an immediate, focused answer — then continue right where you left off. The current task, files, and context are never modified.
|
||||
|
||||
## When to Use
|
||||
|
||||
- You're curious about something while Claude is working and don't want to lose momentum
|
||||
- You need a quick explanation of code Claude is currently editing
|
||||
- You want a second opinion or clarification on a decision without derailing the task
|
||||
- You need to understand an error, concept, or pattern before Claude proceeds
|
||||
- You want to ask something unrelated to the current task without starting a new session
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/aside <your question>
|
||||
/aside what does this function actually return?
|
||||
/aside is this pattern thread-safe?
|
||||
/aside why are we using X instead of Y here?
|
||||
/aside what's the difference between foo() and bar()?
|
||||
/aside should we be worried about the N+1 query we just added?
|
||||
```
|
||||
|
||||
## Process
|
||||
|
||||
### Step 1: Freeze the current task state
|
||||
|
||||
Before answering anything, mentally note:
|
||||
- What is the active task? (what file, feature, or problem was being worked on)
|
||||
- What step was in progress at the moment `/aside` was invoked?
|
||||
- What was about to happen next?
|
||||
|
||||
Do NOT touch, edit, create, or delete any files during the aside.
|
||||
|
||||
### Step 2: Answer the question directly
|
||||
|
||||
Answer the question in the most concise form that is still complete and useful.
|
||||
|
||||
- Lead with the answer, not the reasoning
|
||||
- Keep it short — if a full explanation is needed, offer to go deeper after the task
|
||||
- If the question is about the current file or code being worked on, reference it precisely (file path and line number if relevant)
|
||||
- If answering requires reading a file, read it — but read only, never write
|
||||
|
||||
Format the response as:
|
||||
|
||||
```
|
||||
ASIDE: [restate the question briefly]
|
||||
|
||||
[Your answer here]
|
||||
|
||||
— Back to task: [one-line description of what was being done]
|
||||
```
|
||||
|
||||
### Step 3: Resume the main task
|
||||
|
||||
After delivering the answer, immediately continue the active task from the exact point it was paused. Do not ask for permission to resume unless the aside answer revealed a blocker or a reason to reconsider the current approach (see Edge Cases).
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases
|
||||
|
||||
**No question provided (`/aside` with nothing after it):**
|
||||
Respond:
|
||||
```
|
||||
ASIDE: no question provided
|
||||
|
||||
What would you like to know? (ask your question and I'll answer without losing the current task context)
|
||||
|
||||
— Back to task: [one-line description of what was being done]
|
||||
```
|
||||
|
||||
**Question reveals a potential problem with the current task:**
|
||||
Flag it clearly before resuming:
|
||||
```
|
||||
ASIDE: [answer]
|
||||
|
||||
WARNING: Note: This answer suggests [issue] with the current approach. Want to address this before continuing, or proceed as planned?
|
||||
```
|
||||
Wait for the user's decision before resuming.
|
||||
|
||||
**Question is actually a task redirect (not a side question):**
|
||||
If the question implies changing what is being built (e.g., `/aside actually, let's use Redis instead`), clarify:
|
||||
```
|
||||
ASIDE: That sounds like a direction change, not just a side question.
|
||||
Do you want to:
|
||||
(a) Answer this as information only and keep the current plan
|
||||
(b) Pause the current task and change approach
|
||||
```
|
||||
Wait for the user's answer — do not make assumptions.
|
||||
|
||||
**Question is about the currently open file or code:**
|
||||
Answer from the live context. If the file was read earlier in the session, reference it directly. If not, read it now (read-only) and answer with a file:line reference.
|
||||
|
||||
**No active task (nothing in progress when `/aside` is invoked):**
|
||||
Still use the standard wrapper so the response shape stays consistent:
|
||||
```
|
||||
ASIDE: [restate the question briefly]
|
||||
|
||||
[Your answer here]
|
||||
|
||||
— Back to task: no active task to resume
|
||||
```
|
||||
|
||||
**Question requires a long answer:**
|
||||
Give the essential answer concisely, then offer:
|
||||
```
|
||||
That's the short version. Want a deeper explanation after we finish [current task]?
|
||||
```
|
||||
|
||||
**Multiple `/aside` questions in a row:**
|
||||
Answer each one in sequence. After the last answer, resume the main task. Do not lose task state across a chain of asides.
|
||||
|
||||
**Aside answer implies a code change is needed:**
|
||||
Note the change needed but do not make it during the aside:
|
||||
```
|
||||
ASIDE: [answer]
|
||||
|
||||
Worth fixing: [what should be changed]. I'll flag this after the current task unless you want to address it now.
|
||||
```
|
||||
|
||||
**Question is ambiguous or too vague:**
|
||||
Ask one clarifying question — the shortest question that gets the information needed to answer. Do not ask multiple questions.
|
||||
|
||||
---
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
User: /aside what does fetchWithRetry() actually do?
|
||||
|
||||
ASIDE: what does fetchWithRetry() do?
|
||||
|
||||
fetchWithRetry() (src/api/retry.ts:12) attempts the request up to 3 times with
|
||||
exponential backoff (250ms → 500ms → 1s). It only retries on 5xx responses and
|
||||
network errors — 4xx errors are treated as final and not retried.
|
||||
|
||||
— Back to task: refactoring the auth middleware in src/middleware/auth.ts
|
||||
```
|
||||
|
||||
```
|
||||
User: /aside is the approach we're taking thread-safe?
|
||||
|
||||
ASIDE: is the current approach thread-safe?
|
||||
|
||||
No — the shared cache object in src/cache/store.ts:34 is mutated without locking.
|
||||
Under concurrent requests this is a race condition. It's low risk in a single-process
|
||||
Node.js server but would be a real problem with worker threads or clustering.
|
||||
|
||||
WARNING: Note: This could affect the feature we're building. Want to address this now or continue and fix it in a follow-up?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Never modify files during an aside — read-only access only
|
||||
- The aside is a conversation pause, not a new task — the original task must always resume
|
||||
- Keep answers focused: the goal is to unblock the user quickly, not to deliver a lecture
|
||||
- If an aside sparks a larger discussion, finish the current task first unless the aside reveals a blocker
|
||||
- Asides are not saved to session files unless explicitly relevant to the task outcome
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
description: Detect the project build system and incrementally fix build/type errors with minimal safe changes.
|
||||
---
|
||||
|
||||
# Build and Fix
|
||||
|
||||
Incrementally fix build and type errors with minimal, safe changes.
|
||||
|
||||
## Step 1: Detect Build System
|
||||
|
||||
Identify the project's build tool and run the build:
|
||||
|
||||
| Indicator | Build Command |
|
||||
|-----------|---------------|
|
||||
| `package.json` with `build` script | `npm run build` or `pnpm build` |
|
||||
| `tsconfig.json` (TypeScript only) | `npx tsc --noEmit` |
|
||||
| `Cargo.toml` | `cargo build 2>&1` |
|
||||
| `pom.xml` | `mvn compile` |
|
||||
| `build.gradle` | `./gradlew compileJava` |
|
||||
| `go.mod` | `go build ./...` |
|
||||
| `pyproject.toml` | `python -m compileall -q .` or `mypy .` |
|
||||
|
||||
## Step 2: Parse and Group Errors
|
||||
|
||||
1. Run the build command and capture stderr
|
||||
2. Group errors by file path
|
||||
3. Sort by dependency order (fix imports/types before logic errors)
|
||||
4. Count total errors for progress tracking
|
||||
|
||||
## Step 3: Fix Loop (One Error at a Time)
|
||||
|
||||
For each error:
|
||||
|
||||
1. **Read the file** — Use Read tool to see error context (10 lines around the error)
|
||||
2. **Diagnose** — Identify root cause (missing import, wrong type, syntax error)
|
||||
3. **Fix minimally** — Use Edit tool for the smallest change that resolves the error
|
||||
4. **Re-run build** — Verify the error is gone and no new errors introduced
|
||||
5. **Move to next** — Continue with remaining errors
|
||||
|
||||
## Step 4: Guardrails
|
||||
|
||||
Stop and ask the user if:
|
||||
- A fix introduces **more errors than it resolves**
|
||||
- The **same error persists after 3 attempts** (likely a deeper issue)
|
||||
- The fix requires **architectural changes** (not just a build fix)
|
||||
- Build errors stem from **missing dependencies** (need `npm install`, `cargo add`, etc.)
|
||||
|
||||
## Step 5: Summary
|
||||
|
||||
Show results:
|
||||
- Errors fixed (with file paths)
|
||||
- Errors remaining (if any)
|
||||
- New errors introduced (should be zero)
|
||||
- Suggested next steps for unresolved issues
|
||||
|
||||
## Recovery Strategies
|
||||
|
||||
| Situation | Action |
|
||||
|-----------|--------|
|
||||
| Missing module/import | Check if package is installed; suggest install command |
|
||||
| Type mismatch | Read both type definitions; fix the narrower type |
|
||||
| Circular dependency | Identify cycle with import graph; suggest extraction |
|
||||
| Version conflict | Check `package.json` / `Cargo.toml` for version constraints |
|
||||
| Build tool misconfiguration | Read config file; compare with working defaults |
|
||||
|
||||
Fix one error at a time for safety. Prefer minimal diffs over refactoring.
|
||||
@@ -0,0 +1,289 @@
|
||||
---
|
||||
description: Code review — local uncommitted changes or GitHub PR (pass PR number/URL for PR mode). Use for a step-by-step PRP-style checklist review; for a multi-agent pass use /review-pr, and for the adversarially-verified Workflow pass use /orch-review.
|
||||
argument-hint: [pr-number | pr-url | blank for local review]
|
||||
---
|
||||
|
||||
# Code Review
|
||||
|
||||
> PR review mode adapted from PRPs-agentic-eng by Wirasm. Part of the PRP workflow series.
|
||||
|
||||
**Input**: $ARGUMENTS
|
||||
|
||||
---
|
||||
|
||||
## Mode Selection
|
||||
|
||||
If `$ARGUMENTS` contains a PR number, PR URL, or `--pr`:
|
||||
→ Jump to **PR Review Mode** below.
|
||||
|
||||
Otherwise:
|
||||
→ Use **Local Review Mode**.
|
||||
|
||||
---
|
||||
|
||||
## Local Review Mode
|
||||
|
||||
Comprehensive security and quality review of uncommitted changes.
|
||||
|
||||
### Phase 1 — GATHER
|
||||
|
||||
```bash
|
||||
git diff --name-only HEAD
|
||||
```
|
||||
|
||||
If no changed files, stop: "Nothing to review."
|
||||
|
||||
### Phase 2 — REVIEW
|
||||
|
||||
Read each changed file in full. Check for:
|
||||
|
||||
**Security Issues (CRITICAL):**
|
||||
- Hardcoded credentials, API keys, tokens
|
||||
- SQL injection vulnerabilities
|
||||
- XSS vulnerabilities
|
||||
- Missing input validation
|
||||
- Insecure dependencies
|
||||
- Path traversal risks
|
||||
|
||||
**Code Quality (HIGH):**
|
||||
- Functions > 50 lines
|
||||
- Files > 800 lines
|
||||
- Nesting depth > 4 levels
|
||||
- Missing error handling
|
||||
- console.log statements
|
||||
- TODO/FIXME comments
|
||||
- Missing JSDoc for public APIs
|
||||
|
||||
**Best Practices (MEDIUM):**
|
||||
- Mutation patterns (use immutable instead)
|
||||
- Emoji usage in code/comments
|
||||
- Missing tests for new code
|
||||
- Accessibility issues (a11y)
|
||||
|
||||
### Phase 3 — REPORT
|
||||
|
||||
Generate report with:
|
||||
- Severity: CRITICAL, HIGH, MEDIUM, LOW
|
||||
- File location and line numbers
|
||||
- Issue description
|
||||
- Suggested fix
|
||||
|
||||
Block commit if CRITICAL or HIGH issues found.
|
||||
Never approve code with security vulnerabilities.
|
||||
|
||||
---
|
||||
|
||||
## PR Review Mode
|
||||
|
||||
Comprehensive GitHub PR review — fetches diff, reads full files, runs validation, posts review.
|
||||
|
||||
### Phase 1 — FETCH
|
||||
|
||||
Parse input to determine PR:
|
||||
|
||||
| Input | Action |
|
||||
|---|---|
|
||||
| Number (e.g. `42`) | Use as PR number |
|
||||
| URL (`github.com/.../pull/42`) | Extract PR number |
|
||||
| Branch name | Find PR via `gh pr list --head <branch>` |
|
||||
|
||||
```bash
|
||||
gh pr view <NUMBER> --json number,title,body,author,baseRefName,headRefName,changedFiles,additions,deletions
|
||||
gh pr diff <NUMBER>
|
||||
```
|
||||
|
||||
If PR not found, stop with error. Store PR metadata for later phases.
|
||||
|
||||
### Phase 2 — CONTEXT
|
||||
|
||||
Build review context:
|
||||
|
||||
1. **Project rules** — Read `CLAUDE.md`, `.claude/docs/`, and any contributing guidelines
|
||||
2. **Planning artifacts** — Check `.claude/prds/`, `.claude/plans/`, `.claude/reviews/`, and legacy `.claude/PRPs/{prds,plans,reports,reviews}/` for context related to this PR
|
||||
3. **PR intent** — Parse PR description for goals, linked issues, test plans
|
||||
4. **Changed files** — List all modified files and categorize by type (source, test, config, docs)
|
||||
|
||||
### Phase 3 — REVIEW
|
||||
|
||||
Read each changed file **in full** (not just the diff hunks — you need surrounding context).
|
||||
|
||||
For PR reviews, fetch the full file contents at the PR head revision:
|
||||
```bash
|
||||
gh pr diff <NUMBER> --name-only | while IFS= read -r file; do
|
||||
gh api "repos/{owner}/{repo}/contents/$file?ref=<head-branch>" --jq '.content' | base64 -d
|
||||
done
|
||||
```
|
||||
|
||||
Apply the review checklist across 7 categories:
|
||||
|
||||
| Category | What to Check |
|
||||
|---|---|
|
||||
| **Correctness** | Logic errors, off-by-ones, null handling, edge cases, race conditions |
|
||||
| **Type Safety** | Type mismatches, unsafe casts, `any` usage, missing generics |
|
||||
| **Pattern Compliance** | Matches project conventions (naming, file structure, error handling, imports) |
|
||||
| **Security** | Injection, auth gaps, secret exposure, SSRF, path traversal, XSS |
|
||||
| **Performance** | N+1 queries, missing indexes, unbounded loops, memory leaks, large payloads |
|
||||
| **Completeness** | Missing tests, missing error handling, incomplete migrations, missing docs |
|
||||
| **Maintainability** | Dead code, magic numbers, deep nesting, unclear naming, missing types |
|
||||
|
||||
Assign severity to each finding:
|
||||
|
||||
| Severity | Meaning | Action |
|
||||
|---|---|---|
|
||||
| **CRITICAL** | Security vulnerability or data loss risk | Must fix before merge |
|
||||
| **HIGH** | Bug or logic error likely to cause issues | Should fix before merge |
|
||||
| **MEDIUM** | Code quality issue or missing best practice | Fix recommended |
|
||||
| **LOW** | Style nit or minor suggestion | Optional |
|
||||
|
||||
### Phase 4 — VALIDATE
|
||||
|
||||
Run available validation commands:
|
||||
|
||||
Detect the project type from config files (`package.json`, `Cargo.toml`, `go.mod`, `pyproject.toml`, etc.), then run the appropriate commands:
|
||||
|
||||
**Node.js / TypeScript** (has `package.json`):
|
||||
```bash
|
||||
npm run typecheck 2>/dev/null || npx tsc --noEmit 2>/dev/null # Type check
|
||||
npm run lint # Lint
|
||||
npm test # Tests
|
||||
npm run build # Build
|
||||
```
|
||||
|
||||
**Rust** (has `Cargo.toml`):
|
||||
```bash
|
||||
cargo clippy -- -D warnings # Lint
|
||||
cargo test # Tests
|
||||
cargo build # Build
|
||||
```
|
||||
|
||||
**Go** (has `go.mod`):
|
||||
```bash
|
||||
go vet ./... # Lint
|
||||
go test ./... # Tests
|
||||
go build ./... # Build
|
||||
```
|
||||
|
||||
**Python** (has `pyproject.toml` / `setup.py`):
|
||||
```bash
|
||||
pytest # Tests
|
||||
```
|
||||
|
||||
Run only the commands that apply to the detected project type. Record pass/fail for each.
|
||||
|
||||
### Phase 5 — DECIDE
|
||||
|
||||
Form recommendation based on findings:
|
||||
|
||||
| Condition | Decision |
|
||||
|---|---|
|
||||
| Zero CRITICAL/HIGH issues, validation passes | **APPROVE** |
|
||||
| Only MEDIUM/LOW issues, validation passes | **APPROVE** with comments |
|
||||
| Any HIGH issues or validation failures | **REQUEST CHANGES** |
|
||||
| Any CRITICAL issues | **BLOCK** — must fix before merge |
|
||||
|
||||
Special cases:
|
||||
- Draft PR → Always use **COMMENT** (not approve/block)
|
||||
- Only docs/config changes → Lighter review, focus on correctness
|
||||
- Explicit `--approve` or `--request-changes` flag → Override decision (but still report all findings)
|
||||
|
||||
### Phase 6 — REPORT
|
||||
|
||||
Create review artifact at `.claude/reviews/pr-<NUMBER>-review.md` unless the repo already uses legacy `.claude/PRPs/reviews/` for this workstream:
|
||||
|
||||
```markdown
|
||||
# PR Review: #<NUMBER> — <TITLE>
|
||||
|
||||
**Reviewed**: <date>
|
||||
**Author**: <author>
|
||||
**Branch**: <head> → <base>
|
||||
**Decision**: APPROVE | REQUEST CHANGES | BLOCK
|
||||
|
||||
## Summary
|
||||
<1-2 sentence overall assessment>
|
||||
|
||||
## Findings
|
||||
|
||||
### CRITICAL
|
||||
<findings or "None">
|
||||
|
||||
### HIGH
|
||||
<findings or "None">
|
||||
|
||||
### MEDIUM
|
||||
<findings or "None">
|
||||
|
||||
### LOW
|
||||
<findings or "None">
|
||||
|
||||
## Validation Results
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| Type check | Pass / Fail / Skipped |
|
||||
| Lint | Pass / Fail / Skipped |
|
||||
| Tests | Pass / Fail / Skipped |
|
||||
| Build | Pass / Fail / Skipped |
|
||||
|
||||
## Files Reviewed
|
||||
<list of files with change type: Added/Modified/Deleted>
|
||||
```
|
||||
|
||||
### Phase 7 — PUBLISH
|
||||
|
||||
Post the review to GitHub:
|
||||
|
||||
```bash
|
||||
# If APPROVE
|
||||
gh pr review <NUMBER> --approve --body "<summary of review>"
|
||||
|
||||
# If REQUEST CHANGES
|
||||
gh pr review <NUMBER> --request-changes --body "<summary with required fixes>"
|
||||
|
||||
# If COMMENT only (draft PR or informational)
|
||||
gh pr review <NUMBER> --comment --body "<summary>"
|
||||
```
|
||||
|
||||
For inline comments on specific lines, use the GitHub review comments API:
|
||||
```bash
|
||||
gh api "repos/{owner}/{repo}/pulls/<NUMBER>/comments" \
|
||||
-f body="<comment>" \
|
||||
-f path="<file>" \
|
||||
-F line=<line-number> \
|
||||
-f side="RIGHT" \
|
||||
-f commit_id="$(gh pr view <NUMBER> --json headRefOid --jq .headRefOid)"
|
||||
```
|
||||
|
||||
Alternatively, post a single review with multiple inline comments at once:
|
||||
```bash
|
||||
gh api "repos/{owner}/{repo}/pulls/<NUMBER>/reviews" \
|
||||
-f event="COMMENT" \
|
||||
-f body="<overall summary>" \
|
||||
--input comments.json # [{"path": "file", "line": N, "body": "comment"}, ...]
|
||||
```
|
||||
|
||||
### Phase 8 — OUTPUT
|
||||
|
||||
Report to user:
|
||||
|
||||
```
|
||||
PR #<NUMBER>: <TITLE>
|
||||
Decision: <APPROVE|REQUEST_CHANGES|BLOCK>
|
||||
|
||||
Issues: <critical_count> critical, <high_count> high, <medium_count> medium, <low_count> low
|
||||
Validation: <pass_count>/<total_count> checks passed
|
||||
|
||||
Artifacts:
|
||||
Review: .claude/reviews/pr-<NUMBER>-review.md
|
||||
GitHub: <PR URL>
|
||||
|
||||
Next steps:
|
||||
- <contextual suggestions based on decision>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- **No `gh` CLI**: Fall back to local-only review (read the diff, skip GitHub publish). Warn user.
|
||||
- **Diverged branches**: Suggest `git fetch origin && git rebase origin/<base>` before review.
|
||||
- **Large PRs (>50 files)**: Warn about review scope. Focus on source changes first, then tests, then config/docs.
|
||||
@@ -0,0 +1,251 @@
|
||||
---
|
||||
description: Enforce TDD workflow for C++. Write GoogleTest tests first, then implement. Verify coverage with gcov/lcov.
|
||||
---
|
||||
|
||||
# C++ TDD Command
|
||||
|
||||
This command enforces test-driven development methodology for C++ code using GoogleTest/GoogleMock with CMake/CTest.
|
||||
|
||||
## What This Command Does
|
||||
|
||||
1. **Define Interfaces**: Scaffold class/function signatures first
|
||||
2. **Write Tests**: Create comprehensive GoogleTest test cases (RED)
|
||||
3. **Run Tests**: Verify tests fail for the right reason
|
||||
4. **Implement Code**: Write minimal code to pass (GREEN)
|
||||
5. **Refactor**: Improve while keeping tests green
|
||||
6. **Check Coverage**: Ensure 80%+ coverage
|
||||
|
||||
## When to Use
|
||||
|
||||
Use `/cpp-test` when:
|
||||
- Implementing new C++ functions or classes
|
||||
- Adding test coverage to existing code
|
||||
- Fixing bugs (write failing test first)
|
||||
- Building critical business logic
|
||||
- Learning TDD workflow in C++
|
||||
|
||||
## TDD Cycle
|
||||
|
||||
```
|
||||
RED → Write failing GoogleTest test
|
||||
GREEN → Implement minimal code to pass
|
||||
REFACTOR → Improve code, tests stay green
|
||||
REPEAT → Next test case
|
||||
```
|
||||
|
||||
## Example Session
|
||||
|
||||
````
|
||||
User: /cpp-test I need a function to validate email addresses
|
||||
|
||||
Agent:
|
||||
# TDD Session: Email Validator
|
||||
|
||||
## Step 1: Define Interface
|
||||
|
||||
```cpp
|
||||
// validator/email.hpp
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <expected>
|
||||
|
||||
enum class EmailError {
|
||||
Empty,
|
||||
InvalidFormat
|
||||
};
|
||||
|
||||
std::expected<void, EmailError> validate_email(const std::string& email);
|
||||
```
|
||||
|
||||
## Step 2: Write Tests (RED)
|
||||
|
||||
```cpp
|
||||
// validator/email_test.cpp
|
||||
#include <gtest/gtest.h>
|
||||
#include "email.hpp"
|
||||
|
||||
TEST(ValidateEmail, AcceptsSimpleEmail) {
|
||||
auto result = validate_email("user@example.com");
|
||||
EXPECT_TRUE(result.has_value());
|
||||
}
|
||||
|
||||
TEST(ValidateEmail, AcceptsSubdomain) {
|
||||
EXPECT_TRUE(validate_email("user@mail.example.com").has_value());
|
||||
}
|
||||
|
||||
TEST(ValidateEmail, AcceptsPlus) {
|
||||
EXPECT_TRUE(validate_email("user+tag@example.com").has_value());
|
||||
}
|
||||
|
||||
TEST(ValidateEmail, RejectsEmpty) {
|
||||
auto result = validate_email("");
|
||||
ASSERT_FALSE(result.has_value());
|
||||
EXPECT_EQ(result.error(), EmailError::Empty);
|
||||
}
|
||||
|
||||
TEST(ValidateEmail, RejectsNoAtSign) {
|
||||
EXPECT_FALSE(validate_email("userexample.com").has_value());
|
||||
}
|
||||
|
||||
TEST(ValidateEmail, RejectsNoDomain) {
|
||||
EXPECT_FALSE(validate_email("user@").has_value());
|
||||
}
|
||||
|
||||
TEST(ValidateEmail, RejectsNoLocalPart) {
|
||||
EXPECT_FALSE(validate_email("@example.com").has_value());
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: Run Tests - Verify FAIL
|
||||
|
||||
```bash
|
||||
$ cmake --build build && ctest --test-dir build --output-on-failure
|
||||
|
||||
1/1 Test #1: email_validator_test .....***Failed
|
||||
--- undefined reference to `validate_email`
|
||||
|
||||
FAIL
|
||||
```
|
||||
|
||||
✓ Tests fail as expected (unimplemented).
|
||||
|
||||
## Step 4: Implement Minimal Code (GREEN)
|
||||
|
||||
```cpp
|
||||
// validator/email.cpp
|
||||
#include "email.hpp"
|
||||
#include <regex>
|
||||
|
||||
std::expected<void, EmailError> validate_email(const std::string& email) {
|
||||
if (email.empty()) {
|
||||
return std::unexpected(EmailError::Empty);
|
||||
}
|
||||
static const std::regex pattern(R"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})");
|
||||
if (!std::regex_match(email, pattern)) {
|
||||
return std::unexpected(EmailError::InvalidFormat);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5: Run Tests - Verify PASS
|
||||
|
||||
```bash
|
||||
$ cmake --build build && ctest --test-dir build --output-on-failure
|
||||
|
||||
1/1 Test #1: email_validator_test ..... Passed 0.01 sec
|
||||
|
||||
100% tests passed.
|
||||
```
|
||||
|
||||
✓ All tests passing!
|
||||
|
||||
## Step 6: Check Coverage
|
||||
|
||||
```bash
|
||||
$ cmake -DCMAKE_CXX_FLAGS="--coverage" -B build && cmake --build build
|
||||
$ ctest --test-dir build
|
||||
$ lcov --capture --directory build --output-file coverage.info
|
||||
$ lcov --list coverage.info
|
||||
|
||||
validator/email.cpp | 100%
|
||||
```
|
||||
|
||||
✓ Coverage: 100%
|
||||
|
||||
## TDD Complete!
|
||||
````
|
||||
|
||||
## Test Patterns
|
||||
|
||||
### Basic Tests
|
||||
```cpp
|
||||
TEST(SuiteName, TestName) {
|
||||
EXPECT_EQ(add(2, 3), 5);
|
||||
EXPECT_NE(result, nullptr);
|
||||
EXPECT_TRUE(is_valid);
|
||||
EXPECT_THROW(func(), std::invalid_argument);
|
||||
}
|
||||
```
|
||||
|
||||
### Fixtures
|
||||
```cpp
|
||||
class DatabaseTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { db_ = create_test_db(); }
|
||||
void TearDown() override { db_.reset(); }
|
||||
std::unique_ptr<Database> db_;
|
||||
};
|
||||
|
||||
TEST_F(DatabaseTest, InsertsRecord) {
|
||||
db_->insert("key", "value");
|
||||
EXPECT_EQ(db_->get("key"), "value");
|
||||
}
|
||||
```
|
||||
|
||||
### Parameterized Tests
|
||||
```cpp
|
||||
class PrimeTest : public ::testing::TestWithParam<std::pair<int, bool>> {};
|
||||
|
||||
TEST_P(PrimeTest, ChecksPrimality) {
|
||||
auto [input, expected] = GetParam();
|
||||
EXPECT_EQ(is_prime(input), expected);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(Primes, PrimeTest, ::testing::Values(
|
||||
std::make_pair(2, true),
|
||||
std::make_pair(4, false),
|
||||
std::make_pair(7, true)
|
||||
));
|
||||
```
|
||||
|
||||
## Coverage Commands
|
||||
|
||||
```bash
|
||||
# Build with coverage
|
||||
cmake -DCMAKE_CXX_FLAGS="--coverage" -DCMAKE_EXE_LINKER_FLAGS="--coverage" -B build
|
||||
|
||||
# Run tests
|
||||
cmake --build build && ctest --test-dir build
|
||||
|
||||
# Generate coverage report
|
||||
lcov --capture --directory build --output-file coverage.info
|
||||
lcov --remove coverage.info '/usr/*' --output-file coverage.info
|
||||
genhtml coverage.info --output-directory coverage_html
|
||||
```
|
||||
|
||||
## Coverage Targets
|
||||
|
||||
| Code Type | Target |
|
||||
|-----------|--------|
|
||||
| Critical business logic | 100% |
|
||||
| Public APIs | 90%+ |
|
||||
| General code | 80%+ |
|
||||
| Generated code | Exclude |
|
||||
|
||||
## TDD Best Practices
|
||||
|
||||
**DO:**
|
||||
- Write test FIRST, before any implementation
|
||||
- Run tests after each change
|
||||
- Use `EXPECT_*` (continues) over `ASSERT_*` (stops) when appropriate
|
||||
- Test behavior, not implementation details
|
||||
- Include edge cases (empty, null, max values, boundary conditions)
|
||||
|
||||
**DON'T:**
|
||||
- Write implementation before tests
|
||||
- Skip the RED phase
|
||||
- Test private methods directly (test through public API)
|
||||
- Use `sleep` in tests
|
||||
- Ignore flaky tests
|
||||
|
||||
## Related Commands
|
||||
|
||||
- `/cpp-build` - Fix build errors
|
||||
- `/cpp-review` - Review code after implementation
|
||||
- `verification-loop` skill - Run full verification loop
|
||||
|
||||
## Related
|
||||
|
||||
- Skill: `skills/cpp-testing/`
|
||||
- Skill: `skills/tdd-workflow/`
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
description: Review a FastAPI application for architecture, async correctness, dependency injection, Pydantic schemas, security, performance, and testability.
|
||||
---
|
||||
|
||||
# FastAPI Review
|
||||
|
||||
Invoke the `fastapi-reviewer` agent for a focused FastAPI review.
|
||||
|
||||
## Usage
|
||||
|
||||
```text
|
||||
/fastapi-review [file-or-directory]
|
||||
```
|
||||
|
||||
## Review Areas
|
||||
|
||||
- App factory, router boundaries, middleware, and exception handlers.
|
||||
- Pydantic request and response schema separation.
|
||||
- Dependency injection for database sessions, auth, pagination, and settings.
|
||||
- Async database and external HTTP patterns.
|
||||
- CORS, auth, rate limits, logging, and secret handling.
|
||||
- OpenAPI metadata and documented response models.
|
||||
- Test client setup and dependency overrides.
|
||||
|
||||
## Expected Output
|
||||
|
||||
```text
|
||||
[SEVERITY] Short issue title
|
||||
File: path/to/file.py:42
|
||||
Issue: What is wrong and why it matters.
|
||||
Fix: Concrete change to make.
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- Agent: `fastapi-reviewer`
|
||||
- Skill: `fastapi-patterns`
|
||||
- Command: `/python-review`
|
||||
- Skill: `security-scan`
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
description: Guided feature development with codebase understanding and architecture focus
|
||||
---
|
||||
|
||||
A structured feature-development workflow that emphasizes understanding existing code before writing new code.
|
||||
|
||||
## Phases
|
||||
|
||||
### 1. Discovery
|
||||
|
||||
- read the feature request carefully
|
||||
- identify requirements, constraints, and acceptance criteria
|
||||
- ask clarifying questions if the request is ambiguous
|
||||
|
||||
### 2. Codebase Exploration
|
||||
|
||||
- use `code-explorer` to analyze the relevant existing code
|
||||
- trace execution paths and architecture layers
|
||||
- understand integration points and conventions
|
||||
|
||||
### 3. Clarifying Questions
|
||||
|
||||
- present findings from exploration
|
||||
- ask targeted design and edge-case questions
|
||||
- wait for user response before proceeding
|
||||
|
||||
### 4. Architecture Design
|
||||
|
||||
- use `code-architect` to design the feature
|
||||
- provide the implementation blueprint
|
||||
- wait for approval before implementing
|
||||
|
||||
### 5. Implementation
|
||||
|
||||
- implement the feature following the approved design
|
||||
- prefer TDD where appropriate
|
||||
- keep commits small and focused
|
||||
|
||||
### 6. Quality Review
|
||||
|
||||
- use `code-reviewer` to review the implementation
|
||||
- address critical and important issues
|
||||
- verify test coverage
|
||||
|
||||
### 7. Summary
|
||||
|
||||
- summarize what was built
|
||||
- list follow-up items or limitations
|
||||
- provide testing instructions
|
||||
@@ -0,0 +1,144 @@
|
||||
---
|
||||
description: Run Flutter/Dart tests, report failures, and incrementally fix test issues. Covers unit, widget, golden, and integration tests.
|
||||
---
|
||||
|
||||
# Flutter Test
|
||||
|
||||
This command runs the Flutter test suite and reports results. When failures occur, it diagnoses and fixes issues incrementally.
|
||||
|
||||
## What This Command Does
|
||||
|
||||
1. **Run Tests**: Execute `flutter test` (or scope to changed files)
|
||||
2. **Parse Failures**: Identify failing tests by type and cause
|
||||
3. **Fix Incrementally**: One failure at a time where possible
|
||||
4. **Verify**: Re-run after each fix
|
||||
5. **Report**: Show coverage summary and remaining failures
|
||||
|
||||
## When to Use
|
||||
|
||||
Use `/flutter-test` when:
|
||||
- After implementing a feature to verify nothing broke
|
||||
- After running `/flutter-build` to ensure tests pass
|
||||
- To check test coverage on new code
|
||||
- When a specific test file is failing
|
||||
- Before submitting a PR
|
||||
|
||||
## Commands Run
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
flutter test 2>&1
|
||||
|
||||
# Run with coverage
|
||||
flutter test --coverage 2>&1
|
||||
|
||||
# Run specific test file
|
||||
flutter test test/unit/domain/usecases/get_user_test.dart 2>&1
|
||||
|
||||
# Run tests matching a name pattern
|
||||
flutter test --name "CartBloc" 2>&1
|
||||
|
||||
# Run integration tests (requires device/emulator)
|
||||
flutter test integration_test/ 2>&1
|
||||
|
||||
# Update golden files when intentional visual changes are made
|
||||
flutter test --update-goldens 2>&1
|
||||
```
|
||||
|
||||
## Example Session
|
||||
|
||||
````text
|
||||
User: /flutter-test
|
||||
|
||||
Agent:
|
||||
# Flutter Test Run
|
||||
|
||||
## Test Execution
|
||||
|
||||
```
|
||||
$ flutter test
|
||||
00:05 +42 -2: Some tests failed.
|
||||
|
||||
FAILED: test/unit/data/user_repository_test.dart
|
||||
UserRepository getById
|
||||
returns null when user not found
|
||||
Expected: null
|
||||
Actual: User(id: 'test-id', name: 'Test User')
|
||||
|
||||
FAILED: test/widget/cart_page_test.dart
|
||||
CartPage
|
||||
shows empty state when cart is empty
|
||||
Expected: exactly one widget with text 'Your cart is empty'
|
||||
Found: no widget with text 'Your cart is empty'
|
||||
```
|
||||
|
||||
## Fix 1: Repository Test
|
||||
|
||||
File: test/unit/data/user_repository_test.dart
|
||||
Failure: `getById` returns a user when it should return null for missing ID
|
||||
|
||||
Root cause: Test setup adds a user with ID 'test-id' but queries with 'missing-id'.
|
||||
Fix: Updated test to query with 'missing-id' — setup was correct, query was wrong.
|
||||
|
||||
```
|
||||
$ flutter test test/unit/data/user_repository_test.dart
|
||||
1 test passed.
|
||||
```
|
||||
|
||||
## Fix 2: Widget Test
|
||||
|
||||
File: test/widget/cart_page_test.dart
|
||||
Failure: Empty state text widget not found
|
||||
|
||||
Root cause: Empty state message was renamed from 'Your cart is empty' to 'Cart is empty' in the widget.
|
||||
Fix: Updated test string to match current widget copy.
|
||||
|
||||
```
|
||||
$ flutter test test/widget/cart_page_test.dart
|
||||
1 test passed.
|
||||
```
|
||||
|
||||
## Final Run
|
||||
|
||||
```
|
||||
$ flutter test --coverage
|
||||
All 44 tests passed.
|
||||
Coverage: 84.2% (target: 80%)
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Total tests | 44 |
|
||||
| Passed | 44 |
|
||||
| Failed | 0 |
|
||||
| Coverage | 84.2% |
|
||||
|
||||
Test Status: PASS ✓
|
||||
````
|
||||
|
||||
## Common Test Failures
|
||||
|
||||
| Failure | Typical Fix |
|
||||
|---------|-------------|
|
||||
| `Expected: <X> Actual: <Y>` | Update assertion or fix implementation |
|
||||
| `Widget not found` | Fix finder selector or update test after widget rename |
|
||||
| `Golden file not found` | Run `flutter test --update-goldens` to generate |
|
||||
| `Golden mismatch` | Inspect diff; run `--update-goldens` if change was intentional |
|
||||
| `MissingPluginException` | Mock platform channel in test setup |
|
||||
| `LateInitializationError` | Initialize `late` fields in `setUp()` |
|
||||
| `pumpAndSettle timed out` | Replace with explicit `pump(Duration)` calls |
|
||||
|
||||
## Related Commands
|
||||
|
||||
- `/flutter-build` — Fix build errors before running tests
|
||||
- `/flutter-review` — Review code after tests pass
|
||||
- `tdd-workflow` skill — Test-driven development workflow
|
||||
|
||||
## Related
|
||||
|
||||
- Agent: `agents/flutter-reviewer.md`
|
||||
- Agent: `agents/dart-build-resolver.md`
|
||||
- Skill: `skills/flutter-dart-code-review/`
|
||||
- Rules: `rules/dart/testing.md`
|
||||
@@ -0,0 +1,268 @@
|
||||
---
|
||||
description: Enforce TDD workflow for Go. Write table-driven tests first, then implement. Verify 80%+ coverage with go test -cover.
|
||||
---
|
||||
|
||||
# Go TDD Command
|
||||
|
||||
This command enforces test-driven development methodology for Go code using idiomatic Go testing patterns.
|
||||
|
||||
## What This Command Does
|
||||
|
||||
1. **Define Types/Interfaces**: Scaffold function signatures first
|
||||
2. **Write Table-Driven Tests**: Create comprehensive test cases (RED)
|
||||
3. **Run Tests**: Verify tests fail for the right reason
|
||||
4. **Implement Code**: Write minimal code to pass (GREEN)
|
||||
5. **Refactor**: Improve while keeping tests green
|
||||
6. **Check Coverage**: Ensure 80%+ coverage
|
||||
|
||||
## When to Use
|
||||
|
||||
Use `/go-test` when:
|
||||
- Implementing new Go functions
|
||||
- Adding test coverage to existing code
|
||||
- Fixing bugs (write failing test first)
|
||||
- Building critical business logic
|
||||
- Learning TDD workflow in Go
|
||||
|
||||
## TDD Cycle
|
||||
|
||||
```
|
||||
RED → Write failing table-driven test
|
||||
GREEN → Implement minimal code to pass
|
||||
REFACTOR → Improve code, tests stay green
|
||||
REPEAT → Next test case
|
||||
```
|
||||
|
||||
## Example Session
|
||||
|
||||
````
|
||||
User: /go-test I need a function to validate email addresses
|
||||
|
||||
Agent:
|
||||
# TDD Session: Email Validator
|
||||
|
||||
## Step 1: Define Interface
|
||||
|
||||
```go
|
||||
// validator/email.go
|
||||
package validator
|
||||
|
||||
// ValidateEmail checks if the given string is a valid email address.
|
||||
// Returns nil if valid, error describing the issue otherwise.
|
||||
func ValidateEmail(email string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
```
|
||||
|
||||
## Step 2: Write Table-Driven Tests (RED)
|
||||
|
||||
```go
|
||||
// validator/email_test.go
|
||||
package validator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateEmail(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
email string
|
||||
wantErr bool
|
||||
}{
|
||||
// Valid emails
|
||||
{"simple email", "user@example.com", false},
|
||||
{"with subdomain", "user@mail.example.com", false},
|
||||
{"with plus", "user+tag@example.com", false},
|
||||
{"with dots", "first.last@example.com", false},
|
||||
|
||||
// Invalid emails
|
||||
{"empty string", "", true},
|
||||
{"no at sign", "userexample.com", true},
|
||||
{"no domain", "user@", true},
|
||||
{"no local part", "@example.com", true},
|
||||
{"double at", "user@@example.com", true},
|
||||
{"spaces", "user @example.com", true},
|
||||
{"no tld", "user@example", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateEmail(tt.email)
|
||||
if tt.wantErr && err == nil {
|
||||
t.Errorf("ValidateEmail(%q) = nil; want error", tt.email)
|
||||
}
|
||||
if !tt.wantErr && err != nil {
|
||||
t.Errorf("ValidateEmail(%q) = %v; want nil", tt.email, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: Run Tests - Verify FAIL
|
||||
|
||||
```bash
|
||||
$ go test ./validator/...
|
||||
|
||||
--- FAIL: TestValidateEmail (0.00s)
|
||||
--- FAIL: TestValidateEmail/simple_email (0.00s)
|
||||
panic: not implemented
|
||||
|
||||
FAIL
|
||||
```
|
||||
|
||||
✓ Tests fail as expected (panic).
|
||||
|
||||
## Step 4: Implement Minimal Code (GREEN)
|
||||
|
||||
```go
|
||||
// validator/email.go
|
||||
package validator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
|
||||
var (
|
||||
ErrEmailEmpty = errors.New("email cannot be empty")
|
||||
ErrEmailInvalid = errors.New("email format is invalid")
|
||||
)
|
||||
|
||||
func ValidateEmail(email string) error {
|
||||
if email == "" {
|
||||
return ErrEmailEmpty
|
||||
}
|
||||
if !emailRegex.MatchString(email) {
|
||||
return ErrEmailInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5: Run Tests - Verify PASS
|
||||
|
||||
```bash
|
||||
$ go test ./validator/...
|
||||
|
||||
PASS
|
||||
ok project/validator 0.003s
|
||||
```
|
||||
|
||||
✓ All tests passing!
|
||||
|
||||
## Step 6: Check Coverage
|
||||
|
||||
```bash
|
||||
$ go test -cover ./validator/...
|
||||
|
||||
PASS
|
||||
coverage: 100.0% of statements
|
||||
ok project/validator 0.003s
|
||||
```
|
||||
|
||||
✓ Coverage: 100%
|
||||
|
||||
## TDD Complete!
|
||||
````
|
||||
|
||||
## Test Patterns
|
||||
|
||||
### Table-Driven Tests
|
||||
```go
|
||||
tests := []struct {
|
||||
name string
|
||||
input InputType
|
||||
want OutputType
|
||||
wantErr bool
|
||||
}{
|
||||
{"case 1", input1, want1, false},
|
||||
{"case 2", input2, want2, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Function(tt.input)
|
||||
// assertions
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Parallel Tests
|
||||
```go
|
||||
for _, tt := range tests {
|
||||
tt := tt // Capture
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// test body
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Test Helpers
|
||||
```go
|
||||
func setupTestDB(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
db := createDB()
|
||||
t.Cleanup(func() { db.Close() })
|
||||
return db
|
||||
}
|
||||
```
|
||||
|
||||
## Coverage Commands
|
||||
|
||||
```bash
|
||||
# Basic coverage
|
||||
go test -cover ./...
|
||||
|
||||
# Coverage profile
|
||||
go test -coverprofile=coverage.out ./...
|
||||
|
||||
# View in browser
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
# Coverage by function
|
||||
go tool cover -func=coverage.out
|
||||
|
||||
# With race detection
|
||||
go test -race -cover ./...
|
||||
```
|
||||
|
||||
## Coverage Targets
|
||||
|
||||
| Code Type | Target |
|
||||
|-----------|--------|
|
||||
| Critical business logic | 100% |
|
||||
| Public APIs | 90%+ |
|
||||
| General code | 80%+ |
|
||||
| Generated code | Exclude |
|
||||
|
||||
## TDD Best Practices
|
||||
|
||||
**DO:**
|
||||
- Write test FIRST, before any implementation
|
||||
- Run tests after each change
|
||||
- Use table-driven tests for comprehensive coverage
|
||||
- Test behavior, not implementation details
|
||||
- Include edge cases (empty, nil, max values)
|
||||
|
||||
**DON'T:**
|
||||
- Write implementation before tests
|
||||
- Skip the RED phase
|
||||
- Test private functions directly
|
||||
- Use `time.Sleep` in tests
|
||||
- Ignore flaky tests
|
||||
|
||||
## Related Commands
|
||||
|
||||
- `/go-build` - Fix build errors
|
||||
- `/go-review` - Review code after implementation
|
||||
- `verification-loop` skill - Run full verification loop
|
||||
|
||||
## Related
|
||||
|
||||
- Skill: `skills/golang-testing/`
|
||||
- Skill: `skills/tdd-workflow/`
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
description: Fix Gradle build errors for Android and KMP projects
|
||||
---
|
||||
|
||||
# Gradle Build Fix
|
||||
|
||||
Incrementally fix Gradle build and compilation errors for Android and Kotlin Multiplatform projects.
|
||||
|
||||
## Step 1: Detect Build Configuration
|
||||
|
||||
Identify the project type and run the appropriate build:
|
||||
|
||||
| Indicator | Build Command |
|
||||
|-----------|---------------|
|
||||
| `build.gradle.kts` + `composeApp/` (KMP) | `./gradlew composeApp:compileKotlinMetadata 2>&1` |
|
||||
| `build.gradle.kts` + `app/` (Android) | `./gradlew app:compileDebugKotlin 2>&1` |
|
||||
| `settings.gradle.kts` with modules | `./gradlew assemble 2>&1` |
|
||||
| Detekt configured | `./gradlew detekt 2>&1` |
|
||||
|
||||
Also check `gradle.properties` and `local.properties` for configuration.
|
||||
|
||||
## Step 2: Parse and Group Errors
|
||||
|
||||
1. Run the build command and capture output
|
||||
2. Separate Kotlin compilation errors from Gradle configuration errors
|
||||
3. Group by module and file path
|
||||
4. Sort: configuration errors first, then compilation errors by dependency order
|
||||
|
||||
## Step 3: Fix Loop
|
||||
|
||||
For each error:
|
||||
|
||||
1. **Read the file** — Full context around the error line
|
||||
2. **Diagnose** — Common categories:
|
||||
- Missing import or unresolved reference
|
||||
- Type mismatch or incompatible types
|
||||
- Missing dependency in `build.gradle.kts`
|
||||
- Expect/actual mismatch (KMP)
|
||||
- Compose compiler error
|
||||
3. **Fix minimally** — Smallest change that resolves the error
|
||||
4. **Re-run build** — Verify fix and check for new errors
|
||||
5. **Continue** — Move to next error
|
||||
|
||||
## Step 4: Guardrails
|
||||
|
||||
Stop and ask the user if:
|
||||
- Fix introduces more errors than it resolves
|
||||
- Same error persists after 3 attempts
|
||||
- Error requires adding new dependencies or changing module structure
|
||||
- Gradle sync itself fails (configuration-phase error)
|
||||
- Error is in generated code (Room, SQLDelight, KSP)
|
||||
|
||||
## Step 5: Summary
|
||||
|
||||
Report:
|
||||
- Errors fixed (module, file, description)
|
||||
- Errors remaining
|
||||
- New errors introduced (should be zero)
|
||||
- Suggested next steps
|
||||
|
||||
## Common Gradle/KMP Fixes
|
||||
|
||||
| Error | Fix |
|
||||
|-------|-----|
|
||||
| Unresolved reference in `commonMain` | Check if the dependency is in `commonMain.dependencies {}` |
|
||||
| Expect declaration without actual | Add `actual` implementation in each platform source set |
|
||||
| Compose compiler version mismatch | Align Kotlin and Compose compiler versions in `libs.versions.toml` |
|
||||
| Duplicate class | Check for conflicting dependencies with `./gradlew dependencies` |
|
||||
| KSP error | Run `./gradlew kspCommonMainKotlinMetadata` to regenerate |
|
||||
| Configuration cache issue | Check for non-serializable task inputs |
|
||||
@@ -0,0 +1,312 @@
|
||||
---
|
||||
description: Enforce TDD workflow for Kotlin. Write Kotest tests first, then implement. Verify 80%+ coverage with Kover.
|
||||
---
|
||||
|
||||
# Kotlin TDD Command
|
||||
|
||||
This command enforces test-driven development methodology for Kotlin code using Kotest, MockK, and Kover.
|
||||
|
||||
## What This Command Does
|
||||
|
||||
1. **Define Types/Interfaces**: Scaffold function signatures first
|
||||
2. **Write Kotest Tests**: Create comprehensive test specs (RED)
|
||||
3. **Run Tests**: Verify tests fail for the right reason
|
||||
4. **Implement Code**: Write minimal code to pass (GREEN)
|
||||
5. **Refactor**: Improve while keeping tests green
|
||||
6. **Check Coverage**: Ensure 80%+ coverage with Kover
|
||||
|
||||
## When to Use
|
||||
|
||||
Use `/kotlin-test` when:
|
||||
- Implementing new Kotlin functions or classes
|
||||
- Adding test coverage to existing Kotlin code
|
||||
- Fixing bugs (write failing test first)
|
||||
- Building critical business logic
|
||||
- Learning TDD workflow in Kotlin
|
||||
|
||||
## TDD Cycle
|
||||
|
||||
```
|
||||
RED -> Write failing Kotest test
|
||||
GREEN -> Implement minimal code to pass
|
||||
REFACTOR -> Improve code, tests stay green
|
||||
REPEAT -> Next test case
|
||||
```
|
||||
|
||||
## Example Session
|
||||
|
||||
````text
|
||||
User: /kotlin-test I need a function to validate user registration requests
|
||||
|
||||
Agent:
|
||||
# TDD Session: Registration Validator
|
||||
|
||||
## Step 1: Define Interface
|
||||
|
||||
```kotlin
|
||||
// validator/RegistrationValidator.kt
|
||||
package com.example.validator
|
||||
|
||||
data class RegistrationRequest(
|
||||
val name: String,
|
||||
val email: String,
|
||||
val password: String,
|
||||
)
|
||||
|
||||
sealed class ValidationResult {
|
||||
data object Valid : ValidationResult()
|
||||
data class Invalid(val errors: List<String>) : ValidationResult()
|
||||
}
|
||||
|
||||
fun validateRegistration(request: RegistrationRequest): ValidationResult {
|
||||
TODO("not implemented")
|
||||
}
|
||||
```
|
||||
|
||||
## Step 2: Write Kotest Tests (RED)
|
||||
|
||||
```kotlin
|
||||
// validator/RegistrationValidatorTest.kt
|
||||
package com.example.validator
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.types.shouldBeInstanceOf
|
||||
|
||||
class RegistrationValidatorTest : FunSpec({
|
||||
test("valid registration returns Valid") {
|
||||
val request = RegistrationRequest(
|
||||
name = "Alice",
|
||||
email = "alice@example.com",
|
||||
password = "SecureP@ss1",
|
||||
)
|
||||
|
||||
val result = validateRegistration(request)
|
||||
|
||||
result.shouldBeInstanceOf<ValidationResult.Valid>()
|
||||
}
|
||||
|
||||
test("blank name returns Invalid") {
|
||||
val request = RegistrationRequest(
|
||||
name = "",
|
||||
email = "alice@example.com",
|
||||
password = "SecureP@ss1",
|
||||
)
|
||||
|
||||
val result = validateRegistration(request)
|
||||
|
||||
val invalid = result.shouldBeInstanceOf<ValidationResult.Invalid>()
|
||||
invalid.errors shouldBe listOf("Name is required")
|
||||
}
|
||||
|
||||
test("invalid email returns Invalid") {
|
||||
val request = RegistrationRequest(
|
||||
name = "Alice",
|
||||
email = "not-an-email",
|
||||
password = "SecureP@ss1",
|
||||
)
|
||||
|
||||
val result = validateRegistration(request)
|
||||
|
||||
val invalid = result.shouldBeInstanceOf<ValidationResult.Invalid>()
|
||||
invalid.errors shouldBe listOf("Invalid email format")
|
||||
}
|
||||
|
||||
test("short password returns Invalid") {
|
||||
val request = RegistrationRequest(
|
||||
name = "Alice",
|
||||
email = "alice@example.com",
|
||||
password = "short",
|
||||
)
|
||||
|
||||
val result = validateRegistration(request)
|
||||
|
||||
val invalid = result.shouldBeInstanceOf<ValidationResult.Invalid>()
|
||||
invalid.errors shouldBe listOf("Password must be at least 8 characters")
|
||||
}
|
||||
|
||||
test("multiple errors returns all errors") {
|
||||
val request = RegistrationRequest(
|
||||
name = "",
|
||||
email = "bad",
|
||||
password = "short",
|
||||
)
|
||||
|
||||
val result = validateRegistration(request)
|
||||
|
||||
val invalid = result.shouldBeInstanceOf<ValidationResult.Invalid>()
|
||||
invalid.errors.size shouldBe 3
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Step 3: Run Tests - Verify FAIL
|
||||
|
||||
```bash
|
||||
$ ./gradlew test
|
||||
|
||||
RegistrationValidatorTest > valid registration returns Valid FAILED
|
||||
kotlin.NotImplementedError: An operation is not implemented
|
||||
|
||||
FAILED (5 tests, 0 passed, 5 failed)
|
||||
```
|
||||
|
||||
✓ Tests fail as expected (NotImplementedError).
|
||||
|
||||
## Step 4: Implement Minimal Code (GREEN)
|
||||
|
||||
```kotlin
|
||||
// validator/RegistrationValidator.kt
|
||||
package com.example.validator
|
||||
|
||||
private val EMAIL_REGEX = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
|
||||
private const val MIN_PASSWORD_LENGTH = 8
|
||||
|
||||
fun validateRegistration(request: RegistrationRequest): ValidationResult {
|
||||
val errors = buildList {
|
||||
if (request.name.isBlank()) add("Name is required")
|
||||
if (!EMAIL_REGEX.matches(request.email)) add("Invalid email format")
|
||||
if (request.password.length < MIN_PASSWORD_LENGTH) add("Password must be at least $MIN_PASSWORD_LENGTH characters")
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) ValidationResult.Valid
|
||||
else ValidationResult.Invalid(errors)
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5: Run Tests - Verify PASS
|
||||
|
||||
```bash
|
||||
$ ./gradlew test
|
||||
|
||||
RegistrationValidatorTest > valid registration returns Valid PASSED
|
||||
RegistrationValidatorTest > blank name returns Invalid PASSED
|
||||
RegistrationValidatorTest > invalid email returns Invalid PASSED
|
||||
RegistrationValidatorTest > short password returns Invalid PASSED
|
||||
RegistrationValidatorTest > multiple errors returns all errors PASSED
|
||||
|
||||
PASSED (5 tests, 5 passed, 0 failed)
|
||||
```
|
||||
|
||||
✓ All tests passing!
|
||||
|
||||
## Step 6: Check Coverage
|
||||
|
||||
```bash
|
||||
$ ./gradlew koverHtmlReport
|
||||
|
||||
Coverage: 100.0% of statements
|
||||
```
|
||||
|
||||
✓ Coverage: 100%
|
||||
|
||||
## TDD Complete!
|
||||
````
|
||||
|
||||
## Test Patterns
|
||||
|
||||
### StringSpec (Simplest)
|
||||
|
||||
```kotlin
|
||||
class CalculatorTest : StringSpec({
|
||||
"add two positive numbers" {
|
||||
Calculator.add(2, 3) shouldBe 5
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### BehaviorSpec (BDD)
|
||||
|
||||
```kotlin
|
||||
class OrderServiceTest : BehaviorSpec({
|
||||
Given("a valid order") {
|
||||
When("placed") {
|
||||
Then("should be confirmed") { /* ... */ }
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Data-Driven Tests
|
||||
|
||||
```kotlin
|
||||
class ParserTest : FunSpec({
|
||||
context("valid inputs") {
|
||||
withData("2026-01-15", "2026-12-31", "2000-01-01") { input ->
|
||||
parseDate(input).shouldNotBeNull()
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Coroutine Testing
|
||||
|
||||
```kotlin
|
||||
class AsyncServiceTest : FunSpec({
|
||||
test("concurrent fetch completes") {
|
||||
runTest {
|
||||
val result = service.fetchAll()
|
||||
result.shouldNotBeEmpty()
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Coverage Commands
|
||||
|
||||
```bash
|
||||
# Run tests with coverage
|
||||
./gradlew koverHtmlReport
|
||||
|
||||
# Verify coverage thresholds
|
||||
./gradlew koverVerify
|
||||
|
||||
# XML report for CI
|
||||
./gradlew koverXmlReport
|
||||
|
||||
# Open HTML report
|
||||
open build/reports/kover/html/index.html
|
||||
|
||||
# Run specific test class
|
||||
./gradlew test --tests "com.example.UserServiceTest"
|
||||
|
||||
# Run with verbose output
|
||||
./gradlew test --info
|
||||
```
|
||||
|
||||
## Coverage Targets
|
||||
|
||||
| Code Type | Target |
|
||||
|-----------|--------|
|
||||
| Critical business logic | 100% |
|
||||
| Public APIs | 90%+ |
|
||||
| General code | 80%+ |
|
||||
| Generated code | Exclude |
|
||||
|
||||
## TDD Best Practices
|
||||
|
||||
**DO:**
|
||||
- Write test FIRST, before any implementation
|
||||
- Run tests after each change
|
||||
- Use Kotest matchers for expressive assertions
|
||||
- Use MockK's `coEvery`/`coVerify` for suspend functions
|
||||
- Test behavior, not implementation details
|
||||
- Include edge cases (empty, null, max values)
|
||||
|
||||
**DON'T:**
|
||||
- Write implementation before tests
|
||||
- Skip the RED phase
|
||||
- Test private functions directly
|
||||
- Use `Thread.sleep()` in coroutine tests
|
||||
- Ignore flaky tests
|
||||
|
||||
## Related Commands
|
||||
|
||||
- `/kotlin-build` - Fix build errors
|
||||
- `/kotlin-review` - Review code after implementation
|
||||
- `verification-loop` skill - Run full verification loop
|
||||
|
||||
## Related
|
||||
|
||||
- Skill: `skills/kotlin-testing/`
|
||||
- Skill: `skills/tdd-workflow/`
|
||||
@@ -0,0 +1,162 @@
|
||||
---
|
||||
description: "Generate a lean, problem-first PRD and hand off to /plan for implementation planning."
|
||||
argument-hint: "[product/feature idea] (blank = start with questions)"
|
||||
---
|
||||
|
||||
# PRD Command
|
||||
|
||||
Produces a **Product Requirements Document** — the requirements-phase artifact of the SDLC. Captures *what* must be true for success and *why*, and stops before *how*. Implementation decomposition is delegated to `/plan`.
|
||||
|
||||
**Input**: `$ARGUMENTS`
|
||||
|
||||
## Scope of this command
|
||||
|
||||
| This command does | This command does NOT do |
|
||||
|---|---|
|
||||
| Frame the problem and users | Design the architecture |
|
||||
| Capture success criteria and scope | Pick files or write patterns |
|
||||
| List open questions and risks | Enumerate implementation tasks |
|
||||
| Write `.claude/prds/{name}.prd.md` | Produce an implementation plan — that's `/plan` |
|
||||
|
||||
If you find yourself writing implementation detail, stop and cut it. It belongs in `/plan`.
|
||||
|
||||
**Anti-fluff rule**: When information is missing, write `TBD — needs validation via {method}`. Never invent plausible-sounding requirements.
|
||||
|
||||
## Workflow
|
||||
|
||||
Four phases. Each phase is a single gate — ask the questions, wait for the user, then move on. No nested loops, no parallel research ceremony.
|
||||
|
||||
### Phase 1 — FRAME
|
||||
|
||||
If `$ARGUMENTS` is empty, ask:
|
||||
|
||||
> What do you want to build? One or two sentences.
|
||||
|
||||
If provided, restate in one sentence and ask:
|
||||
|
||||
> I understand: *{restated}*. Correct, or should I adjust?
|
||||
|
||||
Then ask the framing questions in a single set:
|
||||
|
||||
> 1. **Who** has this problem? (specific role or segment)
|
||||
> 2. **What** is the observable pain? (describe behavior, not assumed needs)
|
||||
> 3. **Why** can't they solve it with what exists today?
|
||||
> 4. **Why now?** — what changed that makes this worth doing?
|
||||
|
||||
Wait for the user. Do not proceed without answers (or explicit "skip").
|
||||
|
||||
### Phase 2 — GROUND
|
||||
|
||||
Ask for evidence. This is the shortest phase and the most load-bearing:
|
||||
|
||||
> What evidence do you have that this problem is real and worth solving? (user quotes, support tickets, metrics, observed behavior, failed workarounds — anything concrete)
|
||||
|
||||
If the user has none, record the PRD's Evidence section as `Assumption — needs validation via {user research | analytics | prototype}`. This keeps the PRD honest.
|
||||
|
||||
### Phase 3 — DECIDE
|
||||
|
||||
Scope and hypothesis in a single set:
|
||||
|
||||
> 1. **Hypothesis** — Complete: *We believe **{capability}** will **{solve problem}** for **{users}**. We'll know we're right when **{measurable outcome}**.*
|
||||
> 2. **MVP** — The minimum needed to test the hypothesis?
|
||||
> 3. **Out of scope** — What are you explicitly **not** building (even if users ask)?
|
||||
> 4. **Open questions** — Uncertainties that could change the approach?
|
||||
|
||||
Wait for responses.
|
||||
|
||||
### Phase 4 — GENERATE & HAND OFF
|
||||
|
||||
Create the directory if needed, write the PRD, and report.
|
||||
|
||||
```bash
|
||||
mkdir -p .claude/prds
|
||||
```
|
||||
|
||||
**Output path**: `.claude/prds/{kebab-case-name}.prd.md`
|
||||
|
||||
#### PRD Template
|
||||
|
||||
```markdown
|
||||
# {Product / Feature Name}
|
||||
|
||||
## Problem
|
||||
{2–3 sentences: who has what problem, and what's the cost of leaving it unsolved?}
|
||||
|
||||
## Evidence
|
||||
- {User quote, data point, or observation}
|
||||
- {OR: "Assumption — needs validation via {method}"}
|
||||
|
||||
## Users
|
||||
- **Primary**: {role, context, what triggers the need}
|
||||
- **Not for**: {who this explicitly excludes}
|
||||
|
||||
## Hypothesis
|
||||
We believe **{capability}** will **{solve problem}** for **{users}**.
|
||||
We'll know we're right when **{measurable outcome}**.
|
||||
|
||||
## Success Metrics
|
||||
| Metric | Target | How measured |
|
||||
|---|---|---|
|
||||
| {primary} | {number} | {method} |
|
||||
|
||||
## Scope
|
||||
**MVP** — {the minimum to test the hypothesis}
|
||||
|
||||
**Out of scope**
|
||||
- {item} — {why deferred}
|
||||
|
||||
## Delivery Milestones
|
||||
<!-- Business outcomes, not engineering tasks. /plan turns each into a plan. -->
|
||||
<!-- Status: pending | in-progress | complete -->
|
||||
|
||||
| # | Milestone | Outcome | Status | Plan |
|
||||
|---|---|---|---|---|
|
||||
| 1 | {name} | {user-visible change} | pending | — |
|
||||
| 2 | {name} | {user-visible change} | pending | — |
|
||||
|
||||
## Open Questions
|
||||
- [ ] {question that could change scope or approach}
|
||||
|
||||
## Risks
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
|
||||
---
|
||||
*Status: DRAFT — requirements only. Implementation planning pending via /plan.*
|
||||
```
|
||||
|
||||
#### Report to user
|
||||
|
||||
```
|
||||
PRD created: .claude/prds/{name}.prd.md
|
||||
|
||||
Problem: {one line}
|
||||
Hypothesis: {one line}
|
||||
MVP: {one line}
|
||||
|
||||
Validation status:
|
||||
Problem {validated | assumption}
|
||||
Users {concrete | generic — refine}
|
||||
Metrics {defined | TBD}
|
||||
|
||||
Open questions: {count}
|
||||
|
||||
Next step: /plan .claude/prds/{name}.prd.md
|
||||
→ /plan will pick the next pending milestone and produce an implementation plan.
|
||||
```
|
||||
|
||||
## Integration
|
||||
|
||||
- `/plan <prd-path>` — consume the PRD and produce an implementation plan for the next pending milestone.
|
||||
- `tdd-workflow` skill — implement the plan test-first.
|
||||
- `/pr` — open a PR that references the PRD and plan.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- **PROBLEM_CLEAR**: problem is specific and evidenced (or flagged as assumption).
|
||||
- **USER_CONCRETE**: primary user is a specific role, not "users".
|
||||
- **HYPOTHESIS_TESTABLE**: measurable outcome included.
|
||||
- **SCOPE_BOUNDED**: explicit MVP and explicit out-of-scope.
|
||||
- **NO_IMPLEMENTATION_DETAIL**: file paths, libraries, or task breakdowns are absent — if they appeared, move them to the `/plan` step.
|
||||
|
||||
Background on the staged markdown flow: [docs/PLAN-PRD-PATTERN.md](../docs/PLAN-PRD-PATTERN.md).
|
||||
@@ -0,0 +1,206 @@
|
||||
---
|
||||
description: Restate requirements, assess risks, and create step-by-step implementation plan. WAIT for user CONFIRM before touching any code. Use for a single-model inline or PRD-driven implementation plan; for a dual-model (Codex/Antigravity) plan use /multi-plan, and for visual annotate-and-approve review of the resulting plan use /plan-canvas.
|
||||
argument-hint: "[feature description | path/to/*.prd.md]"
|
||||
---
|
||||
|
||||
# Plan Command
|
||||
|
||||
This command creates a comprehensive implementation plan before writing any code. It accepts either free-form requirements or a PRD markdown file.
|
||||
|
||||
Run inline by default. Do not call the Task tool or any subagent by default. This keeps `/plan` usable from plugin installs that ship commands without agent files.
|
||||
|
||||
## What This Command Does
|
||||
|
||||
1. **Restate Requirements** - Clarify what needs to be built
|
||||
2. **Identify Risks** - Surface potential issues and blockers
|
||||
3. **Create Step Plan** - Break down implementation into phases
|
||||
4. **Wait for Confirmation** - MUST receive user approval before proceeding
|
||||
|
||||
## When to Use
|
||||
|
||||
Use `/plan` when:
|
||||
- Starting a new feature
|
||||
- Making significant architectural changes
|
||||
- Working on complex refactoring
|
||||
- Multiple files/components will be affected
|
||||
- Requirements are unclear or ambiguous
|
||||
|
||||
## How It Works
|
||||
|
||||
The assistant will:
|
||||
|
||||
1. **Analyze the request** and restate requirements in clear terms
|
||||
2. **Ground the plan** in relevant codebase patterns when the repo is available
|
||||
3. **Break down into phases** with specific, actionable steps
|
||||
4. **Identify dependencies** between components
|
||||
5. **Assess risks** and potential blockers
|
||||
6. **Estimate complexity** (High/Medium/Low)
|
||||
7. **Present the plan** and WAIT for your explicit confirmation
|
||||
|
||||
## Input Modes
|
||||
|
||||
| Input | Mode | Behavior |
|
||||
|---|---|---|
|
||||
| `path/to/name.prd.md` | PRD artifact mode | Read the PRD, pick the next pending delivery milestone or implementation phase, and write `.claude/plans/{name}.plan.md` |
|
||||
| Any other markdown path | Reference mode | Read the file as context and produce an inline plan |
|
||||
| Free-form text | Conversational mode | Produce an inline plan |
|
||||
| Empty input | Clarification mode | Ask what should be planned |
|
||||
|
||||
In PRD artifact mode, create `.claude/plans/` if needed. If the PRD contains a `Delivery Milestones` table, update only the selected row from `pending` to `in-progress` and set its `Plan` cell to the generated plan path. If the PRD uses the legacy `.claude/PRPs/prds/` format with `Implementation Phases`, read it without migrating paths.
|
||||
|
||||
## Pattern Grounding
|
||||
|
||||
Before writing the plan, search the codebase for conventions the implementation should mirror. Capture the top example for each relevant category with file references:
|
||||
|
||||
| Category | What to capture |
|
||||
|---|---|
|
||||
| Naming | File, function, type, command, or script naming in the affected area |
|
||||
| Error handling | How failures are raised, returned, logged, or handled gracefully |
|
||||
| Logging | Levels, format, and what gets logged |
|
||||
| Data access | Repository, service, query, or filesystem patterns |
|
||||
| Tests | Test file location, framework, fixtures, and assertion style |
|
||||
|
||||
If no similar code exists, state that explicitly. Do not invent a pattern.
|
||||
|
||||
## PRD Artifact Output
|
||||
|
||||
When called with a `.prd.md` file, write the plan to `.claude/plans/{kebab-case-name}.plan.md` using this structure:
|
||||
|
||||
````markdown
|
||||
# Plan: {Feature Name}
|
||||
|
||||
**Source PRD**: {path}
|
||||
**Selected Milestone**: {milestone or phase name}
|
||||
**Complexity**: {Small | Medium | Large}
|
||||
|
||||
## Summary
|
||||
{2-3 sentences}
|
||||
|
||||
## Patterns to Mirror
|
||||
| Category | Source | Pattern |
|
||||
|---|---|---|
|
||||
| Naming | `path:line` | {short description} |
|
||||
| Errors | `path:line` | {short description} |
|
||||
| Tests | `path:line` | {short description} |
|
||||
|
||||
## Files to Change
|
||||
| File | Action | Why |
|
||||
|---|---|---|
|
||||
| `path` | CREATE / UPDATE / DELETE | {reason} |
|
||||
|
||||
## Tasks
|
||||
### Task 1: {name}
|
||||
- **Action**: {what to do}
|
||||
- **Mirror**: {pattern to follow}
|
||||
- **Validate**: {command that proves correctness}
|
||||
|
||||
## Validation
|
||||
```bash
|
||||
{project-specific validation commands}
|
||||
```
|
||||
|
||||
## Risks
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|---|---|---|
|
||||
|
||||
## Acceptance
|
||||
- [ ] All tasks complete
|
||||
- [ ] Validation passes
|
||||
- [ ] Patterns mirrored, not reinvented
|
||||
````
|
||||
|
||||
After writing the artifact, report its path and WAIT for confirmation before writing code.
|
||||
|
||||
> **Visual review:** instead of asking for a typed confirmation, you can open the
|
||||
> artifact in the browser Plan Canvas (`/plan-canvas`, or the `plan-canvas` skill):
|
||||
> the user annotates the plan in place and clicks **Approve plan** or **Request
|
||||
> changes**, which arrives as your confirmation signal.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
User: /plan I need to add real-time notifications when markets resolve
|
||||
|
||||
Assistant:
|
||||
# Implementation Plan: Real-Time Market Resolution Notifications
|
||||
|
||||
## Requirements Restatement
|
||||
- Send notifications to users when markets they're watching resolve
|
||||
- Support multiple notification channels (in-app, email, webhook)
|
||||
- Ensure notifications are delivered reliably
|
||||
- Include market outcome and user's position result
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Database Schema
|
||||
- Add notifications table with columns: id, user_id, market_id, type, status, created_at
|
||||
- Add user_notification_preferences table for channel preferences
|
||||
- Create indexes on user_id and market_id for performance
|
||||
|
||||
### Phase 2: Notification Service
|
||||
- Create notification service in lib/notifications.ts
|
||||
- Implement notification queue using BullMQ/Redis
|
||||
- Add retry logic for failed deliveries
|
||||
- Create notification templates
|
||||
|
||||
### Phase 3: Integration Points
|
||||
- Hook into market resolution logic (when status changes to "resolved")
|
||||
- Query all users with positions in market
|
||||
- Enqueue notifications for each user
|
||||
|
||||
### Phase 4: Frontend Components
|
||||
- Create NotificationBell component in header
|
||||
- Add NotificationList modal
|
||||
- Implement real-time updates via Supabase subscriptions
|
||||
- Add notification preferences page
|
||||
|
||||
## Dependencies
|
||||
- Redis (for queue)
|
||||
- Email service (SendGrid/Resend)
|
||||
- Supabase real-time subscriptions
|
||||
|
||||
## Risks
|
||||
- HIGH: Email deliverability (SPF/DKIM required)
|
||||
- MEDIUM: Performance with 1000+ users per market
|
||||
- MEDIUM: Notification spam if markets resolve frequently
|
||||
- LOW: Real-time subscription overhead
|
||||
|
||||
## Estimated Complexity: MEDIUM
|
||||
- Backend: 4-6 hours
|
||||
- Frontend: 3-4 hours
|
||||
- Testing: 2-3 hours
|
||||
- Total: 9-13 hours
|
||||
|
||||
**WAITING FOR CONFIRMATION**: Proceed with this plan? (yes/no/modify)
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
**CRITICAL**: This command will **NOT** write any code until you explicitly confirm the plan with "yes" or "proceed" or similar affirmative response.
|
||||
|
||||
If you want changes, respond with:
|
||||
- "modify: [your changes]"
|
||||
- "different approach: [alternative]"
|
||||
- "skip phase 2 and do phase 3 first"
|
||||
|
||||
## Integration with Other Commands
|
||||
|
||||
After planning:
|
||||
- Use `/plan-canvas` to run the confirmation gate visually in the browser (annotate + approve)
|
||||
- Use the `tdd-workflow` skill to implement with test-driven development
|
||||
- Use `/build-fix` if build errors occur
|
||||
- Use `/code-review` to review completed implementation
|
||||
- Use `/pr` or `/prp-pr` to open a pull request
|
||||
|
||||
> **Need requirements first?** Use `/plan-prd` for a lean PRD at `.claude/prds/{name}.prd.md`.
|
||||
>
|
||||
> **Need the legacy PRP flow?** Use `/prp-plan` for deep PRP planning with `.claude/PRPs/` artifacts. Use `/prp-implement` to execute those plans with rigorous validation loops.
|
||||
|
||||
## Optional Planner Agent
|
||||
|
||||
ECC also provides a `planner` agent for manual installs that include agent files. Use it only when the local runtime already exposes that subagent and the user explicitly asks you to delegate planning.
|
||||
|
||||
If the `planner` subagent is unavailable, continue planning inline instead of surfacing an "Agent type 'planner' not found" error.
|
||||
|
||||
For manual installs, the source file lives at:
|
||||
`agents/planner.md`
|
||||
@@ -0,0 +1,184 @@
|
||||
---
|
||||
description: "Create a GitHub PR from current branch with unpushed commits — discovers templates, analyzes changes, pushes"
|
||||
argument-hint: "[base-branch] (default: main)"
|
||||
---
|
||||
|
||||
# Create Pull Request
|
||||
|
||||
**Input**: `$ARGUMENTS` — optional, may contain a base branch name and/or flags (e.g., `--draft`).
|
||||
|
||||
**Parse `$ARGUMENTS`**:
|
||||
- Extract any recognized flags (`--draft`)
|
||||
- Treat remaining non-flag text as the base branch name
|
||||
- Default base branch to `main` if none specified
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — VALIDATE
|
||||
|
||||
Check preconditions:
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
git status --short
|
||||
git log origin/<base>..HEAD --oneline
|
||||
```
|
||||
|
||||
| Check | Condition | Action if Failed |
|
||||
|---|---|---|
|
||||
| Not on base branch | Current branch ≠ base | Stop: "Switch to a feature branch first." |
|
||||
| Clean working directory | No uncommitted changes | Warn: "You have uncommitted changes. Commit or stash first." |
|
||||
| Has commits ahead | `git log origin/<base>..HEAD` not empty | Stop: "No commits ahead of `<base>`. Nothing to PR." |
|
||||
| No existing PR | `gh pr list --head <branch> --json number` is empty | Stop: "PR already exists: #<number>. Use `gh pr view <number> --web` to open it." |
|
||||
|
||||
If all checks pass, proceed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — DISCOVER
|
||||
|
||||
### PR Template
|
||||
|
||||
Search for PR template in order:
|
||||
|
||||
1. `.github/PULL_REQUEST_TEMPLATE/` directory — if exists, list files and let user choose (or use `default.md`)
|
||||
2. `.github/PULL_REQUEST_TEMPLATE.md`
|
||||
3. `.github/pull_request_template.md`
|
||||
4. `docs/pull_request_template.md`
|
||||
|
||||
If found, read it and use its structure for the PR body.
|
||||
|
||||
### Commit Analysis
|
||||
|
||||
```bash
|
||||
git log origin/<base>..HEAD --format="%h %s" --reverse
|
||||
```
|
||||
|
||||
Analyze commits to determine:
|
||||
- **PR title**: Use conventional commit format with type prefix — `feat: ...`, `fix: ...`, etc.
|
||||
- If multiple types, use the dominant one
|
||||
- If single commit, use its message as-is
|
||||
- **Change summary**: Group commits by type/area
|
||||
|
||||
### File Analysis
|
||||
|
||||
```bash
|
||||
git diff origin/<base>..HEAD --stat
|
||||
git diff origin/<base>..HEAD --name-only
|
||||
```
|
||||
|
||||
Categorize changed files: source, tests, docs, config, migrations.
|
||||
|
||||
### Planning Artifacts
|
||||
|
||||
Check for related artifacts produced by `/plan-prd`, `/plan`, or the legacy PRP workflow:
|
||||
- `.claude/prds/` — PRDs this PR implements a milestone of
|
||||
- `.claude/plans/` — Plans executed by this PR
|
||||
- `.claude/PRPs/prds/` — legacy PRP PRDs
|
||||
- `.claude/PRPs/plans/` — legacy PRP implementation plans
|
||||
- `.claude/PRPs/reports/` — legacy PRP implementation reports
|
||||
|
||||
Reference these in the PR body if they exist.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — PUSH
|
||||
|
||||
```bash
|
||||
git push -u origin HEAD
|
||||
```
|
||||
|
||||
If push fails due to divergence:
|
||||
```bash
|
||||
git fetch origin
|
||||
git rebase origin/<base>
|
||||
git push -u origin HEAD
|
||||
```
|
||||
|
||||
If rebase conflicts occur, stop and inform the user.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — CREATE
|
||||
|
||||
### With Template
|
||||
|
||||
If a PR template was found in Phase 2, fill in each section using the commit and file analysis. Preserve all template sections — leave sections as "N/A" if not applicable rather than removing them.
|
||||
|
||||
### Without Template
|
||||
|
||||
Use this default format:
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
|
||||
<1-2 sentence description of what this PR does and why>
|
||||
|
||||
## Changes
|
||||
|
||||
<bulleted list of changes grouped by area>
|
||||
|
||||
## Files Changed
|
||||
|
||||
<table or list of changed files with change type: Added/Modified/Deleted>
|
||||
|
||||
## Testing
|
||||
|
||||
<description of how changes were tested, or "Needs testing">
|
||||
|
||||
## Related Issues
|
||||
|
||||
<linked issues with Closes/Fixes/Relates to #N, or "None">
|
||||
```
|
||||
|
||||
### Create the PR
|
||||
|
||||
```bash
|
||||
gh pr create \
|
||||
--title "<PR title>" \
|
||||
--base <base-branch> \
|
||||
--body "<PR body>"
|
||||
# Add --draft if the --draft flag was parsed from $ARGUMENTS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — VERIFY
|
||||
|
||||
```bash
|
||||
gh pr view --json number,url,title,state,baseRefName,headRefName,additions,deletions,changedFiles
|
||||
gh pr checks --json name,status,conclusion 2>/dev/null || true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — OUTPUT
|
||||
|
||||
Report to user:
|
||||
|
||||
```
|
||||
PR #<number>: <title>
|
||||
URL: <url>
|
||||
Branch: <head> → <base>
|
||||
Changes: +<additions> -<deletions> across <changedFiles> files
|
||||
|
||||
CI Checks: <status summary or "pending" or "none configured">
|
||||
|
||||
Artifacts referenced:
|
||||
- <any PRDs/plans linked in PR body>
|
||||
|
||||
Next steps:
|
||||
- gh pr view <number> --web → open in browser
|
||||
- /code-review <number> → review the PR
|
||||
- gh pr merge <number> → merge when ready
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- **No `gh` CLI**: Stop with: "GitHub CLI (`gh`) is required. Install: <https://cli.github.com/>"
|
||||
- **Not authenticated**: Stop with: "Run `gh auth login` first."
|
||||
- **Force push needed**: If remote has diverged and rebase was done, use `git push --force-with-lease` (never `--force`).
|
||||
- **Multiple PR templates**: If `.github/PULL_REQUEST_TEMPLATE/` has multiple files, list them and ask user to choose.
|
||||
- **Large PR (>20 files)**: Warn about PR size. Suggest splitting if changes are logically separable.
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
description: "Quick commit with natural language file targeting — describe what to commit in plain English"
|
||||
argument-hint: "[target description] (blank = all changes)"
|
||||
---
|
||||
|
||||
# Smart Commit
|
||||
|
||||
> Adapted from PRPs-agentic-eng by Wirasm. Part of the PRP workflow series.
|
||||
|
||||
**Input**: $ARGUMENTS
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — ASSESS
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
```
|
||||
|
||||
If output is empty → stop: "Nothing to commit."
|
||||
|
||||
Show the user a summary of what's changed (added, modified, deleted, untracked).
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — INTERPRET & STAGE
|
||||
|
||||
Interpret `$ARGUMENTS` to determine what to stage:
|
||||
|
||||
| Input | Interpretation | Git Command |
|
||||
|---|---|---|
|
||||
| *(blank / empty)* | Stage everything | `git add -A` |
|
||||
| `staged` | Use whatever is already staged | *(no git add)* |
|
||||
| `*.ts` or `*.py` etc. | Stage matching glob | `git add '*.ts'` |
|
||||
| `except tests` | Stage all, then unstage tests | `git add -A && git reset -- '**/*.test.*' '**/*.spec.*' '**/test_*' 2>/dev/null \|\| true` |
|
||||
| `only new files` | Stage untracked files only | `git ls-files --others --exclude-standard \| grep . && git ls-files --others --exclude-standard \| xargs git add` |
|
||||
| `the auth changes` | Interpret from status/diff — find auth-related files | `git add <matched files>` |
|
||||
| Specific filenames | Stage those files | `git add <files>` |
|
||||
|
||||
For natural language inputs (like "the auth changes"), cross-reference the `git status` output and `git diff` to identify relevant files. Show the user which files you're staging and why.
|
||||
|
||||
```bash
|
||||
git add <determined files>
|
||||
```
|
||||
|
||||
After staging, verify:
|
||||
```bash
|
||||
git diff --cached --stat
|
||||
```
|
||||
|
||||
If nothing staged, stop: "No files matched your description."
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — COMMIT
|
||||
|
||||
Craft a single-line commit message in imperative mood:
|
||||
|
||||
```
|
||||
{type}: {description}
|
||||
```
|
||||
|
||||
Types:
|
||||
- `feat` — New feature or capability
|
||||
- `fix` — Bug fix
|
||||
- `refactor` — Code restructuring without behavior change
|
||||
- `docs` — Documentation changes
|
||||
- `test` — Adding or updating tests
|
||||
- `chore` — Build, config, dependencies
|
||||
- `perf` — Performance improvement
|
||||
- `ci` — CI/CD changes
|
||||
|
||||
Rules:
|
||||
- Imperative mood ("add feature" not "added feature")
|
||||
- Lowercase after the type prefix
|
||||
- No period at the end
|
||||
- Under 72 characters
|
||||
- Describe WHAT changed, not HOW
|
||||
|
||||
```bash
|
||||
git commit -m "{type}: {description}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — OUTPUT
|
||||
|
||||
Report to user:
|
||||
|
||||
```
|
||||
Committed: {hash_short}
|
||||
Message: {type}: {description}
|
||||
Files: {count} file(s) changed
|
||||
|
||||
Next steps:
|
||||
- git push → push to remote
|
||||
- /prp-pr → create a pull request
|
||||
- /code-review → review before pushing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
| You say | What happens |
|
||||
|---|---|
|
||||
| `/prp-commit` | Stages all, auto-generates message |
|
||||
| `/prp-commit staged` | Commits only what's already staged |
|
||||
| `/prp-commit *.ts` | Stages all TypeScript files, commits |
|
||||
| `/prp-commit except tests` | Stages everything except test files |
|
||||
| `/prp-commit the database migration` | Finds DB migration files from status, stages them |
|
||||
| `/prp-commit only new files` | Stages untracked files only |
|
||||
@@ -0,0 +1,385 @@
|
||||
---
|
||||
description: Execute an implementation plan with rigorous validation loops
|
||||
argument-hint: <path/to/plan.md>
|
||||
---
|
||||
|
||||
> Adapted from PRPs-agentic-eng by Wirasm. Part of the PRP workflow series.
|
||||
|
||||
# PRP Implement
|
||||
|
||||
Execute a plan file step-by-step with continuous validation. Every change is verified immediately — never accumulate broken state.
|
||||
|
||||
**Core Philosophy**: Validation loops catch mistakes early. Run checks after every change. Fix issues immediately.
|
||||
|
||||
**Golden Rule**: If a validation fails, fix it before moving on. Never accumulate broken state.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — DETECT
|
||||
|
||||
### Package Manager Detection
|
||||
|
||||
| File Exists | Package Manager | Runner |
|
||||
|---|---|---|
|
||||
| `bun.lockb` | bun | `bun run` |
|
||||
| `pnpm-lock.yaml` | pnpm | `pnpm run` |
|
||||
| `yarn.lock` | yarn | `yarn` |
|
||||
| `package-lock.json` | npm | `npm run` |
|
||||
| `pyproject.toml` or `requirements.txt` | uv / pip | `uv run` or `python -m` |
|
||||
| `Cargo.toml` | cargo | `cargo` |
|
||||
| `go.mod` | go | `go` |
|
||||
|
||||
### Validation Scripts
|
||||
|
||||
Check `package.json` (or equivalent) for available scripts:
|
||||
|
||||
```bash
|
||||
# For Node.js projects
|
||||
cat package.json | grep -A 20 '"scripts"'
|
||||
```
|
||||
|
||||
Note available commands for: type-check, lint, test, build.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — LOAD
|
||||
|
||||
Read the plan file:
|
||||
|
||||
```bash
|
||||
cat "$ARGUMENTS"
|
||||
```
|
||||
|
||||
Extract these sections from the plan:
|
||||
- **Summary** — What is being built
|
||||
- **Patterns to Mirror** — Code conventions to follow
|
||||
- **Files to Change** — What to create or modify
|
||||
- **Step-by-Step Tasks** — Implementation sequence
|
||||
- **Validation Commands** — How to verify correctness
|
||||
- **Acceptance Criteria** — Definition of done
|
||||
|
||||
If the file doesn't exist or isn't a valid plan:
|
||||
```
|
||||
Error: Plan file not found or invalid.
|
||||
Run /prp-plan <feature-description> to create a plan first.
|
||||
```
|
||||
|
||||
**CHECKPOINT**: Plan loaded. All sections identified. Tasks extracted.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — PREPARE
|
||||
|
||||
### Git State
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
git status --porcelain
|
||||
```
|
||||
|
||||
### Branch Decision
|
||||
|
||||
| Current State | Action |
|
||||
|---|---|
|
||||
| On feature branch | Use current branch |
|
||||
| On main, clean working tree | Create feature branch: `git checkout -b feat/{plan-name}` |
|
||||
| On main, dirty working tree | **STOP** — Ask user to stash or commit first |
|
||||
| In a git worktree for this feature | Use the worktree |
|
||||
|
||||
### Sync Remote
|
||||
|
||||
```bash
|
||||
git pull --rebase origin $(git branch --show-current) 2>/dev/null || true
|
||||
```
|
||||
|
||||
**CHECKPOINT**: On correct branch. Working tree ready. Remote synced.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — EXECUTE
|
||||
|
||||
Process each task from the plan sequentially.
|
||||
|
||||
### Per-Task Loop
|
||||
|
||||
For each task in **Step-by-Step Tasks**:
|
||||
|
||||
1. **Read MIRROR reference** — Open the pattern file referenced in the task's MIRROR field. Understand the convention before writing code.
|
||||
|
||||
2. **Implement** — Write the code following the pattern exactly. Apply GOTCHA warnings. Use specified IMPORTS.
|
||||
|
||||
3. **Validate immediately** — After EVERY file change:
|
||||
```bash
|
||||
# Run type-check (adjust command per project)
|
||||
[type-check command from Phase 0]
|
||||
```
|
||||
If type-check fails → fix the error before moving to the next file.
|
||||
|
||||
4. **Track progress** — Log: `[done] Task N: [task name] — complete`
|
||||
|
||||
### Handling Deviations
|
||||
|
||||
If implementation must deviate from the plan:
|
||||
- Note **WHAT** changed
|
||||
- Note **WHY** it changed
|
||||
- Continue with the corrected approach
|
||||
- These deviations will be captured in the report
|
||||
|
||||
**CHECKPOINT**: All tasks executed. Deviations logged.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — VALIDATE
|
||||
|
||||
Run all validation levels from the plan. Fix issues at each level before proceeding.
|
||||
|
||||
### Level 1: Static Analysis
|
||||
|
||||
```bash
|
||||
# Type checking — zero errors required
|
||||
[project type-check command]
|
||||
|
||||
# Linting — fix automatically where possible
|
||||
[project lint command]
|
||||
[project lint-fix command]
|
||||
```
|
||||
|
||||
If lint errors remain after auto-fix, fix manually.
|
||||
|
||||
### Level 2: Unit Tests
|
||||
|
||||
Write tests for every new function (as specified in the plan's Testing Strategy).
|
||||
|
||||
```bash
|
||||
[project test command for affected area]
|
||||
```
|
||||
|
||||
- Every function needs at least one test
|
||||
- Cover edge cases listed in the plan
|
||||
- If a test fails → fix the implementation (not the test, unless the test is wrong)
|
||||
|
||||
### Level 3: Build Check
|
||||
|
||||
```bash
|
||||
[project build command]
|
||||
```
|
||||
|
||||
Build must succeed with zero errors.
|
||||
|
||||
### Level 4: Integration Testing (if applicable)
|
||||
|
||||
```bash
|
||||
# Start server, run tests, stop server
|
||||
[project dev server command] &
|
||||
SERVER_PID=$!
|
||||
|
||||
# Wait for server to be ready (adjust port as needed)
|
||||
SERVER_READY=0
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf http://localhost:PORT/health >/dev/null 2>&1; then
|
||||
SERVER_READY=1
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ "$SERVER_READY" -ne 1 ]; then
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
echo "ERROR: Server failed to start within 30s" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[integration test command]
|
||||
TEST_EXIT=$?
|
||||
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
wait "$SERVER_PID" 2>/dev/null || true
|
||||
|
||||
exit "$TEST_EXIT"
|
||||
```
|
||||
|
||||
### Level 5: Edge Case Testing
|
||||
|
||||
Run through edge cases from the plan's Testing Strategy checklist.
|
||||
|
||||
**CHECKPOINT**: All 5 validation levels pass. Zero errors.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — REPORT
|
||||
|
||||
### Create Implementation Report
|
||||
|
||||
```bash
|
||||
mkdir -p .claude/PRPs/reports
|
||||
```
|
||||
|
||||
Write report to `.claude/PRPs/reports/{plan-name}-report.md`:
|
||||
|
||||
```markdown
|
||||
# Implementation Report: [Feature Name]
|
||||
|
||||
## Summary
|
||||
[What was implemented]
|
||||
|
||||
## Assessment vs Reality
|
||||
|
||||
| Metric | Predicted (Plan) | Actual |
|
||||
|---|---|---|
|
||||
| Complexity | [from plan] | [actual] |
|
||||
| Confidence | [from plan] | [actual] |
|
||||
| Files Changed | [from plan] | [actual count] |
|
||||
|
||||
## Tasks Completed
|
||||
|
||||
| # | Task | Status | Notes |
|
||||
|---|---|---|---|
|
||||
| 1 | [task name] | [done] Complete | |
|
||||
| 2 | [task name] | [done] Complete | Deviated — [reason] |
|
||||
|
||||
## Validation Results
|
||||
|
||||
| Level | Status | Notes |
|
||||
|---|---|---|
|
||||
| Static Analysis | [done] Pass | |
|
||||
| Unit Tests | [done] Pass | N tests written |
|
||||
| Build | [done] Pass | |
|
||||
| Integration | [done] Pass | or N/A |
|
||||
| Edge Cases | [done] Pass | |
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Action | Lines |
|
||||
|---|---|---|
|
||||
| `path/to/file` | CREATED | +N |
|
||||
| `path/to/file` | UPDATED | +N / -M |
|
||||
|
||||
## Deviations from Plan
|
||||
[List any deviations with WHAT and WHY, or "None"]
|
||||
|
||||
## Issues Encountered
|
||||
[List any problems and how they were resolved, or "None"]
|
||||
|
||||
## Tests Written
|
||||
|
||||
| Test File | Tests | Coverage |
|
||||
|---|---|---|
|
||||
| `path/to/test` | N tests | [area covered] |
|
||||
|
||||
## Next Steps
|
||||
- [ ] Code review via `/code-review`
|
||||
- [ ] Create PR via `/prp-pr`
|
||||
```
|
||||
|
||||
### Update PRD (if applicable)
|
||||
|
||||
If this implementation was for a PRD phase:
|
||||
1. Update the phase status from `in-progress` to `complete`
|
||||
2. Add report path as reference
|
||||
|
||||
### Archive Plan
|
||||
|
||||
```bash
|
||||
mkdir -p .claude/PRPs/plans/completed
|
||||
mv "$ARGUMENTS" .claude/PRPs/plans/completed/
|
||||
```
|
||||
|
||||
**CHECKPOINT**: Report created. PRD updated. Plan archived.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — OUTPUT
|
||||
|
||||
Report to user:
|
||||
|
||||
```
|
||||
## Implementation Complete
|
||||
|
||||
- **Plan**: [plan file path] → archived to completed/
|
||||
- **Branch**: [current branch name]
|
||||
- **Status**: [done] All tasks complete
|
||||
|
||||
### Validation Summary
|
||||
|
||||
| Check | Status |
|
||||
|---|---|
|
||||
| Type Check | [done] |
|
||||
| Lint | [done] |
|
||||
| Tests | [done] (N written) |
|
||||
| Build | [done] |
|
||||
| Integration | [done] or N/A |
|
||||
|
||||
### Files Changed
|
||||
- [N] files created, [M] files updated
|
||||
|
||||
### Deviations
|
||||
[Summary or "None — implemented exactly as planned"]
|
||||
|
||||
### Artifacts
|
||||
- Report: `.claude/PRPs/reports/{name}-report.md`
|
||||
- Archived Plan: `.claude/PRPs/plans/completed/{name}.plan.md`
|
||||
|
||||
### PRD Progress (if applicable)
|
||||
| Phase | Status |
|
||||
|---|---|
|
||||
| Phase 1 | [done] Complete |
|
||||
| Phase 2 | [next] |
|
||||
| ... | ... |
|
||||
|
||||
> Next step: Run `/prp-pr` to create a pull request, or `/code-review` to review changes first.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Handling Failures
|
||||
|
||||
### Type Check Fails
|
||||
1. Read the error message carefully
|
||||
2. Fix the type error in the source file
|
||||
3. Re-run type-check
|
||||
4. Continue only when clean
|
||||
|
||||
### Tests Fail
|
||||
1. Identify whether the bug is in the implementation or the test
|
||||
2. Fix the root cause (usually the implementation)
|
||||
3. Re-run tests
|
||||
4. Continue only when green
|
||||
|
||||
### Lint Fails
|
||||
1. Run auto-fix first
|
||||
2. If errors remain, fix manually
|
||||
3. Re-run lint
|
||||
4. Continue only when clean
|
||||
|
||||
### Build Fails
|
||||
1. Usually a type or import issue — check error message
|
||||
2. Fix the offending file
|
||||
3. Re-run build
|
||||
4. Continue only when successful
|
||||
|
||||
### Integration Test Fails
|
||||
1. Check server started correctly
|
||||
2. Verify endpoint/route exists
|
||||
3. Check request format matches expected
|
||||
4. Fix and re-run
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- **TASKS_COMPLETE**: All tasks from the plan executed
|
||||
- **TYPES_PASS**: Zero type errors
|
||||
- **LINT_PASS**: Zero lint errors
|
||||
- **TESTS_PASS**: All tests green, new tests written
|
||||
- **BUILD_PASS**: Build succeeds
|
||||
- **REPORT_CREATED**: Implementation report saved
|
||||
- **PLAN_ARCHIVED**: Plan moved to `completed/`
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Run `/code-review` to review changes before committing
|
||||
- Run `/prp-commit` to commit with a descriptive message
|
||||
- Run `/prp-pr` to create a pull request
|
||||
- Run `/prp-plan <next-phase>` if the PRD has more phases
|
||||
@@ -0,0 +1,502 @@
|
||||
---
|
||||
description: Create comprehensive feature implementation plan with codebase analysis and pattern extraction
|
||||
argument-hint: <feature description | path/to/prd.md>
|
||||
---
|
||||
|
||||
> Adapted from PRPs-agentic-eng by Wirasm. Part of the PRP workflow series.
|
||||
|
||||
# PRP Plan
|
||||
|
||||
Create a detailed, self-contained implementation plan that captures all codebase patterns, conventions, and context needed to implement a feature in a single pass.
|
||||
|
||||
**Core Philosophy**: A great plan contains everything needed to implement without asking further questions. Every pattern, every convention, every gotcha — captured once, referenced throughout.
|
||||
|
||||
**Golden Rule**: If you would need to search the codebase during implementation, capture that knowledge NOW in the plan.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — DETECT
|
||||
|
||||
Determine input type from `$ARGUMENTS`:
|
||||
|
||||
| Input Pattern | Detection | Action |
|
||||
|---|---|---|
|
||||
| Path ending in `.prd.md` | File path to PRD | Parse PRD, find next pending phase |
|
||||
| Path to `.md` with "Implementation Phases" | PRD-like document | Parse phases, find next pending |
|
||||
| Path to any other file | Reference file | Read file for context, treat as free-form |
|
||||
| Free-form text | Feature description | Proceed directly to Phase 1 |
|
||||
| Empty / blank | No input | Ask user what feature to plan |
|
||||
|
||||
### PRD Parsing (when input is a PRD)
|
||||
|
||||
1. Read the PRD file with `cat "$PRD_PATH"`
|
||||
2. Parse the **Implementation Phases** section
|
||||
3. Find phases by status:
|
||||
- Look for `pending` phases
|
||||
- Check dependency chains (a phase may depend on prior phases being `complete`)
|
||||
- Select the **next eligible pending phase**
|
||||
4. Extract from the selected phase:
|
||||
- Phase name and description
|
||||
- Acceptance criteria
|
||||
- Dependencies on prior phases
|
||||
- Any scope notes or constraints
|
||||
5. Use the phase description as the feature to plan
|
||||
|
||||
If no pending phases remain, report that all phases are complete.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — PARSE
|
||||
|
||||
Extract and clarify the feature requirements.
|
||||
|
||||
### Feature Understanding
|
||||
|
||||
From the input (PRD phase or free-form description), identify:
|
||||
|
||||
- **What** is being built (concrete deliverable)
|
||||
- **Why** it matters (user value)
|
||||
- **Who** uses it (target user/system)
|
||||
- **Where** it fits (which part of the codebase)
|
||||
|
||||
### User Story
|
||||
|
||||
Format as:
|
||||
```
|
||||
As a [type of user],
|
||||
I want [capability],
|
||||
So that [benefit].
|
||||
```
|
||||
|
||||
### Complexity Assessment
|
||||
|
||||
| Level | Indicators | Typical Scope |
|
||||
|---|---|---|
|
||||
| **Small** | Single file, isolated change, no new dependencies | 1-3 files, <100 lines |
|
||||
| **Medium** | Multiple files, follows existing patterns, minor new concepts | 3-10 files, 100-500 lines |
|
||||
| **Large** | Cross-cutting concerns, new patterns, external integrations | 10+ files, 500+ lines |
|
||||
| **XL** | Architectural changes, new subsystems, migration needed | 20+ files, consider splitting |
|
||||
|
||||
### Ambiguity Gate
|
||||
|
||||
If any of these are unclear, **STOP and ask the user** before proceeding:
|
||||
|
||||
- The core deliverable is vague
|
||||
- Success criteria are undefined
|
||||
- There are multiple valid interpretations
|
||||
- Technical approach has major unknowns
|
||||
|
||||
Do NOT guess. Ask. A plan built on assumptions fails during implementation.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — EXPLORE
|
||||
|
||||
Gather deep codebase intelligence. Search the codebase directly for each category below.
|
||||
|
||||
### Codebase Search (8 Categories)
|
||||
|
||||
For each category, search using grep, find, and file reading:
|
||||
|
||||
1. **Similar Implementations** — Find existing features that resemble the planned one. Look for analogous patterns, endpoints, components, or modules.
|
||||
|
||||
2. **Naming Conventions** — Identify how files, functions, variables, classes, and exports are named in the relevant area of the codebase.
|
||||
|
||||
3. **Error Handling** — Find how errors are caught, propagated, logged, and returned to users in similar code paths.
|
||||
|
||||
4. **Logging Patterns** — Identify what gets logged, at what level, and in what format.
|
||||
|
||||
5. **Type Definitions** — Find relevant types, interfaces, schemas, and how they're organized.
|
||||
|
||||
6. **Test Patterns** — Find how similar features are tested. Note test file locations, naming, setup/teardown patterns, and assertion styles.
|
||||
|
||||
7. **Configuration** — Find relevant config files, environment variables, and feature flags.
|
||||
|
||||
8. **Dependencies** — Identify packages, imports, and internal modules used by similar features.
|
||||
|
||||
### Codebase Analysis (5 Traces)
|
||||
|
||||
Read relevant files to trace:
|
||||
|
||||
1. **Entry Points** — How does a request/action enter the system and reach the area you're modifying?
|
||||
2. **Data Flow** — How does data move through the relevant code paths?
|
||||
3. **State Changes** — What state is modified and where?
|
||||
4. **Contracts** — What interfaces, APIs, or protocols must be honored?
|
||||
5. **Patterns** — What architectural patterns are used (repository, service, controller, etc.)?
|
||||
|
||||
### Unified Discovery Table
|
||||
|
||||
Compile findings into a single reference:
|
||||
|
||||
| Category | File:Lines | Pattern | Key Snippet |
|
||||
|---|---|---|---|
|
||||
| Naming | `src/services/userService.ts:1-5` | camelCase services, PascalCase types | `export class UserService` |
|
||||
| Error | `src/middleware/errorHandler.ts:10-25` | Custom AppError class | `throw new AppError(...)` |
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — RESEARCH
|
||||
|
||||
If the feature involves external libraries, APIs, or unfamiliar technology:
|
||||
|
||||
1. Search the web for official documentation
|
||||
2. Find usage examples and best practices
|
||||
3. Identify version-specific gotchas
|
||||
|
||||
Format each finding as:
|
||||
|
||||
```
|
||||
KEY_INSIGHT: [what you learned]
|
||||
APPLIES_TO: [which part of the plan this affects]
|
||||
GOTCHA: [any warnings or version-specific issues]
|
||||
```
|
||||
|
||||
If the feature uses only well-understood internal patterns, skip this phase and note: "No external research needed — feature uses established internal patterns."
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — DESIGN
|
||||
|
||||
### UX Transformation (if applicable)
|
||||
|
||||
Document the before/after user experience:
|
||||
|
||||
**Before:**
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ [Current user experience] │
|
||||
│ Show the current flow, │
|
||||
│ what the user sees/does │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
**After:**
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ [New user experience] │
|
||||
│ Show the improved flow, │
|
||||
│ what changes for the user │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
### Interaction Changes
|
||||
|
||||
| Touchpoint | Before | After | Notes |
|
||||
|---|---|---|---|
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
If the feature is purely backend/internal with no UX change, note: "Internal change — no user-facing UX transformation."
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — ARCHITECT
|
||||
|
||||
### Strategic Design
|
||||
|
||||
Define the implementation approach:
|
||||
|
||||
- **Approach**: High-level strategy (e.g., "Add new service layer following existing repository pattern")
|
||||
- **Alternatives Considered**: What other approaches were evaluated and why they were rejected
|
||||
- **Scope**: Concrete boundaries of what WILL be built
|
||||
- **NOT Building**: Explicit list of what is OUT OF SCOPE (prevents scope creep during implementation)
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — GENERATE
|
||||
|
||||
Write the full plan document using the template below. Save to `.claude/PRPs/plans/{kebab-case-feature-name}.plan.md`.
|
||||
|
||||
Create the directory if it doesn't exist:
|
||||
```bash
|
||||
mkdir -p .claude/PRPs/plans
|
||||
```
|
||||
|
||||
### Plan Template
|
||||
|
||||
````markdown
|
||||
# Plan: [Feature Name]
|
||||
|
||||
## Summary
|
||||
[2-3 sentence overview]
|
||||
|
||||
## User Story
|
||||
As a [user], I want [capability], so that [benefit].
|
||||
|
||||
## Problem → Solution
|
||||
[Current state] → [Desired state]
|
||||
|
||||
## Metadata
|
||||
- **Complexity**: [Small | Medium | Large | XL]
|
||||
- **Source PRD**: [path or "N/A"]
|
||||
- **PRD Phase**: [phase name or "N/A"]
|
||||
- **Estimated Files**: [count]
|
||||
|
||||
---
|
||||
|
||||
## UX Design
|
||||
|
||||
### Before
|
||||
[ASCII diagram or "N/A — internal change"]
|
||||
|
||||
### After
|
||||
[ASCII diagram or "N/A — internal change"]
|
||||
|
||||
### Interaction Changes
|
||||
| Touchpoint | Before | After | Notes |
|
||||
|---|---|---|---|
|
||||
|
||||
---
|
||||
|
||||
## Mandatory Reading
|
||||
|
||||
Files that MUST be read before implementing:
|
||||
|
||||
| Priority | File | Lines | Why |
|
||||
|---|---|---|---|
|
||||
| P0 (critical) | `path/to/file` | 1-50 | Core pattern to follow |
|
||||
| P1 (important) | `path/to/file` | 10-30 | Related types |
|
||||
| P2 (reference) | `path/to/file` | all | Similar implementation |
|
||||
|
||||
## External Documentation
|
||||
|
||||
| Topic | Source | Key Takeaway |
|
||||
|---|---|---|
|
||||
| ... | ... | ... |
|
||||
|
||||
---
|
||||
|
||||
## Patterns to Mirror
|
||||
|
||||
Code patterns discovered in the codebase. Follow these exactly.
|
||||
|
||||
### NAMING_CONVENTION
|
||||
// SOURCE: [file:lines]
|
||||
[actual code snippet showing the naming pattern]
|
||||
|
||||
### ERROR_HANDLING
|
||||
// SOURCE: [file:lines]
|
||||
[actual code snippet showing error handling]
|
||||
|
||||
### LOGGING_PATTERN
|
||||
// SOURCE: [file:lines]
|
||||
[actual code snippet showing logging]
|
||||
|
||||
### REPOSITORY_PATTERN
|
||||
// SOURCE: [file:lines]
|
||||
[actual code snippet showing data access]
|
||||
|
||||
### SERVICE_PATTERN
|
||||
// SOURCE: [file:lines]
|
||||
[actual code snippet showing service layer]
|
||||
|
||||
### TEST_STRUCTURE
|
||||
// SOURCE: [file:lines]
|
||||
[actual code snippet showing test setup]
|
||||
|
||||
---
|
||||
|
||||
## Files to Change
|
||||
|
||||
| File | Action | Justification |
|
||||
|---|---|---|
|
||||
| `path/to/file.ts` | CREATE | New service for feature |
|
||||
| `path/to/existing.ts` | UPDATE | Add new method |
|
||||
|
||||
## NOT Building
|
||||
|
||||
- [Explicit item 1 that is out of scope]
|
||||
- [Explicit item 2 that is out of scope]
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Tasks
|
||||
|
||||
### Task 1: [Name]
|
||||
- **ACTION**: [What to do]
|
||||
- **IMPLEMENT**: [Specific code/logic to write]
|
||||
- **MIRROR**: [Pattern from Patterns to Mirror section to follow]
|
||||
- **IMPORTS**: [Required imports]
|
||||
- **GOTCHA**: [Known pitfall to avoid]
|
||||
- **VALIDATE**: [How to verify this task is correct]
|
||||
|
||||
### Task 2: [Name]
|
||||
- **ACTION**: ...
|
||||
- **IMPLEMENT**: ...
|
||||
- **MIRROR**: ...
|
||||
- **IMPORTS**: ...
|
||||
- **GOTCHA**: ...
|
||||
- **VALIDATE**: ...
|
||||
|
||||
[Continue for all tasks...]
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
| Test | Input | Expected Output | Edge Case? |
|
||||
|---|---|---|---|
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
### Edge Cases Checklist
|
||||
- [ ] Empty input
|
||||
- [ ] Maximum size input
|
||||
- [ ] Invalid types
|
||||
- [ ] Concurrent access
|
||||
- [ ] Network failure (if applicable)
|
||||
- [ ] Permission denied
|
||||
|
||||
---
|
||||
|
||||
## Validation Commands
|
||||
|
||||
### Static Analysis
|
||||
```bash
|
||||
# Run type checker
|
||||
[project-specific type check command]
|
||||
```
|
||||
EXPECT: Zero type errors
|
||||
|
||||
### Unit Tests
|
||||
```bash
|
||||
# Run tests for affected area
|
||||
[project-specific test command]
|
||||
```
|
||||
EXPECT: All tests pass
|
||||
|
||||
### Full Test Suite
|
||||
```bash
|
||||
# Run complete test suite
|
||||
[project-specific full test command]
|
||||
```
|
||||
EXPECT: No regressions
|
||||
|
||||
### Database Validation (if applicable)
|
||||
```bash
|
||||
# Verify schema/migrations
|
||||
[project-specific db command]
|
||||
```
|
||||
EXPECT: Schema up to date
|
||||
|
||||
### Browser Validation (if applicable)
|
||||
```bash
|
||||
# Start dev server and verify
|
||||
[project-specific dev server command]
|
||||
```
|
||||
EXPECT: Feature works as designed
|
||||
|
||||
### Manual Validation
|
||||
- [ ] [Step-by-step manual verification checklist]
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] All tasks completed
|
||||
- [ ] All validation commands pass
|
||||
- [ ] Tests written and passing
|
||||
- [ ] No type errors
|
||||
- [ ] No lint errors
|
||||
- [ ] Matches UX design (if applicable)
|
||||
|
||||
## Completion Checklist
|
||||
- [ ] Code follows discovered patterns
|
||||
- [ ] Error handling matches codebase style
|
||||
- [ ] Logging follows codebase conventions
|
||||
- [ ] Tests follow test patterns
|
||||
- [ ] No hardcoded values
|
||||
- [ ] Documentation updated (if needed)
|
||||
- [ ] No unnecessary scope additions
|
||||
- [ ] Self-contained — no questions needed during implementation
|
||||
|
||||
## Risks
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
## Notes
|
||||
[Any additional context, decisions, or observations]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Output
|
||||
|
||||
### Save the Plan
|
||||
|
||||
Write the generated plan to:
|
||||
```
|
||||
.claude/PRPs/plans/{kebab-case-feature-name}.plan.md
|
||||
```
|
||||
|
||||
### Update PRD (if input was a PRD)
|
||||
|
||||
If this plan was generated from a PRD phase:
|
||||
1. Update the phase status from `pending` to `in-progress`
|
||||
2. Add the plan file path as a reference in the phase
|
||||
|
||||
### Report to User
|
||||
|
||||
```
|
||||
## Plan Created
|
||||
|
||||
- **File**: .claude/PRPs/plans/{kebab-case-feature-name}.plan.md
|
||||
- **Source PRD**: [path or "N/A"]
|
||||
- **Phase**: [phase name or "standalone"]
|
||||
- **Complexity**: [level]
|
||||
- **Scope**: [N files, M tasks]
|
||||
- **Key Patterns**: [top 3 discovered patterns]
|
||||
- **External Research**: [topics researched or "none needed"]
|
||||
- **Risks**: [top risk or "none identified"]
|
||||
- **Confidence Score**: [1-10] — likelihood of single-pass implementation
|
||||
|
||||
> Next step: Run `/prp-implement .claude/PRPs/plans/{name}.plan.md` to execute this plan.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
Before finalizing, verify the plan against these checklists:
|
||||
|
||||
### Context Completeness
|
||||
- [ ] All relevant files discovered and documented
|
||||
- [ ] Naming conventions captured with examples
|
||||
- [ ] Error handling patterns documented
|
||||
- [ ] Test patterns identified
|
||||
- [ ] Dependencies listed
|
||||
|
||||
### Implementation Readiness
|
||||
- [ ] Every task has ACTION, IMPLEMENT, MIRROR, and VALIDATE
|
||||
- [ ] No task requires additional codebase searching
|
||||
- [ ] Import paths are specified
|
||||
- [ ] GOTCHAs documented where applicable
|
||||
|
||||
### Pattern Faithfulness
|
||||
- [ ] Code snippets are actual codebase examples (not invented)
|
||||
- [ ] SOURCE references point to real files and line numbers
|
||||
- [ ] Patterns cover naming, errors, logging, data access, and tests
|
||||
- [ ] New code will be indistinguishable from existing code
|
||||
|
||||
### Validation Coverage
|
||||
- [ ] Static analysis commands specified
|
||||
- [ ] Test commands specified
|
||||
- [ ] Build verification included
|
||||
|
||||
### UX Clarity
|
||||
- [ ] Before/after states documented (or marked N/A)
|
||||
- [ ] Interaction changes listed
|
||||
- [ ] Edge cases for UX identified
|
||||
|
||||
### No Prior Knowledge Test
|
||||
A developer unfamiliar with this codebase should be able to implement the feature using ONLY this plan, without searching the codebase or asking questions. If not, add the missing context.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Run `/prp-implement <plan-path>` to execute this plan
|
||||
- Run `/plan` for quick conversational planning without artifacts
|
||||
- Run `/prp-prd` to create a PRD first if scope is unclear
|
||||
````
|
||||
@@ -0,0 +1,184 @@
|
||||
---
|
||||
description: "Alias of /pr for the PRP workflow series. Use when creating a pull request mid-PRP workflow; otherwise use /pr."
|
||||
argument-hint: "[base-branch] (default: main)"
|
||||
---
|
||||
|
||||
# Create Pull Request
|
||||
|
||||
> Adapted from PRPs-agentic-eng by Wirasm. Part of the PRP workflow series.
|
||||
|
||||
**Input**: `$ARGUMENTS` — optional, may contain a base branch name and/or flags (e.g., `--draft`).
|
||||
|
||||
**Parse `$ARGUMENTS`**:
|
||||
- Extract any recognized flags (`--draft`)
|
||||
- Treat remaining non-flag text as the base branch name
|
||||
- Default base branch to `main` if none specified
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — VALIDATE
|
||||
|
||||
Check preconditions:
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
git status --short
|
||||
git log origin/<base>..HEAD --oneline
|
||||
```
|
||||
|
||||
| Check | Condition | Action if Failed |
|
||||
|---|---|---|
|
||||
| Not on base branch | Current branch ≠ base | Stop: "Switch to a feature branch first." |
|
||||
| Clean working directory | No uncommitted changes | Warn: "You have uncommitted changes. Commit or stash first. Use `/prp-commit` to commit." |
|
||||
| Has commits ahead | `git log origin/<base>..HEAD` not empty | Stop: "No commits ahead of `<base>`. Nothing to PR." |
|
||||
| No existing PR | `gh pr list --head <branch> --json number` is empty | Stop: "PR already exists: #<number>. Use `gh pr view <number> --web` to open it." |
|
||||
|
||||
If all checks pass, proceed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — DISCOVER
|
||||
|
||||
### PR Template
|
||||
|
||||
Search for PR template in order:
|
||||
|
||||
1. `.github/PULL_REQUEST_TEMPLATE/` directory — if exists, list files and let user choose (or use `default.md`)
|
||||
2. `.github/PULL_REQUEST_TEMPLATE.md`
|
||||
3. `.github/pull_request_template.md`
|
||||
4. `docs/pull_request_template.md`
|
||||
|
||||
If found, read it and use its structure for the PR body.
|
||||
|
||||
### Commit Analysis
|
||||
|
||||
```bash
|
||||
git log origin/<base>..HEAD --format="%h %s" --reverse
|
||||
```
|
||||
|
||||
Analyze commits to determine:
|
||||
- **PR title**: Use conventional commit format with type prefix — `feat: ...`, `fix: ...`, etc.
|
||||
- If multiple types, use the dominant one
|
||||
- If single commit, use its message as-is
|
||||
- **Change summary**: Group commits by type/area
|
||||
|
||||
### File Analysis
|
||||
|
||||
```bash
|
||||
git diff origin/<base>..HEAD --stat
|
||||
git diff origin/<base>..HEAD --name-only
|
||||
```
|
||||
|
||||
Categorize changed files: source, tests, docs, config, migrations.
|
||||
|
||||
### PRP Artifacts
|
||||
|
||||
Check for related PRP artifacts:
|
||||
- `.claude/PRPs/reports/` — Implementation reports
|
||||
- `.claude/PRPs/plans/` — Plans that were executed
|
||||
- `.claude/PRPs/prds/` — Related PRDs
|
||||
|
||||
Reference these in the PR body if they exist.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — PUSH
|
||||
|
||||
```bash
|
||||
git push -u origin HEAD
|
||||
```
|
||||
|
||||
If push fails due to divergence:
|
||||
```bash
|
||||
git fetch origin
|
||||
git rebase origin/<base>
|
||||
git push -u origin HEAD
|
||||
```
|
||||
|
||||
If rebase conflicts occur, stop and inform the user.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — CREATE
|
||||
|
||||
### With Template
|
||||
|
||||
If a PR template was found in Phase 2, fill in each section using the commit and file analysis. Preserve all template sections — leave sections as "N/A" if not applicable rather than removing them.
|
||||
|
||||
### Without Template
|
||||
|
||||
Use this default format:
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
|
||||
<1-2 sentence description of what this PR does and why>
|
||||
|
||||
## Changes
|
||||
|
||||
<bulleted list of changes grouped by area>
|
||||
|
||||
## Files Changed
|
||||
|
||||
<table or list of changed files with change type: Added/Modified/Deleted>
|
||||
|
||||
## Testing
|
||||
|
||||
<description of how changes were tested, or "Needs testing">
|
||||
|
||||
## Related Issues
|
||||
|
||||
<linked issues with Closes/Fixes/Relates to #N, or "None">
|
||||
```
|
||||
|
||||
### Create the PR
|
||||
|
||||
```bash
|
||||
gh pr create \
|
||||
--title "<PR title>" \
|
||||
--base <base-branch> \
|
||||
--body "<PR body>"
|
||||
# Add --draft if the --draft flag was parsed from $ARGUMENTS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — VERIFY
|
||||
|
||||
```bash
|
||||
gh pr view --json number,url,title,state,baseRefName,headRefName,additions,deletions,changedFiles
|
||||
gh pr checks --json name,status,conclusion 2>/dev/null || true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — OUTPUT
|
||||
|
||||
Report to user:
|
||||
|
||||
```
|
||||
PR #<number>: <title>
|
||||
URL: <url>
|
||||
Branch: <head> → <base>
|
||||
Changes: +<additions> -<deletions> across <changedFiles> files
|
||||
|
||||
CI Checks: <status summary or "pending" or "none configured">
|
||||
|
||||
Artifacts referenced:
|
||||
- <any PRP reports/plans linked in PR body>
|
||||
|
||||
Next steps:
|
||||
- gh pr view <number> --web → open in browser
|
||||
- /code-review <number> → review the PR
|
||||
- gh pr merge <number> → merge when ready
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- **No `gh` CLI**: Stop with: "GitHub CLI (`gh`) is required. Install: <https://cli.github.com/>"
|
||||
- **Not authenticated**: Stop with: "Run `gh auth login` first."
|
||||
- **Force push needed**: If remote has diverged and rebase was done, use `git push --force-with-lease` (never `--force`).
|
||||
- **Multiple PR templates**: If `.github/PULL_REQUEST_TEMPLATE/` has multiple files, list them and ask user to choose.
|
||||
- **Large PR (>20 files)**: Warn about PR size. Suggest splitting if changes are logically separable.
|
||||
@@ -0,0 +1,447 @@
|
||||
---
|
||||
description: "Interactive PRD generator - problem-first, hypothesis-driven product spec with back-and-forth questioning"
|
||||
argument-hint: "[feature/product idea] (blank = start with questions)"
|
||||
---
|
||||
|
||||
# Product Requirements Document Generator
|
||||
|
||||
> Adapted from PRPs-agentic-eng by Wirasm. Part of the PRP workflow series.
|
||||
|
||||
**Input**: $ARGUMENTS
|
||||
|
||||
---
|
||||
|
||||
## Your Role
|
||||
|
||||
You are a sharp product manager who:
|
||||
- Starts with PROBLEMS, not solutions
|
||||
- Demands evidence before building
|
||||
- Thinks in hypotheses, not specs
|
||||
- Asks clarifying questions before assuming
|
||||
- Acknowledges uncertainty honestly
|
||||
|
||||
**Anti-pattern**: Don't fill sections with fluff. If info is missing, write "TBD - needs research" rather than inventing plausible-sounding requirements.
|
||||
|
||||
---
|
||||
|
||||
## Process Overview
|
||||
|
||||
```
|
||||
QUESTION SET 1 → GROUNDING → QUESTION SET 2 → RESEARCH → QUESTION SET 3 → GENERATE
|
||||
```
|
||||
|
||||
Each question set builds on previous answers. Grounding phases validate assumptions.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: INITIATE - Core Problem
|
||||
|
||||
**If no input provided**, ask:
|
||||
|
||||
> **What do you want to build?**
|
||||
> Describe the product, feature, or capability in a few sentences.
|
||||
|
||||
**If input provided**, confirm understanding by restating:
|
||||
|
||||
> I understand you want to build: {restated understanding}
|
||||
> Is this correct, or should I adjust my understanding?
|
||||
|
||||
**GATE**: Wait for user response before proceeding.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: FOUNDATION - Problem Discovery
|
||||
|
||||
Ask these questions (present all at once, user can answer together):
|
||||
|
||||
> **Foundation Questions:**
|
||||
>
|
||||
> 1. **Who** has this problem? Be specific - not just "users" but what type of person/role?
|
||||
>
|
||||
> 2. **What** problem are they facing? Describe the observable pain, not the assumed need.
|
||||
>
|
||||
> 3. **Why** can't they solve it today? What alternatives exist and why do they fail?
|
||||
>
|
||||
> 4. **Why now?** What changed that makes this worth building?
|
||||
>
|
||||
> 5. **How** will you know if you solved it? What would success look like?
|
||||
|
||||
**GATE**: Wait for user responses before proceeding.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: GROUNDING - Market & Context Research
|
||||
|
||||
After foundation answers, conduct research:
|
||||
|
||||
**Research market context:**
|
||||
|
||||
1. Find similar products/features in the market
|
||||
2. Identify how competitors solve this problem
|
||||
3. Note common patterns and anti-patterns
|
||||
4. Check for recent trends or changes in this space
|
||||
|
||||
Compile findings with direct links, key insights, and any gaps in available information.
|
||||
|
||||
**If a codebase exists, explore it in parallel:**
|
||||
|
||||
1. Find existing functionality relevant to the product/feature idea
|
||||
2. Identify patterns that could be leveraged
|
||||
3. Note technical constraints or opportunities
|
||||
|
||||
Record file locations, code patterns, and conventions observed.
|
||||
|
||||
**Summarize findings to user:**
|
||||
|
||||
> **What I found:**
|
||||
> - {Market insight 1}
|
||||
> - {Competitor approach}
|
||||
> - {Relevant pattern from codebase, if applicable}
|
||||
>
|
||||
> Does this change or refine your thinking?
|
||||
|
||||
**GATE**: Brief pause for user input (can be "continue" or adjustments).
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: DEEP DIVE - Vision & Users
|
||||
|
||||
Based on foundation + research, ask:
|
||||
|
||||
> **Vision & Users:**
|
||||
>
|
||||
> 1. **Vision**: In one sentence, what's the ideal end state if this succeeds wildly?
|
||||
>
|
||||
> 2. **Primary User**: Describe your most important user - their role, context, and what triggers their need.
|
||||
>
|
||||
> 3. **Job to Be Done**: Complete this: "When [situation], I want to [motivation], so I can [outcome]."
|
||||
>
|
||||
> 4. **Non-Users**: Who is explicitly NOT the target? Who should we ignore?
|
||||
>
|
||||
> 5. **Constraints**: What limitations exist? (time, budget, technical, regulatory)
|
||||
|
||||
**GATE**: Wait for user responses before proceeding.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: GROUNDING - Technical Feasibility
|
||||
|
||||
**If a codebase exists, perform two parallel investigations:**
|
||||
|
||||
Investigation 1 — Explore feasibility:
|
||||
1. Identify existing infrastructure that can be leveraged
|
||||
2. Find similar patterns already implemented
|
||||
3. Map integration points and dependencies
|
||||
4. Locate relevant configuration and type definitions
|
||||
|
||||
Record file locations, code patterns, and conventions observed.
|
||||
|
||||
Investigation 2 — Analyze constraints:
|
||||
1. Trace how existing related features are implemented end-to-end
|
||||
2. Map data flow through potential integration points
|
||||
3. Identify architectural patterns and boundaries
|
||||
4. Estimate complexity based on similar features
|
||||
|
||||
Document what exists with precise file:line references. No suggestions.
|
||||
|
||||
**If no codebase, research technical approaches:**
|
||||
|
||||
1. Find technical approaches others have used
|
||||
2. Identify common implementation patterns
|
||||
3. Note known technical challenges and pitfalls
|
||||
|
||||
Compile findings with citations and gap analysis.
|
||||
|
||||
**Summarize to user:**
|
||||
|
||||
> **Technical Context:**
|
||||
> - Feasibility: {HIGH/MEDIUM/LOW} because {reason}
|
||||
> - Can leverage: {existing patterns/infrastructure}
|
||||
> - Key technical risk: {main concern}
|
||||
>
|
||||
> Any technical constraints I should know about?
|
||||
|
||||
**GATE**: Brief pause for user input.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: DECISIONS - Scope & Approach
|
||||
|
||||
Ask final clarifying questions:
|
||||
|
||||
> **Scope & Approach:**
|
||||
>
|
||||
> 1. **MVP Definition**: What's the absolute minimum to test if this works?
|
||||
>
|
||||
> 2. **Must Have vs Nice to Have**: What 2-3 things MUST be in v1? What can wait?
|
||||
>
|
||||
> 3. **Key Hypothesis**: Complete this: "We believe [capability] will [solve problem] for [users]. We'll know we're right when [measurable outcome]."
|
||||
>
|
||||
> 4. **Out of Scope**: What are you explicitly NOT building (even if users ask)?
|
||||
>
|
||||
> 5. **Open Questions**: What uncertainties could change the approach?
|
||||
|
||||
**GATE**: Wait for user responses before generating.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: GENERATE - Write PRD
|
||||
|
||||
**Output path**: `.claude/PRPs/prds/{kebab-case-name}.prd.md`
|
||||
|
||||
Create directory if needed: `mkdir -p .claude/PRPs/prds`
|
||||
|
||||
### PRD Template
|
||||
|
||||
```markdown
|
||||
# {Product/Feature Name}
|
||||
|
||||
## Problem Statement
|
||||
|
||||
{2-3 sentences: Who has what problem, and what's the cost of not solving it?}
|
||||
|
||||
## Evidence
|
||||
|
||||
- {User quote, data point, or observation that proves this problem exists}
|
||||
- {Another piece of evidence}
|
||||
- {If none: "Assumption - needs validation through [method]"}
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
{One paragraph: What we're building and why this approach over alternatives}
|
||||
|
||||
## Key Hypothesis
|
||||
|
||||
We believe {capability} will {solve problem} for {users}.
|
||||
We'll know we're right when {measurable outcome}.
|
||||
|
||||
## What We're NOT Building
|
||||
|
||||
- {Out of scope item 1} - {why}
|
||||
- {Out of scope item 2} - {why}
|
||||
|
||||
## Success Metrics
|
||||
|
||||
| Metric | Target | How Measured |
|
||||
|--------|--------|--------------|
|
||||
| {Primary metric} | {Specific number} | {Method} |
|
||||
| {Secondary metric} | {Specific number} | {Method} |
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] {Unresolved question 1}
|
||||
- [ ] {Unresolved question 2}
|
||||
|
||||
---
|
||||
|
||||
## Users & Context
|
||||
|
||||
**Primary User**
|
||||
- **Who**: {Specific description}
|
||||
- **Current behavior**: {What they do today}
|
||||
- **Trigger**: {What moment triggers the need}
|
||||
- **Success state**: {What "done" looks like}
|
||||
|
||||
**Job to Be Done**
|
||||
When {situation}, I want to {motivation}, so I can {outcome}.
|
||||
|
||||
**Non-Users**
|
||||
{Who this is NOT for and why}
|
||||
|
||||
---
|
||||
|
||||
## Solution Detail
|
||||
|
||||
### Core Capabilities (MoSCoW)
|
||||
|
||||
| Priority | Capability | Rationale |
|
||||
|----------|------------|-----------|
|
||||
| Must | {Feature} | {Why essential} |
|
||||
| Must | {Feature} | {Why essential} |
|
||||
| Should | {Feature} | {Why important but not blocking} |
|
||||
| Could | {Feature} | {Nice to have} |
|
||||
| Won't | {Feature} | {Explicitly deferred and why} |
|
||||
|
||||
### MVP Scope
|
||||
|
||||
{What's the minimum to validate the hypothesis}
|
||||
|
||||
### User Flow
|
||||
|
||||
{Critical path - shortest journey to value}
|
||||
|
||||
---
|
||||
|
||||
## Technical Approach
|
||||
|
||||
**Feasibility**: {HIGH/MEDIUM/LOW}
|
||||
|
||||
**Architecture Notes**
|
||||
- {Key technical decision and why}
|
||||
- {Dependency or integration point}
|
||||
|
||||
**Technical Risks**
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|------|------------|------------|
|
||||
| {Risk} | {H/M/L} | {How to handle} |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
<!--
|
||||
STATUS: pending | in-progress | complete
|
||||
PARALLEL: phases that can run concurrently (e.g., "with 3" or "-")
|
||||
DEPENDS: phases that must complete first (e.g., "1, 2" or "-")
|
||||
PRP: link to generated plan file once created
|
||||
-->
|
||||
|
||||
| # | Phase | Description | Status | Parallel | Depends | PRP Plan |
|
||||
|---|-------|-------------|--------|----------|---------|----------|
|
||||
| 1 | {Phase name} | {What this phase delivers} | pending | - | - | - |
|
||||
| 2 | {Phase name} | {What this phase delivers} | pending | - | 1 | - |
|
||||
| 3 | {Phase name} | {What this phase delivers} | pending | with 4 | 2 | - |
|
||||
| 4 | {Phase name} | {What this phase delivers} | pending | with 3 | 2 | - |
|
||||
| 5 | {Phase name} | {What this phase delivers} | pending | - | 3, 4 | - |
|
||||
|
||||
### Phase Details
|
||||
|
||||
**Phase 1: {Name}**
|
||||
- **Goal**: {What we're trying to achieve}
|
||||
- **Scope**: {Bounded deliverables}
|
||||
- **Success signal**: {How we know it's done}
|
||||
|
||||
**Phase 2: {Name}**
|
||||
- **Goal**: {What we're trying to achieve}
|
||||
- **Scope**: {Bounded deliverables}
|
||||
- **Success signal**: {How we know it's done}
|
||||
|
||||
{Continue for each phase...}
|
||||
|
||||
### Parallelism Notes
|
||||
|
||||
{Explain which phases can run in parallel and why}
|
||||
|
||||
---
|
||||
|
||||
## Decisions Log
|
||||
|
||||
| Decision | Choice | Alternatives | Rationale |
|
||||
|----------|--------|--------------|-----------|
|
||||
| {Decision} | {Choice} | {Options considered} | {Why this one} |
|
||||
|
||||
---
|
||||
|
||||
## Research Summary
|
||||
|
||||
**Market Context**
|
||||
{Key findings from market research}
|
||||
|
||||
**Technical Context**
|
||||
{Key findings from technical exploration}
|
||||
|
||||
---
|
||||
|
||||
*Generated: {timestamp}*
|
||||
*Status: DRAFT - needs validation*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: OUTPUT - Summary
|
||||
|
||||
After generating, report:
|
||||
|
||||
```markdown
|
||||
## PRD Created
|
||||
|
||||
**File**: `.claude/PRPs/prds/{name}.prd.md`
|
||||
|
||||
### Summary
|
||||
|
||||
**Problem**: {One line}
|
||||
**Solution**: {One line}
|
||||
**Key Metric**: {Primary success metric}
|
||||
|
||||
### Validation Status
|
||||
|
||||
| Section | Status |
|
||||
|---------|--------|
|
||||
| Problem Statement | {Validated/Assumption} |
|
||||
| User Research | {Done/Needed} |
|
||||
| Technical Feasibility | {Assessed/TBD} |
|
||||
| Success Metrics | {Defined/Needs refinement} |
|
||||
|
||||
### Open Questions ({count})
|
||||
|
||||
{List the open questions that need answers}
|
||||
|
||||
### Recommended Next Step
|
||||
|
||||
{One of: user research, technical spike, prototype, stakeholder review, etc.}
|
||||
|
||||
### Implementation Phases
|
||||
|
||||
| # | Phase | Status | Can Parallel |
|
||||
|---|-------|--------|--------------|
|
||||
{Table of phases from PRD}
|
||||
|
||||
### To Start Implementation
|
||||
|
||||
Run: `/prp-plan .claude/PRPs/prds/{name}.prd.md`
|
||||
|
||||
This will automatically select the next pending phase and create an implementation plan.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Question Flow Summary
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ INITIATE: "What do you want to build?" │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ FOUNDATION: Who, What, Why, Why now, How to measure │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ GROUNDING: Market research, competitor analysis │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ DEEP DIVE: Vision, Primary user, JTBD, Constraints │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ GROUNDING: Technical feasibility, codebase exploration │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ DECISIONS: MVP, Must-haves, Hypothesis, Out of scope │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ GENERATE: Write PRD to .claude/PRPs/prds/ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration with ECC
|
||||
|
||||
After PRD generation:
|
||||
- Use `/prp-plan` to create implementation plans from PRD phases
|
||||
- Use `/plan` for simpler planning without PRD structure
|
||||
- Use `/save-session` to preserve PRD context across sessions
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- **PROBLEM_VALIDATED**: Problem is specific and evidenced (or marked as assumption)
|
||||
- **USER_DEFINED**: Primary user is concrete, not generic
|
||||
- **HYPOTHESIS_CLEAR**: Testable hypothesis with measurable outcome
|
||||
- **SCOPE_BOUNDED**: Clear must-haves and explicit out-of-scope
|
||||
- **QUESTIONS_ACKNOWLEDGED**: Uncertainties are listed, not hidden
|
||||
- **ACTIONABLE**: A skeptic could understand why this is worth building
|
||||
@@ -0,0 +1,265 @@
|
||||
---
|
||||
description: Enforce TDD workflow for React. Write React Testing Library tests first (behavior-focused, accessibility-first), then implement components. Detects Vitest or Jest and verifies coverage targets.
|
||||
---
|
||||
|
||||
# React TDD Command
|
||||
|
||||
This command enforces test-driven development for React using React Testing Library plus Vitest or Jest, detected at runtime.
|
||||
|
||||
## What This Command Does
|
||||
|
||||
1. **Define Component Signature**: Scaffold the component, prop type, and exports
|
||||
2. **Write Behavior Tests First**: RTL queries (role-first), `userEvent`, MSW for network — RED
|
||||
3. **Run Tests**: Verify they fail for the right reason
|
||||
4. **Implement Minimal Code**: Just enough to pass — GREEN
|
||||
5. **Refactor**: Improve while keeping tests green
|
||||
6. **Check Coverage**: Hit the targets in [rules/react/testing.md](../rules/react/testing.md)
|
||||
|
||||
## When to Use
|
||||
|
||||
Use `/react-test` when:
|
||||
|
||||
- Implementing a new React component or custom hook
|
||||
- Adding test coverage to an untested component
|
||||
- Fixing a bug (write failing test first that reproduces it)
|
||||
- Building forms, state machines, or accessibility-critical UI
|
||||
- Onboarding to RTL + Vitest/Jest workflow
|
||||
|
||||
## TDD Cycle
|
||||
|
||||
```
|
||||
RED -> Write failing test for the next behavior
|
||||
GREEN -> Implement minimal component code to pass
|
||||
REFACTOR -> Improve component, tests stay green
|
||||
REPEAT -> Next behavior
|
||||
```
|
||||
|
||||
## Runner Detection
|
||||
|
||||
```bash
|
||||
test -f vitest.config.ts -o -f vitest.config.js -o -f vite.config.ts # Vitest
|
||||
grep -l '"jest"' package.json # Jest
|
||||
```
|
||||
|
||||
Prefer Vitest for new Vite-based projects; respect Jest for existing setups.
|
||||
|
||||
## Example Session
|
||||
|
||||
````text
|
||||
User: /react-test I need a SearchInput component with debounced search
|
||||
|
||||
Agent:
|
||||
# TDD Session: SearchInput
|
||||
|
||||
## Step 1: Define Signature
|
||||
|
||||
```tsx
|
||||
// src/components/SearchInput.tsx
|
||||
type Props = {
|
||||
onSearch: (query: string) => void;
|
||||
placeholder?: string;
|
||||
debounceMs?: number;
|
||||
};
|
||||
|
||||
export function SearchInput({ onSearch, placeholder, debounceMs }: Props) {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
```
|
||||
|
||||
## Step 2: Write Behavior Tests (RED)
|
||||
|
||||
```tsx
|
||||
// src/components/SearchInput.test.tsx
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { SearchInput } from "./SearchInput";
|
||||
|
||||
describe("SearchInput", () => {
|
||||
test("renders with placeholder", () => {
|
||||
render(<SearchInput onSearch={() => {}} placeholder="Search users" />);
|
||||
expect(screen.getByPlaceholderText("Search users")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls onSearch after typing", async () => {
|
||||
vi.useFakeTimers();
|
||||
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
||||
const onSearch = vi.fn();
|
||||
render(<SearchInput onSearch={onSearch} debounceMs={300} />);
|
||||
|
||||
await user.type(screen.getByRole("textbox"), "alice");
|
||||
|
||||
expect(onSearch).not.toHaveBeenCalled(); // before debounce
|
||||
vi.advanceTimersByTime(300);
|
||||
expect(onSearch).toHaveBeenCalledWith("alice"); // after debounce
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("does not call onSearch when typing pauses then continues", async () => {
|
||||
vi.useFakeTimers();
|
||||
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
||||
const onSearch = vi.fn();
|
||||
render(<SearchInput onSearch={onSearch} debounceMs={300} />);
|
||||
|
||||
await user.type(screen.getByRole("textbox"), "ali");
|
||||
vi.advanceTimersByTime(200); // mid-debounce
|
||||
await user.type(screen.getByRole("textbox"), "ce");
|
||||
vi.advanceTimersByTime(300);
|
||||
|
||||
expect(onSearch).toHaveBeenCalledTimes(1);
|
||||
expect(onSearch).toHaveBeenCalledWith("alice");
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("is keyboard reachable and accessible", () => {
|
||||
render(<SearchInput onSearch={() => {}} />);
|
||||
const input = screen.getByRole("textbox");
|
||||
input.focus();
|
||||
expect(input).toHaveFocus();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Step 3: Run Tests — Verify FAIL
|
||||
|
||||
```bash
|
||||
$ vitest run src/components/SearchInput.test.tsx
|
||||
|
||||
× src/components/SearchInput.test.tsx (4 tests) ✘ Error: not implemented
|
||||
```
|
||||
|
||||
✓ Tests fail as expected.
|
||||
|
||||
## Step 4: Implement Minimal Code (GREEN)
|
||||
|
||||
```tsx
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function SearchInput({ onSearch, placeholder, debounceMs = 300 }: Props) {
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const id = setTimeout(() => onSearch(query), debounceMs);
|
||||
return () => clearTimeout(id);
|
||||
}, [query, onSearch, debounceMs]);
|
||||
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5: Run Tests — Verify PASS
|
||||
|
||||
```bash
|
||||
$ vitest run src/components/SearchInput.test.tsx
|
||||
|
||||
✓ src/components/SearchInput.test.tsx (4 tests) 47ms
|
||||
```
|
||||
|
||||
## Step 6: Coverage
|
||||
|
||||
```bash
|
||||
$ vitest run --coverage src/components/SearchInput.test.tsx
|
||||
|
||||
% Stmts: 100 % Branch: 100 % Funcs: 100 % Lines: 100
|
||||
```
|
||||
|
||||
## TDD Complete!
|
||||
````
|
||||
|
||||
## Test Patterns
|
||||
|
||||
### Behavior, not implementation
|
||||
|
||||
Use `getByRole`, `getByLabelText`, `getByText`. Avoid `container.querySelector` and asserting on component state.
|
||||
|
||||
### `userEvent.setup()` per test
|
||||
|
||||
```tsx
|
||||
const user = userEvent.setup();
|
||||
await user.click(screen.getByRole("button", { name: /save/i }));
|
||||
```
|
||||
|
||||
### MSW for network
|
||||
|
||||
```tsx
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
||||
server.use(http.post("/api/users", () => HttpResponse.json({ id: "1" }, { status: 201 })));
|
||||
```
|
||||
|
||||
### Custom hooks
|
||||
|
||||
```tsx
|
||||
const { result } = renderHook(() => useCounter(0));
|
||||
act(() => result.current.increment());
|
||||
expect(result.current.count).toBe(1);
|
||||
```
|
||||
|
||||
### Accessibility
|
||||
|
||||
```tsx
|
||||
import { axe } from "vitest-axe";
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
```
|
||||
|
||||
## Coverage Targets
|
||||
|
||||
| Layer | Target |
|
||||
|---|---|
|
||||
| Pure utilities | >=90% |
|
||||
| Custom hooks | >=85% |
|
||||
| Presentational components | >=80% |
|
||||
| Container components | >=70% |
|
||||
| Pages | E2E covered separately |
|
||||
|
||||
Configure in `vitest.config.ts` / `jest.config.js` to enforce thresholds in CI.
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- `container.querySelector(...)` — bypasses accessibility queries
|
||||
- Asserting on render count
|
||||
- Mocking `react` itself (`jest.mock("react", ...)`)
|
||||
- Mocking child components by default (mock only when child has heavy side effects)
|
||||
- Ignoring `act()` warnings — they signal real bugs
|
||||
- Snapshot tests of rendered components (brittle, rubber-stamped) — use Playwright/Cypress visual diff instead
|
||||
|
||||
## Test Commands
|
||||
|
||||
```bash
|
||||
# Vitest
|
||||
vitest # watch
|
||||
vitest run # one-shot
|
||||
vitest run --coverage # with coverage
|
||||
vitest run path/to/file.test.tsx # single file
|
||||
|
||||
# Jest
|
||||
jest --watch
|
||||
jest --coverage
|
||||
jest path/to/file.test.tsx
|
||||
|
||||
# CI mode
|
||||
CI=true vitest run --coverage
|
||||
```
|
||||
|
||||
## Related Commands
|
||||
|
||||
- `/react-build` — fix build errors before running tests
|
||||
- `/react-review` — review after implementation
|
||||
- `verification-loop` skill — full verification loop
|
||||
|
||||
## Related
|
||||
|
||||
- Skills: `skills/react-testing/`, `skills/tdd-workflow/`, `skills/accessibility/`, `skills/e2e-testing/`
|
||||
- Rules: `rules/react/testing.md`
|
||||
- Agents: `react-reviewer` (reviews test quality), `tdd-guide` (enforces TDD process)
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
description: Safely identify and remove dead code with verification after each change.
|
||||
---
|
||||
|
||||
# Refactor Clean
|
||||
|
||||
Safely identify and remove dead code with test verification at every step.
|
||||
|
||||
## Step 1: Detect Dead Code
|
||||
|
||||
Run analysis tools based on project type:
|
||||
|
||||
| Tool | What It Finds | Command |
|
||||
|------|--------------|---------|
|
||||
| knip | Unused exports, files, dependencies | `npx knip` |
|
||||
| depcheck | Unused npm dependencies | `npx depcheck` |
|
||||
| ts-prune | Unused TypeScript exports | `npx ts-prune` |
|
||||
| vulture | Unused Python code | `vulture src/` |
|
||||
| deadcode | Unused Go code | `deadcode ./...` |
|
||||
| cargo-udeps | Unused Rust dependencies | `cargo +nightly udeps` |
|
||||
|
||||
If no tool is available, use Grep to find exports with zero imports:
|
||||
```
|
||||
# Find exports, then check if they're imported anywhere
|
||||
```
|
||||
|
||||
## Step 2: Categorize Findings
|
||||
|
||||
Sort findings into safety tiers:
|
||||
|
||||
| Tier | Examples | Action |
|
||||
|------|----------|--------|
|
||||
| **SAFE** | Unused utilities, test helpers, internal functions | Delete with confidence |
|
||||
| **CAUTION** | Components, API routes, middleware | Verify no dynamic imports or external consumers |
|
||||
| **DANGER** | Config files, entry points, type definitions | Investigate before touching |
|
||||
|
||||
## Step 3: Safe Deletion Loop
|
||||
|
||||
For each SAFE item:
|
||||
|
||||
1. **Run full test suite** — Establish baseline (all green)
|
||||
2. **Delete the dead code** — Use Edit tool for surgical removal
|
||||
3. **Re-run test suite** — Verify nothing broke
|
||||
4. **If tests fail** — Immediately revert with `git checkout -- <file>` and skip this item
|
||||
5. **If tests pass** — Move to next item
|
||||
|
||||
## Step 4: Handle CAUTION Items
|
||||
|
||||
Before deleting CAUTION items:
|
||||
- Search for dynamic imports: `import()`, `require()`, `__import__`
|
||||
- Search for string references: route names, component names in configs
|
||||
- Check if exported from a public package API
|
||||
- Verify no external consumers (check dependents if published)
|
||||
|
||||
## Step 5: Consolidate Duplicates
|
||||
|
||||
After removing dead code, look for:
|
||||
- Near-duplicate functions (>80% similar) — merge into one
|
||||
- Redundant type definitions — consolidate
|
||||
- Wrapper functions that add no value — inline them
|
||||
- Re-exports that serve no purpose — remove indirection
|
||||
|
||||
## Step 6: Summary
|
||||
|
||||
Report results:
|
||||
|
||||
```
|
||||
Dead Code Cleanup
|
||||
──────────────────────────────
|
||||
Deleted: 12 unused functions
|
||||
3 unused files
|
||||
5 unused dependencies
|
||||
Skipped: 2 items (tests failed)
|
||||
Saved: ~450 lines removed
|
||||
──────────────────────────────
|
||||
All tests passing PASS:
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- **Never delete without running tests first**
|
||||
- **One deletion at a time** — Atomic changes make rollback easy
|
||||
- **Skip if uncertain** — Better to keep dead code than break production
|
||||
- **Don't refactor while cleaning** — Separate concerns (clean first, refactor later)
|
||||
@@ -0,0 +1,308 @@
|
||||
---
|
||||
description: Enforce TDD workflow for Rust. Write tests first, then implement. Verify 80%+ coverage with cargo-llvm-cov.
|
||||
---
|
||||
|
||||
# Rust TDD Command
|
||||
|
||||
This command enforces test-driven development methodology for Rust code using `#[test]`, rstest, proptest, and mockall.
|
||||
|
||||
## What This Command Does
|
||||
|
||||
1. **Define Types/Traits**: Scaffold function signatures with `todo!()`
|
||||
2. **Write Tests**: Create comprehensive test module (RED)
|
||||
3. **Run Tests**: Verify tests fail for the right reason
|
||||
4. **Implement Code**: Write minimal code to pass (GREEN)
|
||||
5. **Refactor**: Improve while keeping tests green
|
||||
6. **Check Coverage**: Ensure 80%+ coverage with cargo-llvm-cov
|
||||
|
||||
## When to Use
|
||||
|
||||
Use `/rust-test` when:
|
||||
- Implementing new Rust functions, methods, or traits
|
||||
- Adding test coverage to existing Rust code
|
||||
- Fixing bugs (write failing test first)
|
||||
- Building critical business logic
|
||||
- Learning TDD workflow in Rust
|
||||
|
||||
## TDD Cycle
|
||||
|
||||
```
|
||||
RED -> Write failing test first
|
||||
GREEN -> Implement minimal code to pass
|
||||
REFACTOR -> Improve code, tests stay green
|
||||
REPEAT -> Next test case
|
||||
```
|
||||
|
||||
## Example Session
|
||||
|
||||
````text
|
||||
User: /rust-test I need a function to validate user registration
|
||||
|
||||
Agent:
|
||||
# TDD Session: Registration Validator
|
||||
|
||||
## Step 1: Define Interface
|
||||
|
||||
```rust
|
||||
// src/validator.rs
|
||||
pub struct RegistrationRequest {
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
pub enum ValidationResult {
|
||||
Valid,
|
||||
Invalid(Vec<String>),
|
||||
}
|
||||
|
||||
pub fn validate_registration(request: &RegistrationRequest) -> ValidationResult {
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
## Step 2: Write Tests (RED)
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn valid_registration_returns_valid() {
|
||||
let request = RegistrationRequest {
|
||||
name: "Alice".into(),
|
||||
email: "alice@example.com".into(),
|
||||
password: "SecureP@ss1".into(),
|
||||
};
|
||||
assert!(matches!(validate_registration(&request), ValidationResult::Valid));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blank_name_returns_invalid() {
|
||||
let request = RegistrationRequest {
|
||||
name: "".into(),
|
||||
email: "alice@example.com".into(),
|
||||
password: "SecureP@ss1".into(),
|
||||
};
|
||||
match validate_registration(&request) {
|
||||
ValidationResult::Invalid(errors) => {
|
||||
assert!(errors.contains(&"Name is required".to_string()));
|
||||
}
|
||||
ValidationResult::Valid => panic!("expected Invalid"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_email_returns_invalid() {
|
||||
let request = RegistrationRequest {
|
||||
name: "Alice".into(),
|
||||
email: "not-an-email".into(),
|
||||
password: "SecureP@ss1".into(),
|
||||
};
|
||||
assert!(matches!(
|
||||
validate_registration(&request),
|
||||
ValidationResult::Invalid(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_password_returns_invalid() {
|
||||
let request = RegistrationRequest {
|
||||
name: "Alice".into(),
|
||||
email: "alice@example.com".into(),
|
||||
password: "short".into(),
|
||||
};
|
||||
assert!(matches!(
|
||||
validate_registration(&request),
|
||||
ValidationResult::Invalid(_)
|
||||
));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: Run Tests — Verify FAIL
|
||||
|
||||
```bash
|
||||
$ cargo test
|
||||
running 4 tests
|
||||
test tests::valid_registration_returns_valid ... FAILED (not yet implemented)
|
||||
test tests::blank_name_returns_invalid ... FAILED
|
||||
test tests::invalid_email_returns_invalid ... FAILED
|
||||
test tests::short_password_returns_invalid ... FAILED
|
||||
```
|
||||
|
||||
Tests fail as expected (todo! panic).
|
||||
|
||||
## Step 4: Implement Minimal Code (GREEN)
|
||||
|
||||
```rust
|
||||
pub fn validate_registration(request: &RegistrationRequest) -> ValidationResult {
|
||||
let mut errors = Vec::new();
|
||||
|
||||
if request.name.trim().is_empty() {
|
||||
errors.push("Name is required".into());
|
||||
}
|
||||
if !request.email.contains('@') {
|
||||
errors.push("Invalid email format".into());
|
||||
}
|
||||
if request.password.len() < 8 {
|
||||
errors.push("Password must be at least 8 characters".into());
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
ValidationResult::Valid
|
||||
} else {
|
||||
ValidationResult::Invalid(errors)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5: Run Tests — Verify PASS
|
||||
|
||||
```bash
|
||||
$ cargo test
|
||||
running 4 tests
|
||||
test tests::valid_registration_returns_valid ... ok
|
||||
test tests::blank_name_returns_invalid ... ok
|
||||
test tests::invalid_email_returns_invalid ... ok
|
||||
test tests::short_password_returns_invalid ... ok
|
||||
|
||||
test result: ok. 4 passed; 0 failed
|
||||
```
|
||||
|
||||
All tests passing!
|
||||
|
||||
## Step 6: Check Coverage
|
||||
|
||||
```bash
|
||||
$ cargo llvm-cov
|
||||
Coverage: 100.0% of lines
|
||||
```
|
||||
|
||||
Coverage: 100%
|
||||
|
||||
## TDD Complete!
|
||||
````
|
||||
|
||||
## Test Patterns
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn adds_two_numbers() {
|
||||
assert_eq!(add(2, 3), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_error() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let result = parse_config(r#"port = 8080"#)?;
|
||||
assert_eq!(result.port, 8080);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Parameterized Tests with rstest
|
||||
|
||||
```rust
|
||||
use rstest::{rstest, fixture};
|
||||
|
||||
#[rstest]
|
||||
#[case("hello", 5)]
|
||||
#[case("", 0)]
|
||||
#[case("rust", 4)]
|
||||
fn test_string_length(#[case] input: &str, #[case] expected: usize) {
|
||||
assert_eq!(input.len(), expected);
|
||||
}
|
||||
```
|
||||
|
||||
### Async Tests
|
||||
|
||||
```rust
|
||||
#[tokio::test]
|
||||
async fn fetches_data_successfully() {
|
||||
let client = TestClient::new().await;
|
||||
let result = client.get("/data").await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
```
|
||||
|
||||
### Property-Based Tests
|
||||
|
||||
```rust
|
||||
use proptest::prelude::*;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn encode_decode_roundtrip(input in ".*") {
|
||||
let encoded = encode(&input);
|
||||
let decoded = decode(&encoded).unwrap();
|
||||
assert_eq!(input, decoded);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Coverage Commands
|
||||
|
||||
```bash
|
||||
# Summary report
|
||||
cargo llvm-cov
|
||||
|
||||
# HTML report
|
||||
cargo llvm-cov --html
|
||||
|
||||
# Fail if below threshold
|
||||
cargo llvm-cov --fail-under-lines 80
|
||||
|
||||
# Run specific test
|
||||
cargo test test_name
|
||||
|
||||
# Run with output
|
||||
cargo test -- --nocapture
|
||||
|
||||
# Run without stopping on first failure
|
||||
cargo test --no-fail-fast
|
||||
```
|
||||
|
||||
## Coverage Targets
|
||||
|
||||
| Code Type | Target |
|
||||
|-----------|--------|
|
||||
| Critical business logic | 100% |
|
||||
| Public API | 90%+ |
|
||||
| General code | 80%+ |
|
||||
| Generated / FFI bindings | Exclude |
|
||||
|
||||
## TDD Best Practices
|
||||
|
||||
**DO:**
|
||||
- Write test FIRST, before any implementation
|
||||
- Run tests after each change
|
||||
- Use `assert_eq!` over `assert!` for better error messages
|
||||
- Use `?` in tests that return `Result` for cleaner output
|
||||
- Test behavior, not implementation
|
||||
- Include edge cases (empty, boundary, error paths)
|
||||
|
||||
**DON'T:**
|
||||
- Write implementation before tests
|
||||
- Skip the RED phase
|
||||
- Use `#[should_panic]` when `Result::is_err()` works
|
||||
- Use `sleep()` in tests — use channels or `tokio::time::pause()`
|
||||
- Mock everything — prefer integration tests when feasible
|
||||
|
||||
## Related Commands
|
||||
|
||||
- `/rust-build` - Fix build errors
|
||||
- `/rust-review` - Review code after implementation
|
||||
- `verification-loop` skill - Run full verification loop
|
||||
|
||||
## Related
|
||||
|
||||
- Skill: `skills/rust-testing/`
|
||||
- Skill: `skills/rust-patterns/`
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
description: Analyze coverage, identify gaps, and generate missing tests toward the target threshold.
|
||||
---
|
||||
|
||||
# Test Coverage
|
||||
|
||||
Analyze test coverage, identify gaps, and generate missing tests to reach 80%+ coverage.
|
||||
|
||||
## Step 1: Detect Test Framework
|
||||
|
||||
| Indicator | Coverage Command |
|
||||
|-----------|-----------------|
|
||||
| `jest.config.*` or `package.json` jest | `npx jest --coverage --coverageReporters=json-summary` |
|
||||
| `vitest.config.*` | `npx vitest run --coverage` |
|
||||
| `pytest.ini` / `pyproject.toml` pytest | `pytest --cov=src --cov-report=json` |
|
||||
| `Cargo.toml` | `cargo llvm-cov --json` |
|
||||
| `pom.xml` with JaCoCo | `mvn test jacoco:report` |
|
||||
| `go.mod` | `go test -coverprofile=coverage.out ./...` |
|
||||
|
||||
## Step 2: Analyze Coverage Report
|
||||
|
||||
1. Run the coverage command
|
||||
2. Parse the output (JSON summary or terminal output)
|
||||
3. List files **below 80% coverage**, sorted worst-first
|
||||
4. For each under-covered file, identify:
|
||||
- Untested functions or methods
|
||||
- Missing branch coverage (if/else, switch, error paths)
|
||||
- Dead code that inflates the denominator
|
||||
|
||||
## Step 3: Generate Missing Tests
|
||||
|
||||
For each under-covered file, generate tests following this priority:
|
||||
|
||||
1. **Happy path** — Core functionality with valid inputs
|
||||
2. **Error handling** — Invalid inputs, missing data, network failures
|
||||
3. **Edge cases** — Empty arrays, null/undefined, boundary values (0, -1, MAX_INT)
|
||||
4. **Branch coverage** — Each if/else, switch case, ternary
|
||||
|
||||
### Test Generation Rules
|
||||
|
||||
- Place tests adjacent to source: `foo.ts` → `foo.test.ts` (or project convention)
|
||||
- Use existing test patterns from the project (import style, assertion library, mocking approach)
|
||||
- Mock external dependencies (database, APIs, file system)
|
||||
- Each test should be independent — no shared mutable state between tests
|
||||
- Name tests descriptively: `test_create_user_with_duplicate_email_returns_409`
|
||||
|
||||
## Step 4: Verify
|
||||
|
||||
1. Run the full test suite — all tests must pass
|
||||
2. Re-run coverage — verify improvement
|
||||
3. If still below 80%, repeat Step 3 for remaining gaps
|
||||
|
||||
## Step 5: Report
|
||||
|
||||
Show before/after comparison:
|
||||
|
||||
```
|
||||
Coverage Report
|
||||
──────────────────────────────
|
||||
File Before After
|
||||
src/services/auth.ts 45% 88%
|
||||
src/utils/validation.ts 32% 82%
|
||||
──────────────────────────────
|
||||
Overall: 67% 84% PASS:
|
||||
```
|
||||
|
||||
## Focus Areas
|
||||
|
||||
- Functions with complex branching (high cyclomatic complexity)
|
||||
- Error handlers and catch blocks
|
||||
- Utility functions used across the codebase
|
||||
- API endpoint handlers (request → response flow)
|
||||
- Edge cases: null, undefined, empty string, empty array, zero, negative numbers
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
description: Scan project structure and generate token-lean architecture codemaps.
|
||||
---
|
||||
|
||||
# Update Codemaps
|
||||
|
||||
Analyze the codebase structure and generate token-lean architecture documentation.
|
||||
|
||||
## Step 1: Scan Project Structure
|
||||
|
||||
1. Identify the project type (monorepo, single app, library, microservice)
|
||||
2. Find all source directories (src/, lib/, app/, packages/)
|
||||
3. Map entry points (main.ts, index.ts, app.py, main.go, etc.)
|
||||
|
||||
## Step 2: Generate Codemaps
|
||||
|
||||
Create or update codemaps in `docs/CODEMAPS/` (or `.reports/codemaps/`):
|
||||
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `architecture.md` | High-level system diagram, service boundaries, data flow |
|
||||
| `backend.md` | API routes, middleware chain, service → repository mapping |
|
||||
| `frontend.md` | Page tree, component hierarchy, state management flow |
|
||||
| `data.md` | Database tables, relationships, migration history |
|
||||
| `dependencies.md` | External services, third-party integrations, shared libraries |
|
||||
|
||||
### Codemap Format
|
||||
|
||||
Each codemap should be token-lean — optimized for AI context consumption:
|
||||
|
||||
```markdown
|
||||
# Backend Architecture
|
||||
|
||||
## Routes
|
||||
POST /api/users → UserController.create → UserService.create → UserRepo.insert
|
||||
GET /api/users/:id → UserController.get → UserService.findById → UserRepo.findById
|
||||
|
||||
## Key Files
|
||||
src/services/user.ts (business logic, 120 lines)
|
||||
src/repos/user.ts (database access, 80 lines)
|
||||
|
||||
## Dependencies
|
||||
- PostgreSQL (primary data store)
|
||||
- Redis (session cache, rate limiting)
|
||||
- Stripe (payment processing)
|
||||
```
|
||||
|
||||
## Step 3: Diff Detection
|
||||
|
||||
1. If previous codemaps exist, calculate the diff percentage
|
||||
2. If changes > 30%, show the diff and request user approval before overwriting
|
||||
3. If changes <= 30%, update in place
|
||||
|
||||
## Step 4: Add Metadata
|
||||
|
||||
Add a freshness header to each codemap:
|
||||
|
||||
```markdown
|
||||
<!-- Generated: 2026-02-11 | Files scanned: 142 | Token estimate: ~800 -->
|
||||
```
|
||||
|
||||
## Step 5: Save Analysis Report
|
||||
|
||||
Write a summary to `.reports/codemap-diff.txt`:
|
||||
- Files added/removed/modified since last scan
|
||||
- New dependencies detected
|
||||
- Architecture changes (new routes, new services, etc.)
|
||||
- Staleness warnings for docs not updated in 90+ days
|
||||
|
||||
## Tips
|
||||
|
||||
- Focus on **high-level structure**, not implementation details
|
||||
- Prefer **file paths and function signatures** over full code blocks
|
||||
- Keep each codemap under **1000 tokens** for efficient context loading
|
||||
- Use ASCII diagrams for data flow instead of verbose descriptions
|
||||
- Run after major feature additions or refactoring sessions
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
description: Sync documentation from source-of-truth files such as scripts, schemas, routes, and exports.
|
||||
---
|
||||
|
||||
# Update Documentation
|
||||
|
||||
Sync documentation with the codebase, generating from source-of-truth files.
|
||||
|
||||
## Step 1: Identify Sources of Truth
|
||||
|
||||
| Source | Generates |
|
||||
|--------|-----------|
|
||||
| `package.json` scripts | Available commands reference |
|
||||
| `.env.example` | Environment variable documentation |
|
||||
| `openapi.yaml` / route files | API endpoint reference |
|
||||
| Source code exports | Public API documentation |
|
||||
| `Dockerfile` / `docker-compose.yml` | Infrastructure setup docs |
|
||||
|
||||
## Step 2: Generate Script Reference
|
||||
|
||||
1. Read `package.json` (or `Makefile`, `Cargo.toml`, `pyproject.toml`)
|
||||
2. Extract all scripts/commands with their descriptions
|
||||
3. Generate a reference table:
|
||||
|
||||
```markdown
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `npm run dev` | Start development server with hot reload |
|
||||
| `npm run build` | Production build with type checking |
|
||||
| `npm test` | Run test suite with coverage |
|
||||
```
|
||||
|
||||
## Step 3: Generate Environment Documentation
|
||||
|
||||
1. Read `.env.example` (or `.env.template`, `.env.sample`)
|
||||
2. Extract all variables with their purposes
|
||||
3. Categorize as required vs optional
|
||||
4. Document expected format and valid values
|
||||
|
||||
```markdown
|
||||
| Variable | Required | Description | Example |
|
||||
|----------|----------|-------------|---------|
|
||||
| `DATABASE_URL` | Yes | PostgreSQL connection string | `postgres://user:pass@host:5432/db` |
|
||||
| `LOG_LEVEL` | No | Logging verbosity (default: info) | `debug`, `info`, `warn`, `error` |
|
||||
```
|
||||
|
||||
## Step 4: Update Contributing Guide
|
||||
|
||||
Generate or update `docs/CONTRIBUTING.md` with:
|
||||
- Development environment setup (prerequisites, install steps)
|
||||
- Available scripts and their purposes
|
||||
- Testing procedures (how to run, how to write new tests)
|
||||
- Code style enforcement (linter, formatter, pre-commit hooks)
|
||||
- PR submission checklist
|
||||
|
||||
## Step 5: Update Runbook
|
||||
|
||||
Generate or update `docs/RUNBOOK.md` with:
|
||||
- Deployment procedures (step-by-step)
|
||||
- Health check endpoints and monitoring
|
||||
- Common issues and their fixes
|
||||
- Rollback procedures
|
||||
- Alerting and escalation paths
|
||||
|
||||
## Step 6: Staleness Check
|
||||
|
||||
1. Find documentation files not modified in 90+ days
|
||||
2. Cross-reference with recent source code changes
|
||||
3. Flag potentially outdated docs for manual review
|
||||
|
||||
## Step 7: Show Summary
|
||||
|
||||
```
|
||||
Documentation Update
|
||||
──────────────────────────────
|
||||
Updated: docs/CONTRIBUTING.md (scripts table)
|
||||
Updated: docs/ENV.md (3 new variables)
|
||||
Flagged: docs/DEPLOY.md (142 days stale)
|
||||
Skipped: docs/API.md (no changes detected)
|
||||
──────────────────────────────
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- **Single source of truth**: Always generate from code, never manually edit generated sections
|
||||
- **Preserve manual sections**: Only update generated sections; leave hand-written prose intact
|
||||
- **Mark generated content**: Use `<!-- AUTO-GENERATED -->` markers around generated sections
|
||||
- **Don't create docs unprompted**: Only create new doc files if the command explicitly requests it
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "ecc-pi-core",
|
||||
"version": "2.2.2",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"pi-package",
|
||||
"skills"
|
||||
],
|
||||
"pi": {
|
||||
"skills": [
|
||||
"./skills"
|
||||
],
|
||||
"prompts": [
|
||||
"./commands"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
---
|
||||
name: accessibility
|
||||
description: Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels, accessibility traits and hints, focus management, contrast, target size, and screen-reader support. Use when building or auditing UI for accessibility compliance, keyboard navigation, or screen-reader support.
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Accessibility (WCAG 2.2)
|
||||
|
||||
This skill ensures that digital interfaces are Perceivable, Operable, Understandable, and Robust (POUR) for all users, including those using screen readers, switch controls, or keyboard navigation. It focuses on the technical implementation of WCAG 2.2 success criteria.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Defining UI component specifications for Web, iOS, or Android.
|
||||
- Auditing existing code for accessibility barriers or compliance gaps.
|
||||
- Implementing new WCAG 2.2 standards like Target Size (Minimum) and Focus Appearance.
|
||||
- Mapping high-level design requirements to technical attributes (ARIA roles, traits, hints).
|
||||
|
||||
## Core Concepts
|
||||
|
||||
- **POUR Principles**: The foundation of WCAG (Perceivable, Operable, Understandable, Robust).
|
||||
- **Semantic Mapping**: Using native elements over generic containers to provide built-in accessibility.
|
||||
- **Accessibility Tree**: The representation of the UI that assistive technologies actually "read."
|
||||
- **Focus Management**: Controlling the order and visibility of the keyboard/screen reader cursor.
|
||||
- **Labeling & Hints**: Providing context through `aria-label`, `accessibilityLabel`, and `contentDescription`.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Step 1: Identify the Component Role
|
||||
|
||||
Determine the functional purpose (e.g., Is this a button, a link, or a tab?). Use the most semantic native element available before resorting to custom roles.
|
||||
|
||||
### Step 2: Define Perceivable Attributes
|
||||
|
||||
- Ensure text contrast meets **4.5:1** (normal) or **3:1** (large/UI).
|
||||
- Add text alternatives for non-text content (images, icons).
|
||||
- Implement responsive reflow (up to 400% zoom without loss of function).
|
||||
|
||||
### Step 3: Implement Operable Controls
|
||||
|
||||
- Ensure a minimum **24x24 CSS pixel** target size (WCAG 2.2 SC 2.5.8).
|
||||
- Verify all interactive elements are reachable via keyboard and have a visible focus indicator (SC 2.4.11).
|
||||
- Provide single-pointer alternatives for dragging movements.
|
||||
|
||||
### Step 4: Ensure Understandable Logic
|
||||
|
||||
- Use consistent navigation patterns.
|
||||
- Provide descriptive error messages and suggestions for correction (SC 3.3.3).
|
||||
- Implement "Redundant Entry" (SC 3.3.7) to prevent asking for the same data twice.
|
||||
|
||||
### Step 5: Verify Robust Compatibility
|
||||
|
||||
- Use correct `Name, Role, Value` patterns.
|
||||
- Implement `aria-live` or live regions for dynamic status updates.
|
||||
|
||||
## Accessibility Architecture Diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
UI["UI Component"] --> Platform{Platform?}
|
||||
Platform -->|Web| ARIA["WAI-ARIA + HTML5"]
|
||||
Platform -->|iOS| SwiftUI["Accessibility Traits + Labels"]
|
||||
Platform -->|Android| Compose["Semantics + ContentDesc"]
|
||||
|
||||
ARIA --> AT["Assistive Technology (Screen Readers, Switches)"]
|
||||
SwiftUI --> AT
|
||||
Compose --> AT
|
||||
```
|
||||
|
||||
## Cross-Platform Mapping
|
||||
|
||||
| Feature | Web (HTML/ARIA) | iOS (SwiftUI) | Android (Compose) |
|
||||
| :----------------- | :----------------------- | :----------------------------------- | :---------------------------------------------------------- |
|
||||
| **Primary Label** | `aria-label` / `<label>` | `.accessibilityLabel()` | `contentDescription` |
|
||||
| **Secondary Hint** | `aria-describedby` | `.accessibilityHint()` | `Modifier.semantics { stateDescription = ... }` |
|
||||
| **Action Role** | `role="button"` | `.accessibilityAddTraits(.isButton)` | `Modifier.semantics { role = Role.Button }` |
|
||||
| **Live Updates** | `aria-live="polite"` | `.accessibilityLiveRegion(.polite)` | `Modifier.semantics { liveRegion = LiveRegionMode.Polite }` |
|
||||
|
||||
## Examples
|
||||
|
||||
### Web: Accessible Search
|
||||
|
||||
```html
|
||||
<form role="search">
|
||||
<label for="search-input" class="sr-only">Search products</label>
|
||||
<input type="search" id="search-input" placeholder="Search..." />
|
||||
<button type="submit" aria-label="Submit Search">
|
||||
<svg aria-hidden="true">...</svg>
|
||||
</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### iOS: Accessible Action Button
|
||||
|
||||
```swift
|
||||
Button(action: deleteItem) {
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
.accessibilityLabel("Delete item")
|
||||
.accessibilityHint("Permanently removes this item from your list")
|
||||
.accessibilityAddTraits(.isButton)
|
||||
```
|
||||
|
||||
### Android: Accessible Toggle
|
||||
|
||||
```kotlin
|
||||
Switch(
|
||||
checked = isEnabled,
|
||||
onCheckedChange = { onToggle() },
|
||||
modifier = Modifier.semantics {
|
||||
contentDescription = "Enable notifications"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- **Div-Buttons**: Using a `<div>` or `<span>` for a click event without adding a role and keyboard support.
|
||||
- **Color-Only Meaning**: Indicating an error or status _only_ with a color change (e.g., turning a border red).
|
||||
- **Uncontained Modal Focus**: Modals that don't trap focus, allowing keyboard users to navigate background content while the modal is open. Focus must be contained _and_ escapable via the `Escape` key or an explicit close button (WCAG SC 2.1.2).
|
||||
- **Redundant Alt Text**: Using "Image of..." or "Picture of..." in alt text (screen readers already announce the role "Image").
|
||||
|
||||
## Best Practices Checklist
|
||||
|
||||
- [ ] Interactive elements meet the **24x24px** (Web) or **44x44pt** (Native) target size.
|
||||
- [ ] Focus indicators are clearly visible and high-contrast.
|
||||
- [ ] Modals **contain focus** while open, and release it cleanly on close (`Escape` key or close button).
|
||||
- [ ] Dropdowns and menus restore focus to the trigger element on close.
|
||||
- [ ] Forms provide text-based error suggestions.
|
||||
- [ ] All icon-only buttons have a descriptive text label.
|
||||
- [ ] Content reflows properly when text is scaled.
|
||||
|
||||
## References
|
||||
|
||||
- [WCAG 2.2 Guidelines](https://www.w3.org/TR/WCAG22/)
|
||||
- [WAI-ARIA Authoring Practices](https://www.w3.org/TR/wai-aria-practices/)
|
||||
- [iOS Accessibility Programming Guide](https://developer.apple.com/documentation/accessibility)
|
||||
- [iOS Human Interface Guidelines - Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility)
|
||||
- [Android Accessibility Developer Guide](https://developer.android.com/guide/topics/ui/accessibility)
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `frontend-patterns`
|
||||
- `design-system`
|
||||
- `liquid-glass-design`
|
||||
- `swiftui-patterns`
|
||||
@@ -0,0 +1,257 @@
|
||||
---
|
||||
name: agent-architecture-audit
|
||||
description: Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.
|
||||
metadata:
|
||||
origin: oh-my-agent-check
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
# Agent Architecture Audit
|
||||
|
||||
A diagnostic workflow for agent systems that hide failures behind wrapper layers, stale memory, retry loops, or transport/rendering mutations.
|
||||
|
||||
## When to Activate
|
||||
|
||||
**MANDATORY for:**
|
||||
- Releasing any agent or LLM-powered application to production
|
||||
- Shipping features with tool calling, memory, or multi-step workflows
|
||||
- Agent behavior degrades after adding wrapper layers
|
||||
- User reports "the agent is getting worse" or "tools are flaky"
|
||||
- Same model works in playground but breaks inside your wrapper
|
||||
- Debugging agent behavior for more than 15 minutes without finding root cause
|
||||
|
||||
**Especially critical when:**
|
||||
- You've added new prompt layers, tool definitions, or memory systems
|
||||
- Different agents in your system behave inconsistently
|
||||
- The model was fine yesterday but is hallucinating today
|
||||
- You suspect hidden repair/retry loops silently mutating responses
|
||||
|
||||
**Do not use for:**
|
||||
- General code debugging — use `agent-introspection-debugging`
|
||||
- Code review — use language-specific reviewer agents
|
||||
- Security scanning — use `security-review` or `security-review/scan`
|
||||
- Agent performance benchmarking — use `agent-eval`
|
||||
- Writing new features — use the appropriate workflow skill
|
||||
|
||||
## The 12-Layer Stack
|
||||
|
||||
Every agent system has these layers. Any of them can corrupt the answer:
|
||||
|
||||
| # | Layer | What Goes Wrong |
|
||||
|---|-------|----------------|
|
||||
| 1 | System prompt | Conflicting instructions, instruction bloat |
|
||||
| 2 | Session history | Stale context injection from previous turns |
|
||||
| 3 | Long-term memory | Pollution across sessions, old topics in new conversations |
|
||||
| 4 | Distillation | Compressed artifacts re-entering as pseudo-facts |
|
||||
| 5 | Active recall | Redundant re-summary layers wasting context |
|
||||
| 6 | Tool selection | Wrong tool routing, model skips required tools |
|
||||
| 7 | Tool execution | Hallucinated execution — claims to call but doesn't |
|
||||
| 8 | Tool interpretation | Misread or ignored tool output |
|
||||
| 9 | Answer shaping | Format corruption in final response |
|
||||
| 10 | Platform rendering | Transport-layer mutation (UI, API, CLI mutates valid answers) |
|
||||
| 11 | Hidden repair loops | Silent fallback/retry agents running second LLM pass |
|
||||
| 12 | Persistence | Expired state or cached artifacts reused as live evidence |
|
||||
|
||||
## Common Failure Patterns
|
||||
|
||||
### 1. Wrapper Regression
|
||||
|
||||
The base model produces correct answers, but the wrapper layers make it worse.
|
||||
|
||||
**Symptoms:**
|
||||
- Model works fine in playground or direct API call, breaks in your agent
|
||||
- Added a new prompt layer, existing behavior degraded
|
||||
- Agent sounds confident but is confidently wrong
|
||||
- "It was working before the last update"
|
||||
|
||||
### 2. Memory Contamination
|
||||
|
||||
Old topics leak into new conversations through history, memory retrieval, or distillation.
|
||||
|
||||
**Symptoms:**
|
||||
- Agent brings up unrelated past topics
|
||||
- User corrections don't stick (old memory overwrites new)
|
||||
- Same-session artifacts re-enter as pseudo-facts
|
||||
- Memory grows without bound, degrading response quality over time
|
||||
|
||||
### 3. Tool Discipline Failure
|
||||
|
||||
Tools are declared in the prompt but not enforced in code. The model skips them or hallucinates execution.
|
||||
|
||||
**Symptoms:**
|
||||
- "Must use tool X" in prompt, but model answers without calling it
|
||||
- Tool results look correct but were never actually executed
|
||||
- Different tools fight over the same responsibility
|
||||
- Model uses tool when it shouldn't, or skips it when it must
|
||||
|
||||
### 4. Rendering/Transport Corruption
|
||||
|
||||
The agent's internal answer is correct, but the platform layer mutates it during delivery.
|
||||
|
||||
**Symptoms:**
|
||||
- Logs show correct answer, user sees broken output
|
||||
- Markdown rendering, JSON parsing, or streaming fragments corrupt valid responses
|
||||
- Hidden fallback agent quietly replaces the answer before delivery
|
||||
- Output differs between terminal and UI
|
||||
|
||||
### 5. Hidden Agent Layers
|
||||
|
||||
Silent repair, retry, summarization, or recall agents run without explicit contracts.
|
||||
|
||||
**Symptoms:**
|
||||
- Output changes between internal generation and user delivery
|
||||
- "Auto-fix" loops run a second LLM pass the user doesn't know about
|
||||
- Multiple agents modify the same output without coordination
|
||||
- Answers get "smoothed" or "corrected" by invisible layers
|
||||
|
||||
## Audit Workflow
|
||||
|
||||
### Phase 1: Scope
|
||||
|
||||
Define what you're auditing:
|
||||
|
||||
- **Target system** — what agent application?
|
||||
- **Entrypoints** — how do users interact with it?
|
||||
- **Model stack** — which LLM(s) and providers?
|
||||
- **Symptoms** — what does the user report?
|
||||
- **Time window** — when did it start?
|
||||
- **Layers to audit** — which of the 12 layers apply?
|
||||
|
||||
### Phase 2: Evidence Collection
|
||||
|
||||
Gather evidence from the codebase:
|
||||
|
||||
- **Source code** — agent loop, tool router, memory admission, prompt assembly
|
||||
- **Logs** — historical session traces, tool call records
|
||||
- **Config** — prompt templates, tool schemas, provider settings
|
||||
- **Memory files** — SOPs, knowledge bases, session archives
|
||||
|
||||
Use `rg` to search for anti-patterns:
|
||||
|
||||
```bash
|
||||
# Tool requirements expressed only in prompt text (not code)
|
||||
rg "must.*tool|必须.*工具|required.*call" --type md
|
||||
|
||||
# Tool execution without validation
|
||||
rg "tool_call|toolCall|tool_use" --type py --type ts
|
||||
|
||||
# Hidden LLM calls outside main agent loop
|
||||
rg "completion|chat\.create|messages\.create|llm\.invoke"
|
||||
|
||||
# Memory admission without user-correction priority
|
||||
rg "memory.*admit|long.*term.*update|persist.*memory" --type py --type ts
|
||||
|
||||
# Fallback loops that run additional LLM calls
|
||||
rg "fallback|retry.*llm|repair.*prompt|re-?prompt" --type py --type ts
|
||||
|
||||
# Silent output mutation
|
||||
rg "mutate|rewrite.*response|transform.*output|shap" --type py --type ts
|
||||
```
|
||||
|
||||
### Phase 3: Failure Mapping
|
||||
|
||||
For each finding, document:
|
||||
|
||||
- **Symptom** — what the user sees
|
||||
- **Mechanism** — how the wrapper causes it
|
||||
- **Source layer** — which of the 12 layers
|
||||
- **Root cause** — the deepest cause
|
||||
- **Evidence** — file:line or log:row reference
|
||||
- **Confidence** — 0.0 to 1.0
|
||||
|
||||
### Phase 4: Fix Strategy
|
||||
|
||||
Default fix order (code-first, not prompt-first):
|
||||
|
||||
1. **Code-gate tool requirements** — enforce in code, not just prompt text
|
||||
2. **Remove or narrow hidden repair agents** — make fallback explicit with contracts
|
||||
3. **Reduce context duplication** — same info through prompt + history + memory + distillation
|
||||
4. **Tighten memory admission** — user corrections > agent assertions
|
||||
5. **Tighten distillation triggers** — don't compress what shouldn't be compressed
|
||||
6. **Reduce rendering mutation** — pass-through, don't transform
|
||||
7. **Convert to typed JSON envelopes** — structured internal flow, not freeform prose
|
||||
|
||||
## Severity Model
|
||||
|
||||
| Level | Meaning | Action |
|
||||
|-------|---------|--------|
|
||||
| `critical` | Agent can confidently produce wrong operational behavior | Fix before next release |
|
||||
| `high` | Agent frequently degrades correctness or stability | Fix this sprint |
|
||||
| `medium` | Correctness usually survives but output is fragile or wasteful | Plan for next cycle |
|
||||
| `low` | Mostly cosmetic or maintainability issues | Backlog |
|
||||
|
||||
## Output Format
|
||||
|
||||
Present findings to the user in this order:
|
||||
|
||||
1. **Severity-ranked findings** (most critical first)
|
||||
2. **Architecture diagnosis** (which layer corrupted what, and why)
|
||||
3. **Ordered fix plan** (code-first, not prompt-first)
|
||||
|
||||
Do not lead with compliments or summaries. If the system is broken, say so directly.
|
||||
|
||||
## Quick Diagnostic Questions
|
||||
|
||||
When auditing an agent system, answer these:
|
||||
|
||||
| # | Question | If Yes → |
|
||||
|---|----------|----------|
|
||||
| 1 | Can the model skip a required tool and still answer? | Tool not code-gated |
|
||||
| 2 | Does old conversation content appear in new turns? | Memory contamination |
|
||||
| 3 | Is the same info in system prompt AND memory AND history? | Context duplication |
|
||||
| 4 | Does the platform run a second LLM pass before delivery? | Hidden repair loop |
|
||||
| 5 | Does the output differ between internal generation and user delivery? | Rendering corruption |
|
||||
| 6 | Are "must use tool X" rules only in prompt text? | Tool discipline failure |
|
||||
| 7 | Can the agent's own monologue become persistent memory? | Memory poisoning |
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- Avoid blaming the model before falsifying wrapper-layer regressions.
|
||||
- Avoid blaming memory without showing the contamination path.
|
||||
- Do not let a clean current state erase a dirty historical incident.
|
||||
- Do not treat markdown prose as a trustworthy internal protocol.
|
||||
- Do not accept "must use tool" in prompt text when code never enforces it.
|
||||
- Keep findings direct, evidence-backed, and severity-ranked.
|
||||
|
||||
## Report Schema
|
||||
|
||||
Audits should produce structured reports following this shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "ecc.agent-architecture-audit.report.v1",
|
||||
"executive_verdict": {
|
||||
"overall_health": "high_risk",
|
||||
"primary_failure_mode": "string",
|
||||
"most_urgent_fix": "string"
|
||||
},
|
||||
"scope": {
|
||||
"target_name": "string",
|
||||
"model_stack": ["string"],
|
||||
"layers_to_audit": ["string"]
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"severity": "critical|high|medium|low",
|
||||
"title": "string",
|
||||
"mechanism": "string",
|
||||
"source_layer": "string",
|
||||
"root_cause": "string",
|
||||
"evidence_refs": ["file:line"],
|
||||
"confidence": 0.0,
|
||||
"recommended_fix": "string"
|
||||
}
|
||||
],
|
||||
"ordered_fix_plan": [
|
||||
{ "order": 1, "goal": "string", "why_now": "string", "expected_effect": "string" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `agent-introspection-debugging` — Debug agent runtime failures (loops, timeouts, state errors)
|
||||
- `agent-eval` — Benchmark agent performance head-to-head
|
||||
- `security-review` — Security audit for code and configuration
|
||||
- `autonomous-agent-harness` — Set up autonomous agent operations
|
||||
- `agent-harness-construction` — Build agent harnesses from scratch
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
name: agent-eval
|
||||
description: Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.
|
||||
license: MIT
|
||||
metadata:
|
||||
origin: ECC
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
# Agent Eval Skill
|
||||
|
||||
A lightweight CLI tool for comparing coding agents head-to-head on reproducible tasks. Every "which coding agent is best?" comparison runs on vibes — this tool systematizes it.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Comparing coding agents (Claude Code, Aider, Codex, etc.) on your own codebase
|
||||
- Measuring agent performance before adopting a new tool or model
|
||||
- Running regression checks when an agent updates its model or tooling
|
||||
- Producing data-backed agent selection decisions for a team
|
||||
|
||||
## Installation
|
||||
|
||||
> **Note:** Install agent-eval from its repository after reviewing the source.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### YAML Task Definitions
|
||||
|
||||
Define tasks declaratively. Each task specifies what to do, which files to touch, and how to judge success:
|
||||
|
||||
```yaml
|
||||
name: add-retry-logic
|
||||
description: Add exponential backoff retry to the HTTP client
|
||||
repo: ./my-project
|
||||
files:
|
||||
- src/http_client.py
|
||||
prompt: |
|
||||
Add retry logic with exponential backoff to all HTTP requests.
|
||||
Max 3 retries. Initial delay 1s, max delay 30s.
|
||||
judge:
|
||||
- type: pytest
|
||||
command: pytest tests/test_http_client.py -v
|
||||
- type: grep
|
||||
pattern: "exponential_backoff|retry"
|
||||
files: src/http_client.py
|
||||
commit: "abc1234" # pin to specific commit for reproducibility
|
||||
```
|
||||
|
||||
### Git Worktree Isolation
|
||||
|
||||
Each agent run gets its own git worktree — no Docker required. This provides reproducibility isolation so agents cannot interfere with each other or corrupt the base repo.
|
||||
|
||||
### Metrics Collected
|
||||
|
||||
| Metric | What It Measures |
|
||||
|--------|-----------------|
|
||||
| Pass rate | Did the agent produce code that passes the judge? |
|
||||
| Cost | API spend per task (when available) |
|
||||
| Time | Wall-clock seconds to completion |
|
||||
| Consistency | Pass rate across repeated runs (e.g., 3/3 = 100%) |
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Define Tasks
|
||||
|
||||
Create a `tasks/` directory with YAML files, one per task:
|
||||
|
||||
```bash
|
||||
mkdir tasks
|
||||
# Write task definitions (see template above)
|
||||
```
|
||||
|
||||
### 2. Run Agents
|
||||
|
||||
Execute agents against your tasks:
|
||||
|
||||
```bash
|
||||
agent-eval run --task tasks/add-retry-logic.yaml --agent claude-code --agent aider --runs 3
|
||||
```
|
||||
|
||||
Each run:
|
||||
1. Creates a fresh git worktree from the specified commit
|
||||
2. Hands the prompt to the agent
|
||||
3. Runs the judge criteria
|
||||
4. Records pass/fail, cost, and time
|
||||
|
||||
### 3. Compare Results
|
||||
|
||||
Generate a comparison report:
|
||||
|
||||
```bash
|
||||
agent-eval report --format table
|
||||
```
|
||||
|
||||
```
|
||||
Task: add-retry-logic (3 runs each)
|
||||
┌──────────────┬───────────┬────────┬────────┬─────────────┐
|
||||
│ Agent │ Pass Rate │ Cost │ Time │ Consistency │
|
||||
├──────────────┼───────────┼────────┼────────┼─────────────┤
|
||||
│ claude-code │ 3/3 │ $0.12 │ 45s │ 100% │
|
||||
│ aider │ 2/3 │ $0.08 │ 38s │ 67% │
|
||||
└──────────────┴───────────┴────────┴────────┴─────────────┘
|
||||
```
|
||||
|
||||
## Judge Types
|
||||
|
||||
### Code-Based (deterministic)
|
||||
|
||||
```yaml
|
||||
judge:
|
||||
- type: pytest
|
||||
command: pytest tests/ -v
|
||||
- type: command
|
||||
command: npm run build
|
||||
```
|
||||
|
||||
### Pattern-Based
|
||||
|
||||
```yaml
|
||||
judge:
|
||||
- type: grep
|
||||
pattern: "class.*Retry"
|
||||
files: src/**/*.py
|
||||
```
|
||||
|
||||
### Model-Based (LLM-as-judge)
|
||||
|
||||
```yaml
|
||||
judge:
|
||||
- type: llm
|
||||
prompt: |
|
||||
Does this implementation correctly handle exponential backoff?
|
||||
Check for: max retries, increasing delays, jitter.
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Start with 3-5 tasks** that represent your real workload, not toy examples
|
||||
- **Run at least 3 trials** per agent to capture variance — agents are non-deterministic
|
||||
- **Pin the commit** in your task YAML so results are reproducible across days/weeks
|
||||
- **Include at least one deterministic judge** (tests, build) per task — LLM judges add noise
|
||||
- **Track cost alongside pass rate** — a 95% agent at 10x the cost may not be the right choice
|
||||
- **Version your task definitions** — they are test fixtures, treat them as code
|
||||
|
||||
## Links
|
||||
|
||||
- Repository: [github.com/joaquinhuigomez/agent-eval](https://github.com/joaquinhuigomez/agent-eval)
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
name: agent-harness-construction
|
||||
description: Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent's tool set, action space, or observation format.
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Agent Harness Construction
|
||||
|
||||
Use this skill when you are improving how an agent plans, calls tools, recovers from errors, and converges on completion.
|
||||
|
||||
## Core Model
|
||||
|
||||
Agent output quality is constrained by:
|
||||
1. Action space quality
|
||||
2. Observation quality
|
||||
3. Recovery quality
|
||||
4. Context budget quality
|
||||
|
||||
## Action Space Design
|
||||
|
||||
1. Use stable, explicit tool names.
|
||||
2. Keep inputs schema-first and narrow.
|
||||
3. Return deterministic output shapes.
|
||||
4. Avoid catch-all tools unless isolation is impossible.
|
||||
|
||||
## Granularity Rules
|
||||
|
||||
- Use micro-tools for high-risk operations (deploy, migration, permissions).
|
||||
- Use medium tools for common edit/read/search loops.
|
||||
- Use macro-tools only when round-trip overhead is the dominant cost.
|
||||
|
||||
## Observation Design
|
||||
|
||||
Every tool response should include:
|
||||
- `status`: success|warning|error
|
||||
- `summary`: one-line result
|
||||
- `next_actions`: actionable follow-ups
|
||||
- `artifacts`: file paths / IDs
|
||||
|
||||
## Error Recovery Contract
|
||||
|
||||
For every error path, include:
|
||||
- root cause hint
|
||||
- safe retry instruction
|
||||
- explicit stop condition
|
||||
|
||||
## Context Budgeting
|
||||
|
||||
1. Keep system prompt minimal and invariant.
|
||||
2. Move large guidance into skills loaded on demand.
|
||||
3. Prefer references to files over inlining long documents.
|
||||
4. Compact at phase boundaries, not arbitrary token thresholds.
|
||||
|
||||
## Architecture Pattern Guidance
|
||||
|
||||
- ReAct: best for exploratory tasks with uncertain path.
|
||||
- Function-calling: best for structured deterministic flows.
|
||||
- Hybrid (recommended): ReAct planning + typed tool execution.
|
||||
|
||||
## Benchmarking
|
||||
|
||||
Track:
|
||||
- completion rate
|
||||
- retries per task
|
||||
- pass@1 and pass@3
|
||||
- cost per successful task
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Too many tools with overlapping semantics.
|
||||
- Opaque tool output with no recovery hints.
|
||||
- Error-only output without next steps.
|
||||
- Context overloading with irrelevant references.
|
||||
@@ -0,0 +1,154 @@
|
||||
---
|
||||
name: agent-introspection-debugging
|
||||
description: Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Agent Introspection Debugging
|
||||
|
||||
Use this skill when an agent run is failing repeatedly, consuming tokens without progress, looping on the same tools, or drifting away from the intended task.
|
||||
|
||||
This is a workflow skill, not a hidden runtime. It teaches the agent to debug itself systematically before escalating to a human.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Maximum tool call / loop-limit failures
|
||||
- Repeated retries with no forward progress
|
||||
- Context growth or prompt drift that starts degrading output quality
|
||||
- File-system or environment state mismatch between expectation and reality
|
||||
- Tool failures that are likely recoverable with diagnosis and a smaller corrective action
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
Activate this skill for:
|
||||
- capturing failure state before retrying blindly
|
||||
- diagnosing common agent-specific failure patterns
|
||||
- applying contained recovery actions
|
||||
- producing a structured human-readable debug report
|
||||
|
||||
Do not use this skill as the primary source for:
|
||||
- feature verification after code changes; use `verification-loop`
|
||||
- framework-specific debugging when a narrower ECC skill already exists
|
||||
- runtime promises the current harness cannot enforce automatically
|
||||
|
||||
## Four-Phase Loop
|
||||
|
||||
### Phase 1: Failure Capture
|
||||
|
||||
Before trying to recover, record the failure precisely.
|
||||
|
||||
Capture:
|
||||
- error type, message, and stack trace when available
|
||||
- last meaningful tool call sequence
|
||||
- what the agent was trying to do
|
||||
- current context pressure: repeated prompts, oversized pasted logs, duplicated plans, or runaway notes
|
||||
- current environment assumptions: cwd, branch, relevant service state, expected files
|
||||
|
||||
Minimum capture template:
|
||||
|
||||
```markdown
|
||||
## Failure Capture
|
||||
- Session / task:
|
||||
- Goal in progress:
|
||||
- Error:
|
||||
- Last successful step:
|
||||
- Last failed tool / command:
|
||||
- Repeated pattern seen:
|
||||
- Environment assumptions to verify:
|
||||
```
|
||||
|
||||
### Phase 2: Root-Cause Diagnosis
|
||||
|
||||
Match the failure to a known pattern before changing anything.
|
||||
|
||||
| Pattern | Likely Cause | Check |
|
||||
| --- | --- | --- |
|
||||
| Maximum tool calls / repeated same command | loop or no-exit observer path | inspect the last N tool calls for repetition |
|
||||
| Context overflow / degraded reasoning | unbounded notes, repeated plans, oversized logs | inspect recent context for duplication and low-signal bulk |
|
||||
| `ECONNREFUSED` / timeout | service unavailable or wrong port | verify service health, URL, and port assumptions |
|
||||
| `429` / quota exhaustion | retry storm or missing backoff | count repeated calls and inspect retry spacing |
|
||||
| file missing after write / stale diff | race, wrong cwd, or branch drift | re-check path, cwd, git status, and actual file existence |
|
||||
| tests still failing after “fix” | wrong hypothesis | isolate the exact failing test and re-derive the bug |
|
||||
|
||||
Diagnosis questions:
|
||||
- is this a logic failure, state failure, environment failure, or policy failure?
|
||||
- did the agent lose the real objective and start optimizing the wrong subtask?
|
||||
- is the failure deterministic or transient?
|
||||
- what is the smallest reversible action that would validate the diagnosis?
|
||||
|
||||
### Phase 3: Contained Recovery
|
||||
|
||||
Recover with the smallest action that changes the diagnosis surface.
|
||||
|
||||
Safe recovery actions:
|
||||
- stop repeated retries and restate the hypothesis
|
||||
- trim low-signal context and keep only the active goal, blockers, and evidence
|
||||
- re-check the actual filesystem / branch / process state
|
||||
- narrow the task to one failing command, one file, or one test
|
||||
- switch from speculative reasoning to direct observation
|
||||
- escalate to a human when the failure is high-risk or externally blocked
|
||||
|
||||
Do not claim unsupported auto-healing actions like “reset agent state” or “update harness config” unless you are actually doing them through real tools in the current environment.
|
||||
|
||||
Contained recovery checklist:
|
||||
|
||||
```markdown
|
||||
## Recovery Action
|
||||
- Diagnosis chosen:
|
||||
- Smallest action taken:
|
||||
- Why this is safe:
|
||||
- What evidence would prove the fix worked:
|
||||
```
|
||||
|
||||
### Phase 4: Introspection Report
|
||||
|
||||
End with a report that makes the recovery legible to the next agent or human.
|
||||
|
||||
```markdown
|
||||
## Agent Self-Debug Report
|
||||
- Session / task:
|
||||
- Failure:
|
||||
- Root cause:
|
||||
- Recovery action:
|
||||
- Result: success | partial | blocked
|
||||
- Token / time burn risk:
|
||||
- Follow-up needed:
|
||||
- Preventive change to encode later:
|
||||
```
|
||||
|
||||
## Recovery Heuristics
|
||||
|
||||
Prefer these interventions in order:
|
||||
|
||||
1. Restate the real objective in one sentence.
|
||||
2. Verify the world state instead of trusting memory.
|
||||
3. Shrink the failing scope.
|
||||
4. Run one discriminating check.
|
||||
5. Only then retry.
|
||||
|
||||
Bad pattern:
|
||||
- retrying the same action three times with slightly different wording
|
||||
|
||||
Good pattern:
|
||||
- capture failure
|
||||
- classify the pattern
|
||||
- run one direct check
|
||||
- change the plan only if the check supports it
|
||||
|
||||
## Integration with ECC
|
||||
|
||||
- Use `verification-loop` after recovery if code was changed.
|
||||
- Use `continuous-learning-v2` when the failure pattern is worth turning into an instinct or later skill.
|
||||
- Use `council` when the issue is not technical failure but decision ambiguity.
|
||||
- Use `workspace-surface-audit` if the failure came from conflicting local state or repo drift.
|
||||
|
||||
## Output Standard
|
||||
|
||||
When this skill is active, do not end with “I fixed it” alone.
|
||||
|
||||
Always provide:
|
||||
- the failure pattern
|
||||
- the root-cause hypothesis
|
||||
- the recovery action
|
||||
- the evidence that the situation is now better or still blocked
|
||||
@@ -0,0 +1,182 @@
|
||||
---
|
||||
name: agent-self-evaluation
|
||||
description: Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Agent Self-Evaluation
|
||||
|
||||
After completing a complex task, the agent pauses to rate its own output against a structured 5-axis rubric. This is NOT a pass/fail gate — it's a deliberate reflection step that catches omissions, flags overconfidence, and surface areas for improvement before the user has to.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- After writing code that spans 3+ files or 50+ lines
|
||||
- After completing a multi-step workflow (implement → test → review)
|
||||
- After a debugging session that involved 3+ attempts
|
||||
- After producing a design document, architecture decision, or written analysis
|
||||
- When the user asks "how good was that?" or "rate yourself"
|
||||
- At the end of any session Stop hook (if configured — see `references/hook-integration.md`)
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### The 5 Evaluation Axes
|
||||
|
||||
| Axis | Question | What it catches |
|
||||
|---|---|---|
|
||||
| **Accuracy** | Are the facts, claims, and outputs correct? | Hallucinations, wrong API names, incorrect syntax, false statements |
|
||||
| **Completeness** | Did it cover everything the user asked for? | Missed edge cases, unhandled error paths, forgotten requirements, skipped subtasks |
|
||||
| **Clarity** | Is the explanation understandable and well-structured? | Confusing explanations, jargon without definition, missing context, rambling |
|
||||
| **Actionability** | Can the user act on the output immediately? | Vague suggestions, missing steps, "you should X" without showing how, no verification path |
|
||||
| **Conciseness** | Did it use the minimum words/tokens needed? | Redundancy, over-explanation, repeating the user's question verbatim, filler content |
|
||||
|
||||
### Scoring Scale
|
||||
|
||||
```
|
||||
5 — Exceptional: no reasonable improvement possible
|
||||
4 — Good: minor nits only, no substantive gaps
|
||||
3 — Adequate: meets the request but has a notable weakness on at least one axis
|
||||
2 — Weak: has a clear gap that affects usability or correctness
|
||||
1 — Poor: fundamentally misses the request or contains significant errors
|
||||
```
|
||||
|
||||
### The Evidence Rule
|
||||
|
||||
Every score below 5 MUST cite specific evidence. A score of 3 cannot just say "could be better" — it must say exactly what is missing or wrong. The mantra: **"Show the gap, don't just name it."**
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Collect the Raw Material
|
||||
|
||||
Gather what you'll evaluate:
|
||||
|
||||
```
|
||||
- The original user request (read back from conversation)
|
||||
- Your final response/output (the deliverable)
|
||||
- Any tool outputs that verify correctness (test results, exit codes, lint output)
|
||||
- Any user feedback received during the task (corrections, "try again", "that's not right")
|
||||
```
|
||||
|
||||
### Step 2: Score Each Axis Independently
|
||||
|
||||
Work through the 5 axes one at a time. For each:
|
||||
|
||||
1. Read the axis question
|
||||
2. Find evidence (or lack of evidence) in the output
|
||||
3. Assign a score 1-5
|
||||
4. If score < 5, write a one-sentence improvement note citing the gap
|
||||
|
||||
Do NOT average the scores in your head first and then work backwards. Score each axis fresh.
|
||||
|
||||
### Step 3: Produce the Evaluation Report
|
||||
|
||||
Use the template from `templates/evaluation-report.md`. The report must include:
|
||||
|
||||
```
|
||||
- One-line summary
|
||||
- 5-axis scorecard (score + evidence per axis)
|
||||
- Overall score (simple average, rounded to 1 decimal)
|
||||
- 1-3 specific improvements ranked by impact
|
||||
- Self-check: "Would the user agree with this assessment?"
|
||||
```
|
||||
|
||||
### Step 4: Apply the Improvement
|
||||
|
||||
If any axis scored 3 or below:
|
||||
|
||||
1. State what you would do differently
|
||||
2. If the gap is fixable in < 30 seconds (missing link, unclear phrasing), fix it now
|
||||
3. If the gap requires rework, flag it explicitly: "This axis scored [reason] because [evidence]. Re-running with [specific fix] would likely raise it to [score]."
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Example: Good Evaluation (Score 4+)
|
||||
|
||||
```
|
||||
Task: Add retry logic to HTTP client
|
||||
|
||||
Scorecard:
|
||||
Accuracy: 5 — All API calls correct. Verified: retries use
|
||||
exponential backoff. No hallucinated methods.
|
||||
Completeness: 4 — Covered happy path + 3 error cases. Missing:
|
||||
timeout handling for hung connections.
|
||||
Clarity: 5 — Code comments explain backoff formula.
|
||||
PR description links to incident that motivated this.
|
||||
Actionability:5 — Single merge. No follow-up tasks. Tests pass.
|
||||
Conciseness: 4 — 47 lines total. The retry loop could be extracted
|
||||
into a helper to drop ~8 lines.
|
||||
|
||||
Overall: 4.6 — One gap (timeout handling). Fix before merging.
|
||||
```
|
||||
|
||||
### Example: Weak Evaluation (Score 2-3)
|
||||
|
||||
```
|
||||
Task: Add retry logic to HTTP client
|
||||
|
||||
Scorecard:
|
||||
Accuracy: 2 — Used urllib3 which doesn't match our
|
||||
httpx-based codebase. Wrong library.
|
||||
Completeness: 3 — Works for GET. POST/PUT not handled (user
|
||||
said "all HTTP requests").
|
||||
Clarity: 4 — Code is readable. Good variable names.
|
||||
Actionability:2 — "Add tests" mentioned but no test file created.
|
||||
User has to write tests before merging.
|
||||
Conciseness: 3 — 120 lines. The retry config is duplicated in
|
||||
3 places instead of one shared RetryConfig object.
|
||||
|
||||
Overall: 2.8 — Wrong library used. Needs httpx rewrite.
|
||||
Fix accuracy first (switch to httpx), then extend to all
|
||||
HTTP methods, then consolidate config.
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### "Everything is a 5"
|
||||
|
||||
```
|
||||
FAIL: Accuracy: 5 — All good.
|
||||
Completeness: 5 — Everything covered.
|
||||
Clarity: 5 — Clear.
|
||||
```
|
||||
|
||||
No evidence cited. This is self-congratulation, not evaluation. A real 5 requires proving there's nothing to improve.
|
||||
|
||||
### Over-penalizing for scope creep
|
||||
|
||||
```
|
||||
FAIL: Completeness: 2 — Didn't handle WebSocket connections or
|
||||
gRPC streaming (user didn't ask for these)
|
||||
```
|
||||
|
||||
Only evaluate against what the user actually requested, not what you could have additionally built.
|
||||
|
||||
### Using the evaluation to re-litigate
|
||||
|
||||
```
|
||||
FAIL: "As I said earlier, this approach is wrong. Score: 1"
|
||||
```
|
||||
|
||||
The evaluation is about the delivered output, not about re-arguing design decisions that were already made. If the approach was wrong, that should have been caught before delivery.
|
||||
|
||||
### Mixing personal preference with objective gaps
|
||||
|
||||
```
|
||||
FAIL: "Score: 3. I don't like Python decorators."
|
||||
```
|
||||
|
||||
"Don't like" is not evidence. Cite a concrete readability, testability, or correctness concern, or leave the score at 4+.
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Evaluate the output, not the process.** The user cares about what you delivered, not how many iterations you took.
|
||||
- **One improvement per weak axis.** Don't list 5 things for one axis — pick the highest-impact gap.
|
||||
- **Tie improvements to user impact.** "Missing error handling means the user's API call will crash silently" beats "add error handling."
|
||||
- **Be specific about what 'fixed' looks like.** "Re-run with httpx transport configured for retries" beats "fix the library issue."
|
||||
- **Use tool outputs as evidence.** If tests passed, cite them. If lint is clean, cite it. Don't guess — grep for the proof.
|
||||
- **If you can't find any gaps, try harder.** A perfect score across all 5 axes is rare. Ask: "If I were the user, what would annoy me about this output?"
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `agent-eval` — Head-to-head comparison of different coding agents on benchmark tasks
|
||||
- `verification-loop` — Systematic verification of outputs against expected results
|
||||
- `security-review` — Security-focused code review checklist
|
||||
@@ -0,0 +1,87 @@
|
||||
# High-Score Example: Adding Retry Logic
|
||||
|
||||
## Task
|
||||
> Add retry logic with exponential backoff to all HTTP requests in the API client. Max 3 retries, initial delay 1s, max delay 30s.
|
||||
|
||||
## Agent Output
|
||||
|
||||
### Summary
|
||||
Added `httpx.HTTPTransport` with retry configuration to the shared `ApiClient` class. All HTTP methods (GET, POST, PUT, DELETE) now retry on 429, 5xx, and connection errors with exponential backoff. 42 tests pass. Single PR ready to merge.
|
||||
|
||||
### Files Changed
|
||||
- `src/api_client.py` — Added `RetryTransport` wrapper with `httpx.HTTPTransport(retries=3)` and exponential backoff configured via `httpx.Limits`
|
||||
- `tests/test_api_client.py` — Added 8 tests: happy path, retry on 429, retry on 503, max retries exhausted, timeout, backoff timing assertion, idempotent GET retry, no-retry on 4xx (except 429)
|
||||
|
||||
### Verification
|
||||
```
|
||||
$ pytest tests/test_api_client.py -v
|
||||
42 passed in 2.34s
|
||||
|
||||
$ grep -r "RetryTransport" src/
|
||||
src/api_client.py:23: class RetryTransport(httpx.BaseTransport):
|
||||
src/api_client.py:45: self._transport = RetryTransport(...)
|
||||
|
||||
$ python -c "from src.api_client import ApiClient; c = ApiClient(); print(type(c._transport))"
|
||||
<class 'src.api_client.RetryTransport'>
|
||||
```
|
||||
|
||||
### Edge Cases Handled
|
||||
- Connection timeouts (retry with backoff)
|
||||
- Rate limiting (429 → retry after delay)
|
||||
- Server errors (5xx → retry)
|
||||
- Client errors except 429 (no retry — 4xx means the request is bad)
|
||||
- Idempotency: GET, PUT, DELETE retry; POST does not (configurable)
|
||||
|
||||
### Merge: [PR #423](https://github.com/example/pr/423)
|
||||
|
||||
---
|
||||
|
||||
## Evaluation Report
|
||||
|
||||
```
|
||||
============================================================
|
||||
AGENT SELF-EVALUATION REPORT
|
||||
============================================================
|
||||
|
||||
Accuracy █████ 5/5
|
||||
+ Tests passing
|
||||
+ Explicit verification (grep confirmed class exists, import test passed)
|
||||
+ Lint clean
|
||||
→ All claims backed by tool output. No hedging.
|
||||
|
||||
Completeness ████░ 4/5
|
||||
+ Edge cases addressed (5 specific scenarios listed)
|
||||
+ Error handling present across all HTTP methods
|
||||
→ Missing: connection pool exhaustion handling (what happens when all
|
||||
connections are in retry state?) — minor gap, not blocking.
|
||||
|
||||
Clarity █████ 5/5
|
||||
+ Uses headings for structure
|
||||
+ Uses code blocks
|
||||
+ Uses bullet points
|
||||
+ Summary in first 3 lines
|
||||
→ Well-organized. Reader can scan in 10 seconds.
|
||||
|
||||
Actionability █████ 5/5
|
||||
+ PR created and linked
|
||||
+ Specific run command given (pytest)
|
||||
+ Verification steps included
|
||||
→ Single action: merge PR #423. Everything else is done.
|
||||
|
||||
Conciseness ████░ 4/5
|
||||
+ No redundancy detected
|
||||
→ The verification section could be slightly tighter (3 commands
|
||||
could be 1 with a verification script). Minor.
|
||||
|
||||
OVERALL 4.6/5
|
||||
|
||||
TOP IMPROVEMENTS:
|
||||
No axes below 4. Strong output across all dimensions.
|
||||
```
|
||||
|
||||
### Why This Scores Well
|
||||
|
||||
1. **Accuracy pinned to tool output.** Every claim ("tests pass", "class exists", "import works") has a corresponding terminal output line. No "should work" or "probably fine."
|
||||
2. **Completeness is explicit about what's covered AND what's not.** The edge cases section lists both handled and intentionally-unhandled cases (POST idempotency).
|
||||
3. **Actionability is single-step.** The user only needs to merge one PR. No follow-up tasks, no "then configure X."
|
||||
4. **Concision is tight.** The output is ~250 words. The information density is high — every sentence carries weight.
|
||||
@@ -0,0 +1,86 @@
|
||||
# Low-Score Example: Adding Retry Logic
|
||||
|
||||
## Task
|
||||
> Add retry logic with exponential backoff to all HTTP requests in the API client. Max 3 retries, initial delay 1s, max delay 30s.
|
||||
|
||||
## Agent Output
|
||||
|
||||
Here's a retry implementation. I think this should work for most cases.
|
||||
|
||||
I used the urllib3 library since it has built-in retry support. The Retry class handles backoff automatically.
|
||||
|
||||
```python
|
||||
from urllib3.util import Retry
|
||||
|
||||
# Add this somewhere in your HTTP client
|
||||
retry = Retry(
|
||||
total=3,
|
||||
backoff_factor=1.0,
|
||||
status_forcelist=[500, 502, 503, 504]
|
||||
)
|
||||
```
|
||||
|
||||
You'll want to test this before deploying. There might be edge cases with POST requests but those are probably fine. I haven't tested the timeout behavior but it should work.
|
||||
|
||||
Let me know if you need anything else!
|
||||
|
||||
---
|
||||
|
||||
## Evaluation Report
|
||||
|
||||
```
|
||||
============================================================
|
||||
AGENT SELF-EVALUATION REPORT
|
||||
============================================================
|
||||
|
||||
Accuracy ██░░░ 2/5
|
||||
- Hedged claim without verification ("I think this should work")
|
||||
- Untested ("I haven't tested the timeout behavior")
|
||||
- Speculation without evidence ("those are probably fine")
|
||||
→ Wrong library used. Project uses httpx, not urllib3.
|
||||
urllib3.util.Retry is incompatible with httpx.
|
||||
|
||||
Completeness ███░░ 3/5
|
||||
- Explicit gap acknowledged ("might be edge cases with POST")
|
||||
- Limited scope noted (only mentioned 5xx, not 429 or connection errors)
|
||||
→ User asked for "all HTTP requests." Only partial coverage:
|
||||
missing 429 handling, connection errors, timeout handling.
|
||||
|
||||
Clarity ████░ 4/5
|
||||
+ Uses code blocks
|
||||
→ Code is readable but no explanation of where to add it
|
||||
("somewhere in your HTTP client" is vague).
|
||||
|
||||
Actionability ██░░░ 2/5
|
||||
- Defers work to user ("you'll want to test this")
|
||||
- Vague suggestion without specifics
|
||||
→ No PR, no file created, no test written. User has to:
|
||||
1. Figure out where to add the code
|
||||
2. Fix the library mismatch (httpx not urllib3)
|
||||
3. Write tests
|
||||
4. Handle POST idempotency
|
||||
5. Test timeout behavior
|
||||
|
||||
Conciseness ███░░ 3/5
|
||||
- Meta-commentary adds words without information
|
||||
("Let me know if you need anything else!")
|
||||
→ 120 words. Low word count but low information density.
|
||||
Half the text is hedging and disclaimers, not substance.
|
||||
|
||||
OVERALL 2.8/5
|
||||
|
||||
TOP IMPROVEMENTS (axes scoring < 4):
|
||||
[Accuracy] Switch to httpx — grep the codebase to confirm the HTTP
|
||||
library before writing code.
|
||||
[Actionability] Create a PR with the changed file + test file. Run the
|
||||
tests. End with "PR #N ready to merge."
|
||||
[Completeness] List what's covered AND what's not. If POST retry is
|
||||
unsafe, say so explicitly with reasoning.
|
||||
```
|
||||
|
||||
### Why This Scores Poorly
|
||||
|
||||
1. **Accuracy fails at the most basic level** — wrong library. One `grep httpx src/` would have caught this. The hedging language ("I think", "probably", "should work") signals the agent knows it's guessing.
|
||||
2. **Not actionable.** The user received a code snippet and a list of things they need to do. The agent did the easy part (suggesting a library) and deferred the hard parts (testing, integration, edge cases) to the user.
|
||||
3. **Completeness gaps are acknowledged but not fixed.** "Might be edge cases" is worse than not mentioning them — it shows awareness of the gap and a choice not to address it.
|
||||
4. **Information density is low.** 120 words, of which ~60 are hedging/disclaimers/politeness. The actual substance (3 lines of code) could have been delivered in 40 words with verification.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Evaluation Criteria — Detailed Scoring Guide
|
||||
|
||||
This reference provides concrete scoring anchors for each axis. Use it when you're unsure whether a gap merits a 4 vs a 3, or a 2 vs a 1.
|
||||
|
||||
## Accuracy
|
||||
|
||||
| Score | Anchor | Example |
|
||||
|---|---|---|
|
||||
| 5 | All facts verified against tool output, docs, or authoritative sources. No errors. | Configured retry via httpx transport — confirmed in httpx docs. All method names verified with grep against codebase. |
|
||||
| 4 | One minor inaccuracy that doesn't affect correctness. | Correct library, wrong default value for one parameter (claimed 0.5s, docs say 1.0s). |
|
||||
| 3 | One significant factual error, or 3+ minor inaccuracies. | Used `urllib3.Retry` in an httpx codebase. Works in this one case but wrong library. |
|
||||
| 2 | Multiple significant errors. Output would fail if followed. | Claimed "add this to package.json" but project uses pyproject.toml. Two other config claims also wrong. |
|
||||
| 1 | Fundamentally incorrect. Output contradicts itself or known facts. | Code has syntax errors. API endpoint doesn't exist. Claims a function signature that grep disproves. |
|
||||
|
||||
## Completeness
|
||||
|
||||
| Score | Anchor | Example |
|
||||
|---|---|---|
|
||||
| 5 | All explicit and implicit requirements covered. Edge cases handled. Error paths addressed. | User said "add retry to all HTTP requests." GET, POST, PUT, DELETE all covered. Timeout, 429, 5xx all handled. |
|
||||
| 4 | All explicit requirements covered. One implicit requirement missed. | All HTTP methods covered. Forgot to handle connection timeouts (not mentioned but expected). |
|
||||
| 3 | One explicit requirement missed, or 2+ implicit gaps. | User said "add logging too." Retry logic added but no logging. |
|
||||
| 2 | Multiple explicit requirements missed. Output is a partial solution. | Asked for retry + circuit breaker. Only retry implemented. |
|
||||
| 1 | Misses the core request. Delivers something adjacent to what was asked. | Asked for retry logic. Wrote a health check endpoint instead. |
|
||||
|
||||
## Clarity
|
||||
|
||||
| Score | Anchor | Example |
|
||||
|---|---|---|
|
||||
| 5 | Perfectly structured. Jargon explained or avoided. Visual hierarchy helps scanning. No ambiguity. | README with clear sections, code blocks, and a 10-second summary at top. |
|
||||
| 4 | Generally clear. One section could be better organized or one term undefined. | Good structure but `exponential backoff` used without explanation — assumes the reader knows it. |
|
||||
| 3 | Understandable after re-reading. Multiple organizational issues or undefined terms. | The explanation circles the point before getting to it. Several terms used before defined. |
|
||||
| 2 | Confusing in places. Reader would need to ask follow-up questions. | Code works but the PR description doesn't explain why retry was needed or what it fixes. |
|
||||
| 1 | Unintelligible or contradictory. Reader cannot determine what was done or why. | Output is a wall of text with no structure. Conclusions contradict earlier statements. |
|
||||
|
||||
## Actionability
|
||||
|
||||
| Score | Anchor | Example |
|
||||
|---|---|---|
|
||||
| 5 | Single action required. Verification path included. No implicit steps. | "Merge this PR. Tests pass: `42 passed`. Deploy with `./deploy.sh`." |
|
||||
| 4 | Single action required but verification path is implied, not explicit. | "Merge this PR." (Tests exist but weren't cited. User has to check themselves.) |
|
||||
| 3 | Multiple actions required, or one action with unclear next step. | "Review and merge. Then update the config." (Which config? Where? No link or path.) |
|
||||
| 2 | User must figure out how to use the output. Missing critical instructions. | Code written but no test file, no run instructions, no PR created. User has to assemble everything. |
|
||||
| 1 | Output cannot be acted on without significant rework or clarification. | "Here's a design idea." (No code, no file, no PR. User has to start from scratch.) |
|
||||
|
||||
## Conciseness
|
||||
|
||||
| Score | Anchor | Example |
|
||||
|---|---|---|
|
||||
| 5 | Every sentence earns its place. No redundancy. Information density is high. | 30 lines that say what 60 lines would. No repeated points. No filler. |
|
||||
| 4 | Minor redundancy. One paragraph could be tightened. | Good overall but repeats the motivation in both the PR description and code comments. |
|
||||
| 3 | Noticeable redundancy. 20%+ of content could be removed without loss. | Explains the same concept three times (in summary, body, and conclusion). Verbose examples. |
|
||||
| 2 | Significantly bloated. 40%+ of content is filler or repetition. | 200 lines for a task that needed 60. Restates the user's question. Includes irrelevant background. |
|
||||
| 1 | Noise-to-signal ratio is inverted. More filler than substance. | 500-line response to a 2-line question. Most of it is boilerplate, repetition, or irrelevant context. |
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### When the user gave unclear instructions
|
||||
|
||||
If the user's request was ambiguous, do NOT penalize completeness for not reading minds. Instead, note in the evaluation: "User's request was ambiguous about [scope]. I chose interpretation [chosen interpretation]. If they meant [alternative interpretation], this score would drop to [score]."
|
||||
|
||||
### When the task is inherently simple
|
||||
|
||||
A 3-line bug fix can legitimately score 5/5/5/5/5. The rubric scales with complexity — a simple task done perfectly IS a 5.0. Don't invent gaps to justify lower scores.
|
||||
|
||||
### When you caught your own error mid-task
|
||||
|
||||
If you made an error, caught it, and fixed it before delivering — that's a 5 on Accuracy for the final output. The evaluation is about what the user received, not your internal process. Note the self-correction as evidence of thoroughness, not as a penalty.
|
||||
|
||||
### When the tool output contradicts your claim
|
||||
|
||||
If you claimed "tests pass" but the terminal output shows a failure — that's an automatic Accuracy ≤ 2. Tool output is ground truth. Claims without verification are the most common source of low accuracy scores.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Hook Integration for Session-Stop Self-Evaluation
|
||||
|
||||
Add this hook to `hooks/hooks.json` to remind the agent to self-evaluate at the end of every session (the hook echoes a reminder; it does not run the evaluator automatically):
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "echo '[Self-Eval] Session complete. Consider running agent-self-evaluation to rate your output.'"
|
||||
}
|
||||
],
|
||||
"description": "Remind agent to self-evaluate at session end"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`Stop` events do not require a `matcher` field (it is optional for `Stop`, `Notification`, `UserPromptSubmit`, and `SubagentStop` per `scripts/ci/validate-hooks.js`). If omitted, the hook object only needs `hooks` and metadata such as `description`.
|
||||
|
||||
## Integration with the Python Evaluator
|
||||
|
||||
The `scripts/evaluate.py` script can be used as a standalone tool:
|
||||
|
||||
```bash
|
||||
# Pipe agent output directly
|
||||
echo "Your agent response here" | python3 skills/agent-self-evaluation/scripts/evaluate.py
|
||||
|
||||
# From files
|
||||
python3 skills/agent-self-evaluation/scripts/evaluate.py --task task.txt --output response.txt
|
||||
```
|
||||
|
||||
To integrate it into hooks, capture the last agent output to a file first, then run the evaluator. For lightweight reminders after shell-based verification, use a simple supported matcher string:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "echo '[Self-Eval] If this command completed verification for a non-trivial task, consider running agent-self-evaluation.'"
|
||||
}
|
||||
],
|
||||
"description": "Remind agent to self-evaluate after shell verification"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This avoids documenting unsupported command-expression matcher syntax. If your harness supports command-level matcher expressions, prefer a word-boundary regex such as `\b(pytest|npm test|go test)\b` rather than a broad `test` substring.
|
||||
|
||||
These hooks are opt-in. Add them to your local `hooks/hooks.json` if you want automated evaluation prompts.
|
||||
|
||||
## Manual Usage (Recommended)
|
||||
|
||||
The most reliable approach is manual invocation — the agent runs self-evaluation as part of its workflow when the `agent-self-evaluation` skill is active, without requiring hook configuration. The skill's "When to Activate" section already covers trigger conditions (multi-file changes, debugging sessions, design documents).
|
||||
+408
@@ -0,0 +1,408 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Standalone agent output evaluator using the 5-axis rubric.
|
||||
|
||||
Reads a task description and agent output from stdin or files,
|
||||
scores each axis, and prints a structured evaluation report.
|
||||
|
||||
Usage:
|
||||
# Pipe output directly
|
||||
echo "Task: Add retry logic" | evaluate.py --output response.txt
|
||||
|
||||
# From files
|
||||
evaluate.py --task task.txt --output response.txt
|
||||
|
||||
# Interactive (reads task from prompt, output from stdin)
|
||||
evaluate.py --interactive
|
||||
|
||||
The evaluator uses keyword heuristics + structural checks as a first pass.
|
||||
For production use, pair with an LLM judge for semantic understanding.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
# Tunable thresholds for evaluation heuristics
|
||||
WALL_OF_TEXT_WORDS = 200
|
||||
SUMMARY_CHECK_WORDS = 300
|
||||
SUMMARY_CHECK_FIRST_N = 100
|
||||
TASK_OUTPUT_RATIO_HIGH = 15
|
||||
TASK_OUTPUT_RATIO_MEDIUM = 8
|
||||
|
||||
|
||||
@dataclass
|
||||
class AxisScore:
|
||||
name: str
|
||||
score: int
|
||||
evidence: list[str] = field(default_factory=list)
|
||||
improvement: Optional[str] = None
|
||||
|
||||
|
||||
def count_words(text: str) -> int:
|
||||
return len(text.split())
|
||||
|
||||
|
||||
def check_accuracy(text: str) -> AxisScore:
|
||||
"""Check for verifiable claims, tool output references, error signs."""
|
||||
evidence = []
|
||||
deductions = 0
|
||||
score = 5
|
||||
|
||||
# Positive signals: verified claims
|
||||
verified_patterns = [
|
||||
(r"(?i)(tests?\s+pass|all\s+tests?\s+passing|\d+\s+passed)", "Tests passing"),
|
||||
(r"(?i)(exit\s+code\s*[:=]?\s*0|exited\s+with\s+0)", "Clean exit code"),
|
||||
(r"(?i)(lint.*clean|no\s+lint\s+errors|0\s+errors)", "Lint clean"),
|
||||
(r"(?i)(verified|confirmed|validated)\s+(with|against|using|by)", "Explicit verification"),
|
||||
(r"(?i)(grep|rg)\s+.*\b(found|matched|returned)", "Grep confirmed"),
|
||||
]
|
||||
for pattern, label in verified_patterns:
|
||||
if re.search(pattern, text):
|
||||
evidence.append(f"+ {label}")
|
||||
|
||||
# Negative signals: unverified claims
|
||||
danger_patterns = [
|
||||
(r"(?i)(should\s+work|probably\s+fine|should\s+be\s+ok)", "Hedged claim without verification"),
|
||||
(r"(?i)(I\s+think|I\s+believe|I\s+assume|might\s+be)", "Speculation without evidence"),
|
||||
(r"(?i)(untested|not\s+tested|haven'?t\s+tested)", "Explicitly untested"),
|
||||
(r"(?i)(TODO|FIXME|HACK|WORKAROUND)", "Unresolved TODO/FIXME"),
|
||||
]
|
||||
for pattern, label in danger_patterns:
|
||||
if re.search(pattern, text):
|
||||
deductions += 1
|
||||
evidence.append(f"- {label}")
|
||||
|
||||
if deductions >= 3:
|
||||
score = 2
|
||||
elif deductions == 2:
|
||||
score = 3
|
||||
elif deductions == 1:
|
||||
score = 4
|
||||
|
||||
if not evidence:
|
||||
evidence.append("No verification signals detected — score assumes correctness")
|
||||
|
||||
result = AxisScore(name="Accuracy", score=score, evidence=evidence)
|
||||
if score < 5:
|
||||
result.improvement = "Cite specific tool outputs (test results, exit codes, grep findings) to back claims"
|
||||
return result
|
||||
|
||||
|
||||
def check_completeness(text: str) -> AxisScore:
|
||||
"""Check for requirement coverage, edge cases, error handling."""
|
||||
evidence = []
|
||||
score = 5
|
||||
|
||||
# Positive signals
|
||||
completeness_signals = [
|
||||
(r"(?i)(edge\s*cases?|corner\s*cases?)", "Edge cases addressed"),
|
||||
(r"(?i)(error\s*handling|exception\s*handling|try/except|try\s*{)", "Error handling present"),
|
||||
(r"(?i)(all\s+\w+\s+(methods|endpoints|routes))", "Full coverage claimed"),
|
||||
(r"(?i)(verification|verified\s+that|confirmed\s+that)", "Verification step present"),
|
||||
]
|
||||
for pattern, label in completeness_signals:
|
||||
if re.search(pattern, text):
|
||||
evidence.append(f"+ {label}")
|
||||
|
||||
# Gaps
|
||||
gap_signals = [
|
||||
(r"(?i)(not\s+covered|not\s+handled|out\s+of\s+scope)", "Explicit gap acknowledged"),
|
||||
(r"(?i)(only\s+(works|handles|supports)\s+\w+)", "Limited scope noted"),
|
||||
(r"(?i)(assume[sd]?\s+that|assuming\s+the)", "Assumption without verification"),
|
||||
]
|
||||
deductions = 0
|
||||
for pattern, label in gap_signals:
|
||||
if re.search(pattern, text):
|
||||
deductions += 1
|
||||
evidence.append(f"- {label}")
|
||||
|
||||
if deductions >= 2:
|
||||
score = 3
|
||||
elif deductions == 1:
|
||||
score = 4
|
||||
|
||||
if not evidence:
|
||||
evidence.append("No completeness signals — unable to assess coverage")
|
||||
|
||||
result = AxisScore(name="Completeness", score=score, evidence=evidence)
|
||||
if score < 5:
|
||||
result.improvement = "List what was covered AND what was intentionally excluded, with reasoning"
|
||||
return result
|
||||
|
||||
|
||||
def _check_jargon(text: str) -> tuple[int, list[str]]:
|
||||
"""Return clarity deductions for unexplained domain jargon."""
|
||||
jargon = [
|
||||
(r"\b(idempotent|race condition|deadlock|thundering herd)\b", "concurrency"),
|
||||
(r"\b(exponential backoff|circuit breaker|bulkhead)\b", "resilience"),
|
||||
(r"\b(ACID|CAP|eventual consistency|linearizability)\b", "database theory"),
|
||||
]
|
||||
explanation_pattern = r"(?i)({domain}|means|refers to|i\.e\.|in other words)"
|
||||
for pattern, domain in jargon:
|
||||
has_term = re.search(pattern, text, re.IGNORECASE)
|
||||
explains_term = re.search(explanation_pattern.format(domain=domain), text)
|
||||
if has_term and not explains_term:
|
||||
return 1, [f"- Domain term used without explanation ({domain})"]
|
||||
return 0, []
|
||||
|
||||
|
||||
def _check_summary(text: str) -> tuple[int, list[str]]:
|
||||
"""Return clarity deduction when long output lacks an early summary."""
|
||||
summary_terms = ["summary", "tldr", "overview", "in short"]
|
||||
has_early_summary = any(term in ' '.join(text.split()[:SUMMARY_CHECK_FIRST_N]).lower() for term in summary_terms)
|
||||
if not has_early_summary and count_words(text) > SUMMARY_CHECK_WORDS:
|
||||
return 1, ["- No summary/TLDR in first 100 words (text is 300+ words)"]
|
||||
return 0, []
|
||||
|
||||
|
||||
def check_clarity(text: str) -> AxisScore:
|
||||
"""Check for structure, readability, jargon handling."""
|
||||
evidence = []
|
||||
deductions = 0
|
||||
|
||||
if re.search(r"^#{1,3}\s+", text, re.MULTILINE):
|
||||
evidence.append("+ Uses headings for structure")
|
||||
if re.search(r"```", text):
|
||||
evidence.append("+ Uses code blocks")
|
||||
if re.search(r"^\s*[-*]\s+", text, re.MULTILINE):
|
||||
evidence.append("+ Uses bullet points")
|
||||
|
||||
for paragraph in [p for p in text.split("\n\n") if p.strip()]:
|
||||
if count_words(paragraph) > WALL_OF_TEXT_WORDS:
|
||||
deductions += 1
|
||||
evidence.append("- Wall-of-text paragraph (>200 words without break)")
|
||||
break
|
||||
|
||||
jargon_deductions, jargon_evidence = _check_jargon(text)
|
||||
summary_deductions, summary_evidence = _check_summary(text)
|
||||
deductions += jargon_deductions + summary_deductions
|
||||
evidence.extend(jargon_evidence + summary_evidence)
|
||||
|
||||
if deductions >= 3:
|
||||
score = 2
|
||||
elif deductions == 2:
|
||||
score = 3
|
||||
elif deductions == 1:
|
||||
score = 4
|
||||
else:
|
||||
score = 5
|
||||
|
||||
if not evidence:
|
||||
evidence.append("+ Well-structured with no clarity issues detected")
|
||||
|
||||
result = AxisScore(name="Clarity", score=score, evidence=evidence)
|
||||
if score < 5:
|
||||
result.improvement = "Add headings, break long paragraphs, define domain terms on first use"
|
||||
return result
|
||||
|
||||
|
||||
def check_actionability(text: str) -> AxisScore:
|
||||
"""Check if the user can act on the output immediately."""
|
||||
evidence = []
|
||||
score = 5
|
||||
deductions = 0
|
||||
|
||||
# Positive signals
|
||||
actionable_signals = [
|
||||
(r"(?i)(merge|PR|pull request).*?(created|ready|open)", "PR created"),
|
||||
(r"(?i)(run|execute)\s+[`\"']?[\w./-]+", "Specific run command given"),
|
||||
(r"(?i)(next\s+steps?|follow[- ]up|what\s+to\s+do)", "Next steps provided"),
|
||||
(r"(?i)(file\s+(created|written|modified|updated)\s+at)", "File path specified"),
|
||||
]
|
||||
for pattern, label in actionable_signals:
|
||||
if re.search(pattern, text):
|
||||
evidence.append(f"+ {label}")
|
||||
|
||||
# Negative signals
|
||||
vague_signals = [
|
||||
(r"(?i)(you\s+(should|could|might\s+want\s+to))\s+\w+", "Vague suggestion without specifics"),
|
||||
(r"(?i)(consider|maybe|perhaps)\s+\w+ing", "Non-committal suggestion"),
|
||||
(r"(?i)(figure\s+out|look\s+into|investigate)\s", "Defers work to user"),
|
||||
]
|
||||
for pattern, label in vague_signals:
|
||||
if re.search(pattern, text):
|
||||
deductions += 1
|
||||
evidence.append(f"- {label}")
|
||||
|
||||
if deductions >= 3:
|
||||
score = 2
|
||||
elif deductions == 2:
|
||||
score = 3
|
||||
elif deductions == 1:
|
||||
score = 4
|
||||
|
||||
if not evidence:
|
||||
evidence.append("No actionability signals — user may need to ask 'what now?'")
|
||||
|
||||
result = AxisScore(name="Actionability", score=score, evidence=evidence)
|
||||
if score < 5:
|
||||
result.improvement = "End with a single clear action: 'Merge this PR', 'Run ./deploy.sh', or 'Review the 3 changed files'"
|
||||
return result
|
||||
|
||||
|
||||
def check_conciseness(text: str, task: Optional[str] = None) -> AxisScore:
|
||||
"""Check for redundancy, filler, information density."""
|
||||
evidence = []
|
||||
score = 5
|
||||
wc = count_words(text)
|
||||
|
||||
# Heuristic: task-to-output ratio
|
||||
if task:
|
||||
task_wc = count_words(task)
|
||||
ratio = wc / max(task_wc, 1)
|
||||
if ratio > TASK_OUTPUT_RATIO_HIGH:
|
||||
evidence.append(f"- Output is {ratio:.0f}x longer than task description (high ratio)")
|
||||
score = min(score, 3)
|
||||
elif ratio > TASK_OUTPUT_RATIO_MEDIUM:
|
||||
evidence.append(f"- Output is {ratio:.0f}x longer than task description")
|
||||
score = min(score, 4)
|
||||
|
||||
# Redundancy signals
|
||||
redundancy_checks = [
|
||||
(r"(?i)(as\s+(I|we)\s+(mentioned|said|noted|discussed)\s+(earlier|above|before))",
|
||||
"Refers back to earlier statement (possible repetition)"),
|
||||
(r"(?i)(to\s+summarize|in\s+summary|in\s+conclusion|to\s+conclude)",
|
||||
"Has explicit summary (good if needed, flag if redundant)"),
|
||||
(r"(?i)(let\s+me\s+(explain|break\s+this\s+down|walk\s+you\s+through))",
|
||||
"Meta-commentary adds words without information"),
|
||||
]
|
||||
redundant_count = 0
|
||||
for pattern, label in redundancy_checks:
|
||||
matches = re.findall(pattern, text)
|
||||
if len(matches) > 2:
|
||||
redundant_count += 1
|
||||
evidence.append(f"- '{label}' appears {len(matches)} times")
|
||||
|
||||
if redundant_count >= 2:
|
||||
score = min(score, 3)
|
||||
elif redundant_count == 1:
|
||||
score = min(score, 4)
|
||||
|
||||
if not evidence and score == 5:
|
||||
evidence.append("+ No redundancy detected. Information density appears good.")
|
||||
|
||||
result = AxisScore(name="Conciseness", score=score, evidence=evidence)
|
||||
if score < 5:
|
||||
result.improvement = "Cut meta-commentary, remove repeated points, trim examples to one representative case"
|
||||
return result
|
||||
|
||||
|
||||
def evaluate(task: Optional[str], output: str) -> list[AxisScore]:
|
||||
"""Run all 5 axis checks and return scored results."""
|
||||
return [
|
||||
check_accuracy(output),
|
||||
check_completeness(output),
|
||||
check_clarity(output),
|
||||
check_actionability(output),
|
||||
check_conciseness(output, task),
|
||||
]
|
||||
|
||||
|
||||
def format_report(scores: list[AxisScore]) -> str:
|
||||
"""Format scores into a readable evaluation report."""
|
||||
avg = sum(s.score for s in scores) / len(scores)
|
||||
lines = []
|
||||
lines.append("=" * 60)
|
||||
lines.append("AGENT SELF-EVALUATION REPORT")
|
||||
lines.append("=" * 60)
|
||||
lines.append(f"Summary: Overall score {avg:.1f}/5 across 5 quality axes.")
|
||||
lines.append("")
|
||||
|
||||
for s in scores:
|
||||
bar = "█" * s.score + "░" * (5 - s.score)
|
||||
lines.append(f" {s.name:<15} {bar} {s.score}/5")
|
||||
lines.extend(f" {e}" for e in s.evidence)
|
||||
if s.improvement:
|
||||
lines.append(f" → {s.improvement}")
|
||||
lines.append("")
|
||||
|
||||
lines.append(f" {'OVERALL':<15} {avg:.1f}/5")
|
||||
lines.append("")
|
||||
|
||||
# Critical issues (axes ≤ 2)
|
||||
critical = [(s, s.improvement or "No improvement suggested") for s in scores if s.score <= 2]
|
||||
lines.append("CRITICAL ISSUES (axes ≤ 2):")
|
||||
if critical:
|
||||
for s, imp in critical:
|
||||
lines.append(f" [{s.name}] Score {s.score}/5 — {imp}")
|
||||
else:
|
||||
lines.append(" None")
|
||||
|
||||
lines.append("")
|
||||
lines.append("Self-check: Would the user agree with this assessment? [Yes/No + brief justification]")
|
||||
lines.append("")
|
||||
|
||||
# Top improvements (axes scoring < 4, ranked by impact)
|
||||
improvements = [(s, s.improvement) for s in scores if s.improvement and s.score < 4]
|
||||
lines.append("TOP IMPROVEMENTS:")
|
||||
if improvements:
|
||||
for i, (s, imp) in enumerate(sorted(improvements, key=lambda x: x[0].score), 1):
|
||||
lines.append(f" {i}. [{s.name}] {imp}")
|
||||
else:
|
||||
lines.append(" No axes below 4. Strong output across all dimensions.")
|
||||
|
||||
lines.append("")
|
||||
|
||||
# Verdict
|
||||
min_score = min(s.score for s in scores)
|
||||
if min_score <= 2:
|
||||
verdict = f"Redo with specific fixes. Weakest axis: {min(scores, key=lambda s: s.score).name} ({min_score}/5)."
|
||||
elif any(s.score <= 3 for s in scores):
|
||||
weak = [s.name for s in scores if s.score <= 3]
|
||||
verdict = f"Fix {'/'.join(weak)} issues, then deliver."
|
||||
elif avg >= 4.5:
|
||||
verdict = "Deliver as-is. No changes needed."
|
||||
else:
|
||||
verdict = "Deliver as-is. Minor improvements noted above."
|
||||
lines.append(f"VERDICT: {verdict}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _read_file_or_text(path: Optional[str], *, required: bool = False) -> Optional[str]:
|
||||
"""Read a file path or return inline text when allowed."""
|
||||
if path is None:
|
||||
return None
|
||||
try:
|
||||
with open(path) as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
if required:
|
||||
print(f"Error: output file '{path}' not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return path
|
||||
|
||||
|
||||
def _read_input(args: argparse.Namespace) -> tuple[Optional[str], str]:
|
||||
"""Read task and output for interactive, file, or pipe mode."""
|
||||
if args.interactive:
|
||||
task = input("Task description: ").strip()
|
||||
print("Paste agent output (Ctrl+D to finish):")
|
||||
return task, sys.stdin.read()
|
||||
if args.output:
|
||||
return _read_file_or_text(args.task), _read_file_or_text(args.output, required=True) or ""
|
||||
return _read_file_or_text(args.task), sys.stdin.read()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Evaluate agent output against the 5-axis rubric"
|
||||
)
|
||||
parser.add_argument("--task", help="Task description (file path or inline text)")
|
||||
parser.add_argument("--output", help="Agent output to evaluate (file path)")
|
||||
parser.add_argument("--interactive", action="store_true", help="Prompt for task and read output from stdin")
|
||||
args = parser.parse_args()
|
||||
|
||||
task, output = _read_input(args)
|
||||
if not output:
|
||||
print("Error: no output to evaluate", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
scores = evaluate(task, output)
|
||||
print(format_report(scores))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,86 @@
|
||||
# Agent Self-Evaluation Report Template
|
||||
|
||||
Copy this template and fill in after completing a task. The format matches `scripts/evaluate.py` output.
|
||||
|
||||
```
|
||||
============================================================
|
||||
AGENT SELF-EVALUATION REPORT
|
||||
============================================================
|
||||
Summary: Overall score X.X/5 across 5 quality axes.
|
||||
|
||||
Accuracy █████ 5/5 or ███░░ 3/5
|
||||
+ [Evidence: passing tests, verified claims]
|
||||
- [Gaps: unverified claims, hedging language]
|
||||
→ [Improvement if score < 5]
|
||||
|
||||
Completeness █████ 5/5
|
||||
+ [What's covered: all requirements + edge cases]
|
||||
- [What's missing: explicitly acknowledge gaps]
|
||||
→ [Improvement if score < 5]
|
||||
|
||||
Clarity █████ 5/5
|
||||
+ [Structure: headings, code blocks, bullet points]
|
||||
- [Issues: undefined terms, wall of text, no summary]
|
||||
→ [Improvement if score < 5]
|
||||
|
||||
Actionability █████ 5/5
|
||||
+ [User can: merge PR, run command, review file]
|
||||
- [Blockers: missing steps, vague suggestions]
|
||||
→ [Improvement if score < 5]
|
||||
|
||||
Conciseness █████ 5/5
|
||||
+ [Tight: no repetition, high information density]
|
||||
- [Bloat: filler, meta-commentary, repeated points]
|
||||
→ [Improvement if score < 5]
|
||||
|
||||
OVERALL X.X/5
|
||||
|
||||
CRITICAL ISSUES (axes ≤ 2):
|
||||
[Axis] Score N/5 — specific fix needed
|
||||
(or "None" if no axis ≤ 2)
|
||||
|
||||
Self-check: Would the user agree with this assessment? [Yes/No + brief justification]
|
||||
|
||||
TOP IMPROVEMENTS:
|
||||
1. [Highest impact fix]
|
||||
2. [Second highest]
|
||||
(Only list axes scoring < 4, ranked by user impact)
|
||||
|
||||
VERDICT: [Deliver as-is / Fix N issues then deliver / Redo from scratch]
|
||||
```
|
||||
|
||||
## Quick Reference: Scoring Triggers
|
||||
|
||||
| If you see this... | Accuracy | Completeness | Clarity | Actionability | Conciseness |
|
||||
|---|---|---|---|---|---|
|
||||
| "should work" / "probably fine" | ≤4 | — | — | — | — |
|
||||
| "I think" / "I believe" | ≤4 | — | — | — | — |
|
||||
| No test output cited | ≤4 | — | — | — | — |
|
||||
| "TODO" / "FIXME" left behind | ≤3 | ≤3 | — | ≤3 | — |
|
||||
| Missing error handling | — | ≤3 | — | — | — |
|
||||
| Only happy path covered | — | ≤3 | — | — | — |
|
||||
| Wall-of-text paragraph (>200 words) | — | — | ≤3 | — | — |
|
||||
| No headings or structure | — | — | ≤3 | — | — |
|
||||
| "You should..." without specifics | — | — | — | ≤3 | — |
|
||||
| No PR or file created | — | — | — | ≤3 | — |
|
||||
| User needs to figure out next step | — | — | — | ≤2 | — |
|
||||
| Repeated points (3+ times) | — | — | — | — | ≤3 |
|
||||
| "Let me explain..." / "To summarize..." x3+ | — | — | — | — | ≤3 |
|
||||
| Output >15x longer than task | — | — | — | — | ≤3 |
|
||||
|
||||
## When to Skip
|
||||
|
||||
Skip the evaluation if:
|
||||
- Task was a single tool call (e.g., "read this file" — nothing to evaluate)
|
||||
- User explicitly says "don't evaluate" or "just do it"
|
||||
- Task is purely conversational (greeting, small talk)
|
||||
- You're mid-workflow and the user will judge the final output, not intermediate steps
|
||||
|
||||
## Post-Evaluation Actions
|
||||
|
||||
| Overall Score | What to do |
|
||||
|---|---|
|
||||
| ≥4.5 | Deliver as-is. No changes needed. |
|
||||
| 3.5–4.4 | Flag top improvement but deliver. Fix if <30 seconds. |
|
||||
| 2.5–3.4 | State what you'd change. Ask user: "Should I redo [axis] or deliver as-is?" |
|
||||
| <2.5 | Don't deliver. Say: "This scored [score] because [evidence]. Let me redo this with [specific fix]." Then redo. |
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: agentic-engineering
|
||||
description: Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Agentic Engineering
|
||||
|
||||
Use this skill for engineering workflows where AI agents perform most implementation work and humans enforce quality and risk controls.
|
||||
|
||||
## Operating Principles
|
||||
|
||||
1. Define completion criteria before execution.
|
||||
2. Decompose work into agent-sized units.
|
||||
3. Route model tiers by task complexity.
|
||||
4. Measure with evals and regression checks.
|
||||
|
||||
## Eval-First Loop
|
||||
|
||||
1. Define capability eval and regression eval.
|
||||
2. Run baseline and capture failure signatures.
|
||||
3. Execute implementation.
|
||||
4. Re-run evals and compare deltas.
|
||||
|
||||
## Task Decomposition
|
||||
|
||||
Apply the 15-minute unit rule:
|
||||
- each unit should be independently verifiable
|
||||
- each unit should have a single dominant risk
|
||||
- each unit should expose a clear done condition
|
||||
|
||||
## Model Routing
|
||||
|
||||
- Haiku: classification, boilerplate transforms, narrow edits
|
||||
- Sonnet: implementation and refactors
|
||||
- Opus: architecture, root-cause analysis, multi-file invariants
|
||||
|
||||
## Session Strategy
|
||||
|
||||
- Continue session for closely-coupled units.
|
||||
- Start fresh session after major phase transitions.
|
||||
- Compact after milestone completion, not during active debugging.
|
||||
|
||||
## Review Focus for AI-Generated Code
|
||||
|
||||
Prioritize:
|
||||
- invariants and edge cases
|
||||
- error boundaries
|
||||
- security and auth assumptions
|
||||
- hidden coupling and rollout risk
|
||||
|
||||
Do not waste review cycles on style-only disagreements when automated format/lint already enforce style.
|
||||
|
||||
## Cost Discipline
|
||||
|
||||
Track per task:
|
||||
- model
|
||||
- token estimate
|
||||
- retries
|
||||
- wall-clock time
|
||||
- success/failure
|
||||
|
||||
Escalate model tier only when lower tier fails with a clear reasoning gap.
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: ai-first-engineering
|
||||
description: Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# AI-First Engineering
|
||||
|
||||
Use this skill when designing process, reviews, and architecture for teams shipping with AI-assisted code generation.
|
||||
|
||||
## Process Shifts
|
||||
|
||||
1. Planning quality matters more than typing speed.
|
||||
2. Eval coverage matters more than anecdotal confidence.
|
||||
3. Review focus shifts from syntax to system behavior.
|
||||
|
||||
## Architecture Requirements
|
||||
|
||||
Prefer architectures that are agent-friendly:
|
||||
- explicit boundaries
|
||||
- stable contracts
|
||||
- typed interfaces
|
||||
- deterministic tests
|
||||
|
||||
Avoid implicit behavior spread across hidden conventions.
|
||||
|
||||
## Code Review in AI-First Teams
|
||||
|
||||
Review for:
|
||||
- behavior regressions
|
||||
- security assumptions
|
||||
- data integrity
|
||||
- failure handling
|
||||
- rollout safety
|
||||
|
||||
Minimize time spent on style issues already covered by automation.
|
||||
|
||||
## Hiring and Evaluation Signals
|
||||
|
||||
Strong AI-first engineers:
|
||||
- decompose ambiguous work cleanly
|
||||
- define measurable acceptance criteria
|
||||
- produce high-signal prompts and evals
|
||||
- enforce risk controls under delivery pressure
|
||||
|
||||
## Testing Standard
|
||||
|
||||
Raise testing bar for generated code:
|
||||
- required regression coverage for touched domains
|
||||
- explicit edge-case assertions
|
||||
- integration checks for interface boundaries
|
||||
@@ -0,0 +1,340 @@
|
||||
---
|
||||
name: android-clean-architecture
|
||||
description: Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Android Clean Architecture
|
||||
|
||||
Clean Architecture patterns for Android and KMP projects. Covers module boundaries, dependency inversion, UseCase/Repository patterns, and data layer design with Room, SQLDelight, and Ktor.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Structuring Android or KMP project modules
|
||||
- Implementing UseCases, Repositories, or DataSources
|
||||
- Designing data flow between layers (domain, data, presentation)
|
||||
- Setting up dependency injection with Koin or Hilt
|
||||
- Working with Room, SQLDelight, or Ktor in a layered architecture
|
||||
|
||||
## Module Structure
|
||||
|
||||
### Recommended Layout
|
||||
|
||||
```
|
||||
project/
|
||||
├── app/ # Android entry point, DI wiring, Application class
|
||||
├── core/ # Shared utilities, base classes, error types
|
||||
├── domain/ # UseCases, domain models, repository interfaces (pure Kotlin)
|
||||
├── data/ # Repository implementations, DataSources, DB, network
|
||||
├── presentation/ # Screens, ViewModels, UI models, navigation
|
||||
├── design-system/ # Reusable Compose components, theme, typography
|
||||
└── feature/ # Feature modules (optional, for larger projects)
|
||||
├── auth/
|
||||
├── settings/
|
||||
└── profile/
|
||||
```
|
||||
|
||||
### Dependency Rules
|
||||
|
||||
```
|
||||
app → presentation, domain, data, core
|
||||
presentation → domain, design-system, core
|
||||
data → domain, core
|
||||
domain → core (or no dependencies)
|
||||
core → (nothing)
|
||||
```
|
||||
|
||||
**Critical**: `domain` must NEVER depend on `data`, `presentation`, or any framework. It contains pure Kotlin only.
|
||||
|
||||
## Domain Layer
|
||||
|
||||
### UseCase Pattern
|
||||
|
||||
Each UseCase represents one business operation. Use `operator fun invoke` for clean call sites:
|
||||
|
||||
```kotlin
|
||||
class GetItemsByCategoryUseCase(
|
||||
private val repository: ItemRepository
|
||||
) {
|
||||
suspend operator fun invoke(category: String): Result<List<Item>> {
|
||||
return repository.getItemsByCategory(category)
|
||||
}
|
||||
}
|
||||
|
||||
// Flow-based UseCase for reactive streams
|
||||
class ObserveUserProgressUseCase(
|
||||
private val repository: UserRepository
|
||||
) {
|
||||
operator fun invoke(userId: String): Flow<UserProgress> {
|
||||
return repository.observeProgress(userId)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Domain Models
|
||||
|
||||
Domain models are plain Kotlin data classes — no framework annotations:
|
||||
|
||||
```kotlin
|
||||
data class Item(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val tags: List<String>,
|
||||
val status: Status,
|
||||
val category: String
|
||||
)
|
||||
|
||||
enum class Status { DRAFT, ACTIVE, ARCHIVED }
|
||||
```
|
||||
|
||||
### Repository Interfaces
|
||||
|
||||
Defined in domain, implemented in data:
|
||||
|
||||
```kotlin
|
||||
interface ItemRepository {
|
||||
suspend fun getItemsByCategory(category: String): Result<List<Item>>
|
||||
suspend fun saveItem(item: Item): Result<Unit>
|
||||
fun observeItems(): Flow<List<Item>>
|
||||
}
|
||||
```
|
||||
|
||||
## Data Layer
|
||||
|
||||
### Repository Implementation
|
||||
|
||||
Coordinates between local and remote data sources:
|
||||
|
||||
```kotlin
|
||||
class ItemRepositoryImpl(
|
||||
private val localDataSource: ItemLocalDataSource,
|
||||
private val remoteDataSource: ItemRemoteDataSource
|
||||
) : ItemRepository {
|
||||
|
||||
override suspend fun getItemsByCategory(category: String): Result<List<Item>> {
|
||||
return runCatching {
|
||||
val remote = remoteDataSource.fetchItems(category)
|
||||
localDataSource.insertItems(remote.map { it.toEntity() })
|
||||
localDataSource.getItemsByCategory(category).map { it.toDomain() }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveItem(item: Item): Result<Unit> {
|
||||
return runCatching {
|
||||
localDataSource.insertItems(listOf(item.toEntity()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeItems(): Flow<List<Item>> {
|
||||
return localDataSource.observeAll().map { entities ->
|
||||
entities.map { it.toDomain() }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mapper Pattern
|
||||
|
||||
Keep mappers as extension functions near the data models:
|
||||
|
||||
```kotlin
|
||||
// In data layer
|
||||
fun ItemEntity.toDomain() = Item(
|
||||
id = id,
|
||||
title = title,
|
||||
description = description,
|
||||
tags = tags.split("|"),
|
||||
status = Status.valueOf(status),
|
||||
category = category
|
||||
)
|
||||
|
||||
fun ItemDto.toEntity() = ItemEntity(
|
||||
id = id,
|
||||
title = title,
|
||||
description = description,
|
||||
tags = tags.joinToString("|"),
|
||||
status = status,
|
||||
category = category
|
||||
)
|
||||
```
|
||||
|
||||
### Room Database (Android)
|
||||
|
||||
```kotlin
|
||||
@Entity(tableName = "items")
|
||||
data class ItemEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val tags: String,
|
||||
val status: String,
|
||||
val category: String
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface ItemDao {
|
||||
@Query("SELECT * FROM items WHERE category = :category")
|
||||
suspend fun getByCategory(category: String): List<ItemEntity>
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(items: List<ItemEntity>)
|
||||
|
||||
@Query("SELECT * FROM items")
|
||||
fun observeAll(): Flow<List<ItemEntity>>
|
||||
}
|
||||
```
|
||||
|
||||
### SQLDelight (KMP)
|
||||
|
||||
```sql
|
||||
-- Item.sq
|
||||
CREATE TABLE ItemEntity (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
tags TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
category TEXT NOT NULL
|
||||
);
|
||||
|
||||
getByCategory:
|
||||
SELECT * FROM ItemEntity WHERE category = ?;
|
||||
|
||||
upsert:
|
||||
INSERT OR REPLACE INTO ItemEntity (id, title, description, tags, status, category)
|
||||
VALUES (?, ?, ?, ?, ?, ?);
|
||||
|
||||
observeAll:
|
||||
SELECT * FROM ItemEntity;
|
||||
```
|
||||
|
||||
### Ktor Network Client (KMP)
|
||||
|
||||
```kotlin
|
||||
class ItemRemoteDataSource(private val client: HttpClient) {
|
||||
|
||||
suspend fun fetchItems(category: String): List<ItemDto> {
|
||||
return client.get("api/items") {
|
||||
parameter("category", category)
|
||||
}.body()
|
||||
}
|
||||
}
|
||||
|
||||
// HttpClient setup with content negotiation
|
||||
val httpClient = HttpClient {
|
||||
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
|
||||
install(Logging) { level = LogLevel.HEADERS }
|
||||
defaultRequest { url("https://api.example.com/") }
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
### Koin (KMP-friendly)
|
||||
|
||||
```kotlin
|
||||
// Domain module
|
||||
val domainModule = module {
|
||||
factory { GetItemsByCategoryUseCase(get()) }
|
||||
factory { ObserveUserProgressUseCase(get()) }
|
||||
}
|
||||
|
||||
// Data module
|
||||
val dataModule = module {
|
||||
single<ItemRepository> { ItemRepositoryImpl(get(), get()) }
|
||||
single { ItemLocalDataSource(get()) }
|
||||
single { ItemRemoteDataSource(get()) }
|
||||
}
|
||||
|
||||
// Presentation module
|
||||
val presentationModule = module {
|
||||
viewModelOf(::ItemListViewModel)
|
||||
viewModelOf(::DashboardViewModel)
|
||||
}
|
||||
```
|
||||
|
||||
### Hilt (Android-only)
|
||||
|
||||
```kotlin
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class RepositoryModule {
|
||||
@Binds
|
||||
abstract fun bindItemRepository(impl: ItemRepositoryImpl): ItemRepository
|
||||
}
|
||||
|
||||
@HiltViewModel
|
||||
class ItemListViewModel @Inject constructor(
|
||||
private val getItems: GetItemsByCategoryUseCase
|
||||
) : ViewModel()
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Result/Try Pattern
|
||||
|
||||
Use `Result<T>` or a custom sealed type for error propagation:
|
||||
|
||||
```kotlin
|
||||
sealed interface Try<out T> {
|
||||
data class Success<T>(val value: T) : Try<T>
|
||||
data class Failure(val error: AppError) : Try<Nothing>
|
||||
}
|
||||
|
||||
sealed interface AppError {
|
||||
data class Network(val message: String) : AppError
|
||||
data class Database(val message: String) : AppError
|
||||
data object Unauthorized : AppError
|
||||
}
|
||||
|
||||
// In ViewModel — map to UI state
|
||||
viewModelScope.launch {
|
||||
when (val result = getItems(category)) {
|
||||
is Try.Success -> _state.update { it.copy(items = result.value, isLoading = false) }
|
||||
is Try.Failure -> _state.update { it.copy(error = result.error.toMessage(), isLoading = false) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Convention Plugins (Gradle)
|
||||
|
||||
For KMP projects, use convention plugins to reduce build file duplication:
|
||||
|
||||
```kotlin
|
||||
// build-logic/src/main/kotlin/kmp-library.gradle.kts
|
||||
plugins {
|
||||
id("org.jetbrains.kotlin.multiplatform")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
androidTarget()
|
||||
iosX64(); iosArm64(); iosSimulatorArm64()
|
||||
sourceSets {
|
||||
commonMain.dependencies { /* shared deps */ }
|
||||
commonTest.dependencies { implementation(kotlin("test")) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Apply in modules:
|
||||
|
||||
```kotlin
|
||||
// domain/build.gradle.kts
|
||||
plugins { id("kmp-library") }
|
||||
```
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- Importing Android framework classes in `domain` — keep it pure Kotlin
|
||||
- Exposing database entities or DTOs to the UI layer — always map to domain models
|
||||
- Putting business logic in ViewModels — extract to UseCases
|
||||
- Using `GlobalScope` or unstructured coroutines — use `viewModelScope` or structured concurrency
|
||||
- Fat repository implementations — split into focused DataSources
|
||||
- Circular module dependencies — if A depends on B, B must not depend on A
|
||||
|
||||
## References
|
||||
|
||||
See skill: `compose-multiplatform-patterns` for UI patterns.
|
||||
See skill: `kotlin-coroutines-flows` for async patterns.
|
||||
@@ -0,0 +1,155 @@
|
||||
---
|
||||
name: angular-developer
|
||||
description: Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Angular Developer Guidelines
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Working in any Angular project or codebase
|
||||
- Creating or scaffolding a new Angular project, application, or library
|
||||
- Generating components, services, directives, pipes, guards, or resolvers
|
||||
- Implementing reactivity with Angular Signals, `linkedSignal`, or `resource`
|
||||
- Working with Angular forms (signal forms, reactive forms, or template-driven)
|
||||
- Setting up dependency injection, routing, lazy loading, or route guards
|
||||
- Adding accessibility (ARIA), animations, or component styling
|
||||
- Writing or debugging Angular-specific tests (unit, component harness, E2E)
|
||||
- Configuring Angular CLI tooling or the Angular MCP server
|
||||
|
||||
1. Always analyze the project's Angular version before providing guidance, as best practices and available features can vary significantly between versions. If creating a new project with Angular CLI, do not specify a version unless prompted by the user.
|
||||
|
||||
2. When generating code, follow Angular's style guide and best practices for maintainability and performance. Use the Angular CLI for scaffolding components, services, directives, pipes, and routes to ensure consistency.
|
||||
|
||||
3. Once you finish generating code, run `ng build` to ensure there are no build errors. If there are errors, analyze the error messages and fix them before proceeding. Do not skip this step, as it is critical for ensuring the generated code is correct and functional.
|
||||
|
||||
## Creating New Projects
|
||||
|
||||
If no guidelines are provided by the user, use these defaults when creating a new Angular project:
|
||||
|
||||
1. Use the latest stable version of Angular unless the user specifies otherwise.
|
||||
2. Prefer Signal Forms for new projects only when the target Angular version supports them. [Find out more](references/signal-forms.md).
|
||||
|
||||
**Execution Rules for `ng new`:**
|
||||
When asked to create a new Angular project, you must determine the correct execution command by following these strict steps:
|
||||
|
||||
**Step 1: Check for an explicit user version.**
|
||||
|
||||
- **IF** the user requests a specific version (e.g., Angular 15), bypass local installations and strictly use `npx`.
|
||||
- **Command:** `npx @angular/cli@<requested_version> new <project-name>`
|
||||
|
||||
**Step 2: Check for an existing Angular installation.**
|
||||
|
||||
- **IF** no specific version is requested, run `ng version` in the terminal to check if the Angular CLI is already installed on the system.
|
||||
- **IF** the command succeeds and returns an installed version, use the local/global installation directly.
|
||||
- **Command:** `ng new <project-name>`
|
||||
|
||||
**Step 3: Fallback to Latest.**
|
||||
|
||||
- **IF** no specific version is requested AND the `ng version` command fails (indicating no Angular installation exists), you must use `npx` to fetch the latest version.
|
||||
- **Command:** `npx @angular/cli@latest new <project-name>`
|
||||
|
||||
## Components
|
||||
|
||||
When working with Angular components, consult the following references based on the task:
|
||||
|
||||
- **Fundamentals**: Anatomy, metadata, core concepts, and template control flow (@if, @for, @switch). Read [components.md](references/components.md)
|
||||
- **Inputs**: Signal-based inputs, transforms, and model inputs. Read [inputs.md](references/inputs.md)
|
||||
- **Outputs**: Signal-based outputs and custom event best practices. Read [outputs.md](references/outputs.md)
|
||||
- **Host Elements**: Host bindings and attribute injection. Read [host-elements.md](references/host-elements.md)
|
||||
|
||||
If you require deeper documentation not found in the references above, read the documentation at `https://angular.dev/guide/components`.
|
||||
|
||||
## Reactivity and Data Management
|
||||
|
||||
When managing state and data reactivity, use Angular Signals and consult the following references:
|
||||
|
||||
- **Signals Overview**: Core signal concepts (`signal`, `computed`), reactive contexts, and `untracked`. Read [signals-overview.md](references/signals-overview.md)
|
||||
- **Dependent State (`linkedSignal`)**: Creating writable state linked to source signals. Read [linked-signal.md](references/linked-signal.md)
|
||||
- **Async Reactivity (`resource`)**: Fetching asynchronous data directly into signal state. Read [resource.md](references/resource.md)
|
||||
- **Side Effects (`effect`)**: Logging, third-party DOM manipulation (`afterRenderEffect`), and when NOT to use effects. Read [effects.md](references/effects.md)
|
||||
|
||||
## Forms
|
||||
|
||||
In most cases for new apps, **prefer signal forms**. When making a forms decision, analyze the project and consider the following guidelines:
|
||||
|
||||
- If the application version supports Signal Forms and this is a new form, **prefer signal forms**.
|
||||
- For older applications or existing forms, match the application's current form strategy.
|
||||
|
||||
- **Signal Forms**: Use signals for form state management. Read [signal-forms.md](references/signal-forms.md)
|
||||
- **Template-driven forms**: Use for simple forms. Read [template-driven-forms.md](references/template-driven-forms.md)
|
||||
- **Reactive forms**: Use for complex forms. Read [reactive-forms.md](references/reactive-forms.md)
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
When implementing dependency injection in Angular, follow these guidelines:
|
||||
|
||||
- **Fundamentals**: Overview of Dependency Injection, services, and the `inject()` function. Read [di-fundamentals.md](references/di-fundamentals.md)
|
||||
- **Creating and Using Services**: Creating services, the `providedIn: 'root'` option, and injecting into components or other services. Read [creating-services.md](references/creating-services.md)
|
||||
- **Defining Dependency Providers**: Automatic vs manual provision, `InjectionToken`, `useClass`, `useValue`, `useFactory`, and scopes. Read [defining-providers.md](references/defining-providers.md)
|
||||
- **Injection Context**: Where `inject()` is allowed, `runInInjectionContext`, and `assertInInjectionContext`. Read [injection-context.md](references/injection-context.md)
|
||||
- **Hierarchical Injectors**: The `EnvironmentInjector` vs `ElementInjector`, resolution rules, modifiers (`optional`, `skipSelf`), and `providers` vs `viewProviders`. Read [hierarchical-injectors.md](references/hierarchical-injectors.md)
|
||||
|
||||
## Angular Aria
|
||||
|
||||
When building accessible custom components for any of the following patterns: Accordion, Listbox, Combobox, Menu, Tabs, Toolbar, Tree, Grid, consult the following reference:
|
||||
|
||||
- **Angular Aria Components**: Building headless, accessible components (Accordion, Listbox, Combobox, Menu, Tabs, Toolbar, Tree, Grid) and styling ARIA attributes. Read [angular-aria.md](references/angular-aria.md)
|
||||
|
||||
## Routing
|
||||
|
||||
When implementing navigation in Angular, consult the following references:
|
||||
|
||||
- **Define Routes**: URL paths, static vs dynamic segments, wildcards, and redirects. Read [define-routes.md](references/define-routes.md)
|
||||
- **Route Loading Strategies**: Eager vs lazy loading, and context-aware loading. Read [loading-strategies.md](references/loading-strategies.md)
|
||||
- **Show Routes with Outlets**: Using `<router-outlet>`, nested outlets, and named outlets. Read [show-routes-with-outlets.md](references/show-routes-with-outlets.md)
|
||||
- **Navigate to Routes**: Declarative navigation with `RouterLink` and programmatic navigation with `Router`. Read [navigate-to-routes.md](references/navigate-to-routes.md)
|
||||
- **Control Route Access with Guards**: Implementing `CanActivate`, `CanMatch`, and other guards for security. Read [route-guards.md](references/route-guards.md)
|
||||
- **Data Resolvers**: Pre-fetching data before route activation with `ResolveFn`. Read [data-resolvers.md](references/data-resolvers.md)
|
||||
- **Router Lifecycle and Events**: Chronological order of navigation events and debugging. Read [router-lifecycle.md](references/router-lifecycle.md)
|
||||
- **Rendering Strategies**: CSR, SSG (Prerendering), and SSR with hydration. Read [rendering-strategies.md](references/rendering-strategies.md)
|
||||
- **Route Transition Animations**: Enabling and customizing the View Transitions API. Read [route-animations.md](references/route-animations.md)
|
||||
|
||||
If you require deeper documentation or more context, visit the [official Angular Routing guide](https://angular.dev/guide/routing).
|
||||
|
||||
## Styling and Animations
|
||||
|
||||
When implementing styling and animations in Angular, consult the following references:
|
||||
|
||||
- **Using Tailwind CSS with Angular**: Integrating Tailwind CSS into Angular projects. Read [tailwind-css.md](references/tailwind-css.md)
|
||||
- **Angular Animations**: Using native CSS (recommended) or the legacy DSL for dynamic effects. Read [angular-animations.md](references/angular-animations.md)
|
||||
- **Styling components**: Best practices for component styles and encapsulation. Read [component-styling.md](references/component-styling.md)
|
||||
|
||||
## Testing
|
||||
|
||||
When writing or updating tests, consult the following references based on the task:
|
||||
|
||||
- **Fundamentals**: Best practices for unit testing, async patterns, and `TestBed`. Read [testing-fundamentals.md](references/testing-fundamentals.md)
|
||||
- **Component Harnesses**: Standard patterns for robust component interaction. Read [component-harnesses.md](references/component-harnesses.md)
|
||||
- **Router Testing**: Using `RouterTestingHarness` for reliable navigation tests. Read [router-testing.md](references/router-testing.md)
|
||||
- **End-to-End (E2E) Testing**: Best practices for E2E tests with Cypress or Playwright. Read [e2e-testing.md](references/e2e-testing.md)
|
||||
|
||||
## Tooling
|
||||
|
||||
When working with Angular tooling, consult the following references:
|
||||
|
||||
- **Angular CLI**: Creating applications, generating code (components, routes, services), serving, and building. Read [cli.md](references/cli.md)
|
||||
- **Angular MCP Server**: Available tools, configuration, and experimental features. Read [mcp.md](references/mcp.md)
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Using `null` or `undefined` as initial signal form field values — use `''`, `0`, or `[]` instead
|
||||
- Accessing form field state flags without calling the field first: `form.field.valid()` — use `form.field().valid()`
|
||||
- Starting new forms with older form APIs when the target Angular version supports Signal Forms
|
||||
- Setting `min`, `max`, `value`, `disabled`, or `readonly` HTML attributes on `[formField]` inputs — define these as schema rules instead
|
||||
- Calling `inject()` outside an injection context — use `runInInjectionContext` when needed
|
||||
- Using `effect()` for derived state that should use `computed()`
|
||||
- Referencing `$parent.$index` in nested `@for` loops — Angular does not support `$parent`; use `let outerIdx = $index` instead
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `tdd-workflow` — test-driven development workflow applicable to Angular components and services
|
||||
- `security-review` — security checklist for web applications including Angular-specific concerns
|
||||
- `frontend-patterns` — general frontend patterns for context on React/Next.js approaches
|
||||
@@ -0,0 +1,160 @@
|
||||
# Angular Animations
|
||||
|
||||
When animating elements in Angular, **first analyze the project's Angular version** in `package.json`.
|
||||
For modern applications (**Angular v20.2 and above**), prefer using native CSS with `animate.enter` and `animate.leave`. For older applications, you may need to use the deprecated `@angular/animations` package.
|
||||
|
||||
## 1. Native CSS Animations (v20.2+ Recommended)
|
||||
|
||||
Modern Angular provides `animate.enter` and `animate.leave` to animate elements as they enter or leave the DOM. They apply CSS classes at the appropriate times.
|
||||
|
||||
### `animate.enter` and `animate.leave`
|
||||
|
||||
Use these directly on elements to apply CSS classes during the enter or leave phase. Angular automatically removes the enter classes when the animation completes. For `animate.leave`, Angular waits for the animation to finish before removing the element from the DOM.
|
||||
|
||||
`animate.enter` example:
|
||||
|
||||
```html
|
||||
@if (isShown()) {
|
||||
<div class="enter-container" animate.enter="enter-animation">
|
||||
<p>The box is entering.</p>
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
/* Ensure you have a starting style if using transitions instead of keyframes */
|
||||
.enter-container {
|
||||
border: 1px solid #dddddd;
|
||||
margin-top: 1em;
|
||||
padding: 20px;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
.enter-container p {
|
||||
margin: 0;
|
||||
}
|
||||
.enter-animation {
|
||||
animation: slide-fade 1s;
|
||||
}
|
||||
@keyframes slide-fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
_Note: `animate.leave` may be added to child elements being removed._
|
||||
|
||||
### Event Bindings and Third-party Libraries
|
||||
|
||||
You can bind to `(animate.enter)` and `(animate.leave)` to call functions or use JS libraries like GSAP.
|
||||
|
||||
```html
|
||||
@if(show()) {
|
||||
<div (animate.leave)="onLeave($event)">...</div>
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
import { AnimationCallbackEvent } from '@angular/core';
|
||||
|
||||
onLeave(event: AnimationCallbackEvent) {
|
||||
// Custom animation logic here
|
||||
// CRITICAL: You MUST call animationComplete() when done so Angular removes the element!
|
||||
event.animationComplete();
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Advanced CSS Animations
|
||||
|
||||
CSS offers robust tools for advanced animation sequences.
|
||||
|
||||
### Animating State and Styles
|
||||
|
||||
Toggle CSS classes on elements using property binding to trigger transitions.
|
||||
|
||||
```html
|
||||
<div [class.open]="isOpen">...</div>
|
||||
```
|
||||
|
||||
```css
|
||||
div {
|
||||
transition: height 0.3s ease-out;
|
||||
height: 100px;
|
||||
}
|
||||
div.open {
|
||||
height: 200px;
|
||||
}
|
||||
```
|
||||
|
||||
### Animating Auto Height
|
||||
|
||||
You can use `css-grid` to animate to auto height.
|
||||
|
||||
```css
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
transition: grid-template-rows 0.3s;
|
||||
}
|
||||
.container.open {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
.container > div {
|
||||
overflow: hidden;
|
||||
}
|
||||
```
|
||||
|
||||
### Staggering and Parallel Animations
|
||||
|
||||
- **Staggering**: Use `animation-delay` or `transition-delay` with different values for items in a list.
|
||||
- **Parallel**: Apply multiple animations in the `animation` shorthand (e.g., `animation: rotate 3s, fade-in 2s;`).
|
||||
|
||||
### Programmatic Control
|
||||
|
||||
Retrieve animations directly using standard Web APIs:
|
||||
|
||||
```ts
|
||||
const animations = element.getAnimations();
|
||||
animations.forEach((anim) => anim.pause());
|
||||
```
|
||||
|
||||
## 3. Legacy Animations DSL (Deprecated)
|
||||
|
||||
For older projects (pre v20.2 or where `@angular/animations` is already heavily used), you use the component metadata DSL.
|
||||
|
||||
**Important:** Do not mix legacy animations and `animate.enter`/`leave` in the same component.
|
||||
|
||||
### Setup
|
||||
|
||||
```ts
|
||||
bootstrapApplication(App, {
|
||||
providers: [provideAnimationsAsync()],
|
||||
});
|
||||
```
|
||||
|
||||
### Defining Transitions
|
||||
|
||||
```ts
|
||||
import {signal} from '@angular/core';
|
||||
import {trigger, state, style, animate, transition} from '@angular/animations';
|
||||
|
||||
@Component({
|
||||
animations: [
|
||||
trigger('openClose', [
|
||||
state('open', style({opacity: 1})),
|
||||
state('closed', style({opacity: 0})),
|
||||
transition('open <=> closed', [animate('0.5s')]),
|
||||
]),
|
||||
],
|
||||
template: `<div [@openClose]="isOpen() ? 'open' : 'closed'">...</div>`,
|
||||
})
|
||||
export class OpenClose {
|
||||
isOpen = signal(true);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,410 @@
|
||||
# Angular Aria
|
||||
|
||||
Angular Aria (`@angular/aria`) is a collection of headless, accessible directives that implement common WAI-ARIA patterns. These directives handle keyboard interactions, ARIA attributes, focus management, and screen reader support.
|
||||
|
||||
**As an AI Agent, your role is to provide the HTML structure and CSS styling**, while the directives handle the complex accessibility logic.
|
||||
|
||||
## Styling Headless Components
|
||||
|
||||
Because Angular Aria components are headless, they do not come with default styles. You **must** use CSS to style different states based on the ARIA attributes or structural classes the directives automatically apply.
|
||||
|
||||
Common ARIA attributes to target in CSS:
|
||||
|
||||
- `[aria-expanded="true"]` / `[aria-expanded="false"]`
|
||||
- `[aria-selected="true"]`
|
||||
- `[aria-disabled="true"]`
|
||||
- `[aria-current="page"]` (for navigation)
|
||||
|
||||
---
|
||||
|
||||
**CRITICAL**: Before using this package, it must be installed via the package manager. Confirm that it has been installed in the project. Use `npm install @angular/aria` to install if necessary.
|
||||
|
||||
## 1. Accordion
|
||||
|
||||
Organizes related content into expandable/collapsible sections.
|
||||
|
||||
**Usage:** The Accordion is a layout component designed to organize content into logical groups that users can expand one at a time to reduce scrolling on content-heavy pages. Use it for FAQs, long forms, or progressive disclosure of information, but avoid it for primary navigation or scenarios where users must view multiple sections of content simultaneously.
|
||||
|
||||
**Imports:** `import { AccordionContent, AccordionGroup, AccordionPanel, AccordionTrigger } from '@angular/aria/accordion';`
|
||||
|
||||
**Directives:** `ngAccordionGroup`, `ngAccordionTrigger`, `ngAccordionPanel`, `ngAccordionContent` (for lazy loading).
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
selector: 'app-cmp',
|
||||
imports: [AccordionContent, AccordionGroup, AccordionPanel, AccordionTrigger],
|
||||
template: `...`,
|
||||
styles: [],
|
||||
})
|
||||
export class App {
|
||||
protected readonly title = signal('angular-app');
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<div ngAccordionGroup [multiExpandable]="false">
|
||||
<div class="accordion-item">
|
||||
<button ngAccordionTrigger panelId="panel-1" class="accordion-header">
|
||||
Section 1
|
||||
<span class="icon">▼</span>
|
||||
</button>
|
||||
<div ngAccordionPanel panelId="panel-1" class="accordion-panel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>Lazy loaded content here.</p>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Styling Strategy:**
|
||||
Target the `[aria-expanded]` attribute on the trigger to rotate icons, and style the panel visibility.
|
||||
|
||||
```css
|
||||
.accordion-header[aria-expanded='true'] .icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* The panel directive handles DOM removal, but you can style the transition */
|
||||
.accordion-panel {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Listbox
|
||||
|
||||
A foundational directive for displaying a list of options. Used for visible selection lists (not dropdowns).
|
||||
|
||||
**Usage:** Visible selectable lists (single or multi-select).
|
||||
|
||||
**Imports:** `import {Listbox, Option} from '@angular/aria/listbox';`
|
||||
|
||||
**Directives:** `ngListbox`, `ngOption`.
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
selector: 'app-cmp',
|
||||
imports: [Listbox, Option],
|
||||
template: `...`,
|
||||
styles: [],
|
||||
})
|
||||
export class App {
|
||||
protected readonly title = signal('angular-app');
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- horizontal or vertical orientation -->
|
||||
<ul ngListbox [(values)]="selectedItems" orientation="horizontal" [multi]="true">
|
||||
<li ngOption value="apple" class="option">Apple</li>
|
||||
<li ngOption value="banana" class="option">Banana</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
**Styling Strategy:**
|
||||
Target `[aria-selected="true"]` for selected state and `:focus-visible` or `[data-active]` for the focused item (Angular Aria uses roving tabindex or activedescendant).
|
||||
|
||||
```css
|
||||
.option {
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.option[aria-selected='true'] {
|
||||
background: #e0f7fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
/* Focus state managed by aria */
|
||||
.option:focus-visible {
|
||||
outline: 2px solid blue;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Combobox, Select, and Multiselect
|
||||
|
||||
These patterns combine `ngCombobox` with a popup containing an `ngListbox`.
|
||||
|
||||
- **Combobox**: Text input + popup (used for Autocomplete).
|
||||
- **Select**: Readonly Combobox + single-select Listbox.
|
||||
- **Multiselect**: Readonly Combobox + multi-select Listbox.
|
||||
|
||||
**Usage:** The Combobox is a low-level primitive directive that synchronizes a text input with a popup, serving as the foundational logic for autocomplete, select, and multiselect patterns. Use it specifically for building custom filtering, unique selection requirements, or specialized input-to-popup coordination that deviates from standard, documented components.
|
||||
|
||||
**Imports:**
|
||||
|
||||
```
|
||||
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
```
|
||||
|
||||
**Directives:** `ngCombobox`, `ngComboboxInput`, `ngComboboxPopupContainer`, `ngListbox`, `ngOption`.
|
||||
|
||||
```html
|
||||
<!-- Example: Standard Select -->
|
||||
<div ngCombobox [readonly]="true">
|
||||
<button ngComboboxInput class="select-trigger">
|
||||
{{ selectedValue() || 'Choose an option' }}
|
||||
</button>
|
||||
|
||||
<ng-template ngComboboxPopupContainer>
|
||||
<ul ngListbox [(values)]="selectedValue" class="dropdown-menu">
|
||||
<li ngOption value="option1">Option 1</li>
|
||||
<li ngOption value="option2">Option 2</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Styling Strategy:**
|
||||
Style the popup container to look like a dropdown floating above content (often paired with CDK Overlay).
|
||||
|
||||
```css
|
||||
.select-trigger {
|
||||
width: 200px;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
.dropdown-menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 1px solid #ccc;
|
||||
background: white;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Menu and Menubar
|
||||
|
||||
For actions, commands, and context menus (not for form selection).
|
||||
|
||||
**Usage:** The Menubar is a high-level navigation pattern designed for building desktop-style application command bars (e.g., File, Edit, View) that stay persistent across an interface. It is best utilized for organizing complex commands into logical top-level categories with full horizontal keyboard support, but it should be avoided for simple standalone action lists or mobile-first layouts where horizontal space is constrained.
|
||||
|
||||
**Imports:** `import {MenuBar, Menu, MenuContent, MenuItem} from '@angular/aria/menu';`
|
||||
|
||||
**Directives:** `ngMenuBar`, `ngMenu`, `ngMenuItem`, `ngMenuTrigger`.
|
||||
|
||||
```html
|
||||
<!-- Menubar Example -->
|
||||
<ul ngMenuBar class="menubar">
|
||||
<li ngMenuItem value="file">
|
||||
<button ngMenuTrigger [menu]="fileMenu">File</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul ngMenu #fileMenu="ngMenu" class="menu">
|
||||
<li ngMenuItem value="new">New</li>
|
||||
<li ngMenuItem value="open">Open</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
**Styling Strategy:**
|
||||
Use flexbox for the menubar. Hide/show submenus based on the trigger's state.
|
||||
|
||||
```css
|
||||
.menubar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.menu {
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px 0;
|
||||
}
|
||||
.menu li {
|
||||
padding: 5px 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Tabs
|
||||
|
||||
Layered content sections where only one panel is visible.
|
||||
|
||||
**Usage:** The Tabs component is used to organize related content into distinct, navigable sections, allowing users to switch between categories or views without leaving the page. It is ideal for settings panels, multi-topic documentation, or dashboards, but should be avoided for sequential workflows (steppers) or when navigation involves more than 7–8 sections.
|
||||
|
||||
**Imports:** `import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';`
|
||||
|
||||
**Directives:** `ngTabs`, `ngTabList`, `ngTab`, `ngTabPanel`, `ngTabContent`.
|
||||
|
||||
```html
|
||||
<div ngTabs>
|
||||
<ul ngTabList class="tab-list">
|
||||
<li ngTab value="profile" class="tab-btn">Profile</li>
|
||||
<li ngTab value="security" class="tab-btn">Security</li>
|
||||
</ul>
|
||||
|
||||
<div ngTabPanel value="profile" class="tab-panel">
|
||||
<ng-template ngTabContent>Profile Settings</ng-template>
|
||||
</div>
|
||||
<div ngTabPanel value="security" class="tab-panel">
|
||||
<ng-template ngTabContent>Security Settings</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Styling Strategy:**
|
||||
Target `[aria-selected="true"]` on the tab buttons.
|
||||
|
||||
```css
|
||||
.tab-list {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #ccc;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.tab-btn {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
.tab-btn[aria-selected='true'] {
|
||||
border-bottom-color: blue;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tab-panel {
|
||||
padding: 20px;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Toolbar
|
||||
|
||||
Groups related controls (like text formatting).
|
||||
|
||||
**Usage:** The Toolbar is an organizational component designed to group frequently accessed, related controls into a single logical container. It is best used to enhance keyboard efficiency (via arrow-key navigation) and visual structure for workflows requiring repeated actions, such as text formatting or media controls.
|
||||
|
||||
**Imports:** `import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';`
|
||||
|
||||
**Directives:** `ngToolbar`, `ngToolbarWidget`, `ngToolbarWidgetGroup`.
|
||||
|
||||
```html
|
||||
<div ngToolbar class="toolbar">
|
||||
<div ngToolbarWidgetGroup [multi]="true" role="group" aria-label="Formatting">
|
||||
<button ngToolbarWidget value="bold" class="tool-btn">B</button>
|
||||
<button ngToolbarWidget value="italic" class="tool-btn">I</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Styling Strategy:**
|
||||
Target `[aria-pressed="true"]` (for toggle buttons) or `[aria-checked="true"]` (for radio groups) within the toolbar.
|
||||
|
||||
```css
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
padding: 8px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.tool-btn {
|
||||
padding: 5px 10px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.tool-btn[aria-pressed='true'],
|
||||
.tool-btn[aria-checked='true'] {
|
||||
background: #ddd;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Tree
|
||||
|
||||
Displays hierarchical data (file systems, nested nav).
|
||||
|
||||
**Usage:** The Tree component is designed for navigating and displaying deeply nested, hierarchical data structures like file systems, organization charts, or complex site architectures. It should be used specifically for multi-level relationships where users need to expand or collapse branches, but it should be avoided for flat lists, data tables, or simple selection menus.
|
||||
|
||||
**Imports:** `import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';`
|
||||
|
||||
**Directives:** `ngTree`, `ngTreeItem`, `ngTreeGroup`.
|
||||
|
||||
```html
|
||||
<ul ngTree class="tree">
|
||||
<li ngTreeItem value="documents">
|
||||
<span class="tree-label">Documents</span>
|
||||
<ul ngTreeGroup class="tree-group">
|
||||
<li ngTreeItem value="resume">Resume.pdf</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
**Styling Strategy:**
|
||||
Target `[aria-expanded]` to show/hide children or rotate chevron icons. Use `padding-left` on nested groups to show hierarchy.
|
||||
|
||||
```css
|
||||
.tree,
|
||||
.tree-group {
|
||||
list-style: none;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.tree-label::before {
|
||||
content: '> ';
|
||||
display: inline-block;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
li[aria-expanded='true'] > .tree-label::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Grid
|
||||
|
||||
A two-dimensional interactive collection of cells enabling navigation via arrow keys.
|
||||
|
||||
**Usage:** Data tables, calendars, spreadsheets, and layout patterns for interactive elements.
|
||||
**Directives:** `ngGrid`, `ngGridRow`, `ngGridCell`, `ngGridCellWidget`.
|
||||
|
||||
```html
|
||||
<table ngGrid [multi]="true" [enableSelection]="true" class="grid-table">
|
||||
<tr ngGridRow>
|
||||
<th ngGridCell role="columnheader">Name</th>
|
||||
<th ngGridCell role="columnheader">Status</th>
|
||||
</tr>
|
||||
<tr ngGridRow>
|
||||
<td ngGridCell>Project A</td>
|
||||
<td ngGridCell [(selected)]="isSelected">
|
||||
<button ngGridCellWidget (activated)="onActivate()">Active</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
```
|
||||
|
||||
**Styling Strategy:**
|
||||
Target `[aria-selected="true"]` for selected cells and `:focus-visible` for the active cell (roving tabindex) or `[aria-activedescendant]` on the container.
|
||||
|
||||
```css
|
||||
.grid-table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
[ngGridCell] {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
[ngGridCell][aria-selected='true'] {
|
||||
background: #e3f2fd;
|
||||
}
|
||||
/* Focus state managed by roving tabindex */
|
||||
[ngGridCell]:focus-visible {
|
||||
outline: 2px solid #2196f3;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
```
|
||||
|
||||
## General Rules for Agents
|
||||
|
||||
1. **Never use native HTML elements like `<select>`** when asked to implement these specific Aria patterns. Use the `ng*` directives.
|
||||
2. **Handle CSS manually**: Remember that `Angular Aria` does NOT provide styles. You must write the CSS, targeting the native ARIA attributes (`aria-expanded`, `aria-selected`, etc.) that the directives automatically toggle.
|
||||
3. **Lazy Loading**: Always use the provided structural directives (`ngAccordionContent`, `ngTabContent`) inside `ng-template` for heavy content panels to ensure they are lazily rendered.
|
||||
@@ -0,0 +1,86 @@
|
||||
# Angular CLI Guide for Agents
|
||||
|
||||
The Angular CLI (`ng`) is the primary tool for managing an Angular workspace. Always prefer CLI commands over manual file creation or generic `npm` commands when modifying project structure or adding Angular-specific dependencies.
|
||||
|
||||
## 1. Managing Dependencies
|
||||
|
||||
**ALWAYS use `ng add` for Angular libraries** instead of `npm install`. `ng add` installs the package AND runs initialization schematics (e.g., configuring `angular.json`, updating root providers).
|
||||
|
||||
```bash
|
||||
ng add @angular/material
|
||||
ng add tailwindcss
|
||||
ng add @angular/fire
|
||||
```
|
||||
|
||||
To update the application and its dependencies (which automatically runs code migrations):
|
||||
|
||||
```bash
|
||||
ng update @angular/core@<latest or specific version> @angular/cli<latest or specific version>
|
||||
```
|
||||
|
||||
## 2. Generating Code (`ng generate` or `ng g`)
|
||||
|
||||
Always use the CLI to generate code to ensure it adheres to Angular standards and updates necessary configuration files automatically.
|
||||
|
||||
| Target | Command | Notes |
|
||||
| :----------- | :-------------------- | :--------------------------------------------------------------------------------------------- |
|
||||
| Component | `ng g c path/to/name` | Generates a component. Use `--inline-style` (`-s`) or `--inline-template` (`-t`) if requested. |
|
||||
| Service | `ng g s path/to/name` | Generates an `@Injectable({providedIn: 'root'})` service. |
|
||||
| Directive | `ng g d path/to/name` | Generates a directive. |
|
||||
| Pipe | `ng g p path/to/name` | Generates a pipe. |
|
||||
| Guard | `ng g g path/to/name` | Generates a functional route guard. |
|
||||
| Environments | `ng g environments` | Scaffolds `src/environments/` and updates `angular.json` with file replacements. |
|
||||
|
||||
_Note: There is no command to generate a single route definition. Generate a component, then manually add it to the `Routes` array in `app.routes.ts`._
|
||||
|
||||
## 3. Development Server & Proxying
|
||||
|
||||
Start the local development server with hot-module replacement (HMR):
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
### Backend API Proxying
|
||||
|
||||
To proxy API requests during development (e.g., rerouting `/api` to a local Node server):
|
||||
|
||||
1. Create `src/proxy.conf.json`:
|
||||
```json
|
||||
{
|
||||
"/api/**": {"target": "http://localhost:3000", "secure": false}
|
||||
}
|
||||
```
|
||||
2. Update `angular.json` under the `serve` target:
|
||||
```json
|
||||
"serve": {
|
||||
"builder": "@angular/build:dev-server",
|
||||
"options": { "proxyConfig": "src/proxy.conf.json" }
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Building the Application
|
||||
|
||||
Compile the application into an output directory (default: `dist/<project-name>/browser`). Modern Angular uses the `@angular/build:application` builder (esbuild-based).
|
||||
|
||||
```bash
|
||||
ng build
|
||||
```
|
||||
|
||||
- `ng build` defaults to the production configuration, which enables Ahead-of-Time (AOT) compilation, minification, and tree-shaking.
|
||||
- Target specific configurations defined in `angular.json` using `--configuration`: `ng build --configuration=staging`.
|
||||
|
||||
## 5. Testing
|
||||
|
||||
- **Unit Tests**: Run `ng test` to execute unit tests via the configured test runner (e.g., Karma or Vitest).
|
||||
- **End-to-End (E2E)**: Run `ng e2e`. If no E2E framework is configured, the CLI will prompt to install one (Cypress, Playwright, Puppeteer, etc.).
|
||||
|
||||
## 6. Deployment
|
||||
|
||||
To deploy an application, you must first add a deployment builder, then run the deploy command:
|
||||
|
||||
```bash
|
||||
# Example for Firebase
|
||||
ng add @angular/fire
|
||||
ng deploy
|
||||
```
|
||||
@@ -0,0 +1,59 @@
|
||||
# Testing with Component Harnesses
|
||||
|
||||
Component harnesses are the standard, preferred way to interact with components in tests. They provide a robust, user-centric API that makes tests less brittle and easier to read by insulating them from changes to a component's internal DOM structure.
|
||||
|
||||
## Why Use Harnesses?
|
||||
|
||||
- **Robustness:** Tests don't break when you refactor a component's internal HTML or CSS classes.
|
||||
- **Readability:** Tests describe interactions from a user's perspective (e.g., `button.click()`, `slider.getValue()`) instead of through DOM queries (`fixture.nativeElement.querySelector(...)`).
|
||||
- **Reusability:** The same harness can be used in both unit tests and E2E tests.
|
||||
|
||||
Angular Material provides a test harness for every component in its library.
|
||||
|
||||
## Using a Harness in a Unit Test
|
||||
|
||||
The `TestbedHarnessEnvironment` is the entry point for using harnesses in unit tests.
|
||||
|
||||
### Example: Testing with a `MatButtonHarness`
|
||||
|
||||
```ts
|
||||
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
|
||||
import {MatButtonHarness} from '@angular/material/button/testing';
|
||||
import {MyButtonContainerComponent} from './my-button-container.component';
|
||||
|
||||
describe('MyButtonContainerComponent', () => {
|
||||
let fixture: ComponentFixture<MyButtonContainerComponent>;
|
||||
let loader: HarnessLoader;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MyButtonContainerComponent, MatButtonModule],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MyButtonContainerComponent);
|
||||
// Create a harness loader for the component's fixture
|
||||
loader = TestbedHarnessEnvironment.loader(fixture);
|
||||
});
|
||||
|
||||
it('should find a button with specific text', async () => {
|
||||
// Load the harness for a MatButton with the text "Submit"
|
||||
const submitButton = await loader.getHarness(MatButtonHarness.with({text: 'Submit'}));
|
||||
|
||||
// Use the harness API to interact with the component
|
||||
expect(await submitButton.isDisabled()).toBe(false);
|
||||
await submitButton.click();
|
||||
|
||||
// ... assertions
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Key Concepts
|
||||
|
||||
1. **`HarnessLoader`**: An object used to find and create harness instances. Get a loader for your component's fixture using `TestbedHarnessEnvironment.loader(fixture)`.
|
||||
|
||||
2. **`loader.getHarness(HarnessClass)`**: Asynchronously finds and returns a harness instance for the first matching component.
|
||||
|
||||
3. **`HarnessClass.with({ ... })`**: Many harnesses provide a static `with` method that returns a `HarnessPredicate`. This allows you to filter and find components based on their properties, like text, selector, or disabled state. Always use this to precisely target the component you want to test.
|
||||
|
||||
4. **Harness API:** Once you have a harness instance, use its methods (e.g., `.click()`, `.getText()`, `.getValue()`) to interact with the component. These methods automatically handle waiting for async operations and change detection.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Component Styling
|
||||
|
||||
Angular components can define styles that apply specifically to their template, enabling encapsulation and modularity.
|
||||
|
||||
## Defining Styles
|
||||
|
||||
Styles can be defined inline or in separate files.
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
selector: 'app-photo',
|
||||
// Inline styles
|
||||
styles: `
|
||||
img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
`,
|
||||
// OR external file
|
||||
styleUrl: 'photo.component.css',
|
||||
})
|
||||
export class Photo {}
|
||||
```
|
||||
|
||||
## View Encapsulation
|
||||
|
||||
Every component has a view encapsulation setting that determines how styles are scoped.
|
||||
|
||||
| Mode | Behavior |
|
||||
| :------------------------------ | :-------------------------------------------------------------------------------------------- |
|
||||
| `Emulated` (Default) | Scopes styles to the component using unique HTML attributes. Global styles can still leak in. |
|
||||
| `ShadowDom` | Uses the browser's native Shadow DOM API to isolate styles completely. |
|
||||
| `None` | Disables encapsulation. Component styles become global. |
|
||||
| `ExperimentalIsolatedShadowDom` | Strictly guarantees that only the component's styles apply. |
|
||||
|
||||
### Usage
|
||||
|
||||
```ts
|
||||
import { ViewEncapsulation } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
...,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class GlobalStyled {}
|
||||
```
|
||||
|
||||
## Special Selectors
|
||||
|
||||
### `:host`
|
||||
|
||||
Targets the component's host element (the element matching the component's selector).
|
||||
|
||||
```css
|
||||
:host {
|
||||
display: block;
|
||||
border: 1px solid black;
|
||||
}
|
||||
```
|
||||
|
||||
### `:host-context()`
|
||||
|
||||
Targets the host element based on some condition in its ancestry.
|
||||
|
||||
```css
|
||||
/* Apply styles if any ancestor has the 'theme-dark' class */
|
||||
:host-context(.theme-dark) {
|
||||
background-color: #333;
|
||||
}
|
||||
```
|
||||
|
||||
### `::ng-deep`
|
||||
|
||||
Disables view encapsulation for a specific rule, allowing it to "leak" into child components.
|
||||
**Note: The Angular team strongly discourages the use of `::ng-deep`.** It is supported only for backwards compatibility.
|
||||
|
||||
## Styles in Templates
|
||||
|
||||
You can use `<style>` elements directly in a component's template. View encapsulation rules still apply.
|
||||
|
||||
```html
|
||||
<style>
|
||||
.dynamic-class {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
<div class="dynamic-class">Hello</div>
|
||||
```
|
||||
|
||||
## External Styles
|
||||
|
||||
Using `<link>` or `@import` in CSS is treated as external styles. **External styles are not affected by emulated view encapsulation.**
|
||||
@@ -0,0 +1,117 @@
|
||||
# Components
|
||||
|
||||
Angular components are the fundamental building blocks of an application. Each component consists of a TypeScript class with behaviors, an HTML template, and a CSS selector.
|
||||
|
||||
## Component Definition
|
||||
|
||||
Use the `@Component` decorator to define a component's metadata.
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
selector: 'app-profile',
|
||||
template: `
|
||||
<img src="profile.jpg" alt="Profile photo" />
|
||||
<button (click)="save()">Save</button>
|
||||
`,
|
||||
styles: `
|
||||
img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class Profile {
|
||||
save() {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Metadata Options
|
||||
|
||||
- `selector`: The CSS selector that identifies this component in templates.
|
||||
- `template`: Inline HTML template (preferred for small templates).
|
||||
- `templateUrl`: Path to an external HTML file.
|
||||
- `styles`: Inline CSS styles.
|
||||
- `styleUrl` / `styleUrls`: Path(s) to external CSS file(s).
|
||||
- `imports`: Lists the components, directives, or pipes used in this component's template.
|
||||
|
||||
## Using Components
|
||||
|
||||
To use a component, add it to the `imports` array of the consuming component and use its selector in the template.
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [Profile],
|
||||
template: `<app-profile />`,
|
||||
})
|
||||
export class App {}
|
||||
```
|
||||
|
||||
## Template Control Flow
|
||||
|
||||
Angular uses built-in blocks for conditional rendering and loops.
|
||||
|
||||
### Conditional Rendering (`@if`)
|
||||
|
||||
Use `@if` to conditionally show content. You can include `@else if` and `@else` blocks.
|
||||
|
||||
```html
|
||||
@if (user.isAdmin) {
|
||||
<admin-dashboard />
|
||||
} @else if (user.isModerator) {
|
||||
<mod-dashboard />
|
||||
} @else {
|
||||
<standard-dashboard />
|
||||
}
|
||||
```
|
||||
|
||||
**Result aliasing**: Save the result of the expression for reuse.
|
||||
|
||||
```html
|
||||
@if (user.settings(); as settings) {
|
||||
<p>Theme: {{ settings.theme }}</p>
|
||||
}
|
||||
```
|
||||
|
||||
### Loops (`@for`)
|
||||
|
||||
The `@for` block iterates over collections. The `track` expression is **required** for performance and DOM reuse.
|
||||
|
||||
```html
|
||||
<ul>
|
||||
@for (item of items(); track item.id; let i = $index, total = $count) {
|
||||
<li>{{ i + 1 }}/{{ total }}: {{ item.name }}</li>
|
||||
} @empty {
|
||||
<li>No items to display.</li>
|
||||
}
|
||||
</ul>
|
||||
```
|
||||
|
||||
**Implicit Variables**: `$index`, `$count`, `$first`, `$last`, `$even`, `$odd`.
|
||||
|
||||
### Switching Content (`@switch`)
|
||||
|
||||
The `@switch` block renders content based on a value. It uses strict equality (`===`) and has **no fallthrough**.
|
||||
|
||||
```html
|
||||
@switch (status()) { @case ('loading') { <app-spinner /> } @case ('error') { <app-error-msg /> }
|
||||
@case ('success') { <app-data-grid /> } @default {
|
||||
<p>Unknown status</p>
|
||||
} }
|
||||
```
|
||||
|
||||
**Exhaustive Type Checking**: Use `@default never;` to ensure all cases of a union type are handled.
|
||||
|
||||
```html
|
||||
@switch (state) { @case ('on') { ... } @case ('off') { ... } @default never; // Errors if a new
|
||||
state like 'standby' is added }
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
- **Host Element**: The DOM element that matches the component's selector.
|
||||
- **View**: The DOM rendered by the component's template inside the host element.
|
||||
- **Standalone**: By default, components are standalone (since Angular 19, `standalone: true` is default). For older versions, `standalone: true` must be explicit or the component must be part of an `NgModule`.
|
||||
- **Component Tree**: Angular applications are structured as a tree of components, where each component can host child components.
|
||||
- **Component Naming**: Do not add suffixes the `Component` suffix for Component classes (e.g., AppComponent) unless the project has been configured to use that naming configuration.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Creating and Using Services
|
||||
|
||||
Services in Angular are reusable pieces of code that handle data fetching, business logic, or state management that multiple components or other services need to access.
|
||||
|
||||
## Creating a Service
|
||||
|
||||
You can generate a service using the Angular CLI:
|
||||
|
||||
```bash
|
||||
ng generate service my-data
|
||||
```
|
||||
|
||||
Or you can manually create a TypeScript class and decorate it with `@Injectable()`.
|
||||
|
||||
```ts
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class BasicDataStore {
|
||||
private data: string[] = [];
|
||||
|
||||
addData(item: string): void {
|
||||
this.data.push(item);
|
||||
}
|
||||
|
||||
getData(): string[] {
|
||||
return [...this.data];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### The `providedIn: 'root'` Option
|
||||
|
||||
Using `providedIn: 'root'` is the recommended approach for most services. It tells Angular to:
|
||||
|
||||
- **Create a single instance (singleton)** for the entire application.
|
||||
- **Make it available everywhere** automatically, without needing to list it in any `providers` array.
|
||||
- **Enable tree-shaking**, meaning the service is only included in the final JavaScript bundle if it is actually injected somewhere.
|
||||
|
||||
## Injecting a Service
|
||||
|
||||
Once a service is created, you can inject it into components, directives, or other services using the `inject()` function.
|
||||
|
||||
### Injecting into a Component
|
||||
|
||||
```ts
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {BasicDataStore} from './basic-data-store.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-example',
|
||||
template: `
|
||||
<div>
|
||||
<p>Data items: {{ dataStore.getData().length }}</p>
|
||||
<button (click)="dataStore.addData('New Item')">Add Item</button>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class Example {
|
||||
// Inject the service as a class field
|
||||
dataStore = inject(BasicDataStore);
|
||||
}
|
||||
```
|
||||
|
||||
### Injecting into Another Service
|
||||
|
||||
Services can inject other services in the exact same way.
|
||||
|
||||
```ts
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {AdvancedDataStore} from './advanced-data-store.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class BasicDataStore {
|
||||
// Injecting another service
|
||||
private advancedDataStore = inject(AdvancedDataStore);
|
||||
|
||||
private data: string[] = [];
|
||||
|
||||
getData(): string[] {
|
||||
// Combine data from this service and the injected service
|
||||
return [...this.data, ...this.advancedDataStore.getData()];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Service Patterns
|
||||
|
||||
While `providedIn: 'root'` covers most scenarios, you may sometimes need:
|
||||
|
||||
- **Component-specific instances**: If a component needs its own isolated instance of a service, provide it directly in the component's `@Component({ providers: [MyService] })` array.
|
||||
- **Factory providers**: For dynamic creation.
|
||||
- **Value providers**: For injecting configuration objects.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Data Resolvers
|
||||
|
||||
Data resolvers fetch data before a route activates, ensuring components have the necessary data upon rendering.
|
||||
|
||||
## Creating a Resolver
|
||||
|
||||
Implement the `ResolveFn` type.
|
||||
|
||||
```ts
|
||||
export const userResolver: ResolveFn<User> = (route, state) => {
|
||||
const userService = inject(UserService);
|
||||
const id = route.paramMap.get('id')!;
|
||||
return userService.getUser(id);
|
||||
};
|
||||
```
|
||||
|
||||
## Configuring the Route
|
||||
|
||||
Add the resolver under the `resolve` key.
|
||||
|
||||
```ts
|
||||
{
|
||||
path: 'user/:id',
|
||||
component: UserProfile,
|
||||
resolve: {
|
||||
user: userResolver
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Accessing Resolved Data
|
||||
|
||||
### 1. Via `ActivatedRoute` (Traditional)
|
||||
|
||||
```ts
|
||||
private route = inject(ActivatedRoute);
|
||||
data = toSignal(this.route.data);
|
||||
user = computed(() => this.data().user);
|
||||
```
|
||||
|
||||
### 2. Via Component Inputs (Modern)
|
||||
|
||||
Enable `withComponentInputBinding()` in `provideRouter` to pass resolved data directly to `@Input` or `input()`.
|
||||
|
||||
```ts
|
||||
// app.config.ts
|
||||
provideRouter(routes, withComponentInputBinding());
|
||||
|
||||
// component.ts
|
||||
user = input.required<User>();
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Navigation is blocked if a resolver fails.
|
||||
|
||||
- Use `withNavigationErrorHandler` for global handling.
|
||||
- Use `catchError` within the resolver to return a `RedirectCommand` or fallback data.
|
||||
|
||||
```ts
|
||||
return userService
|
||||
.get(id)
|
||||
.pipe(catchError(() => of(new RedirectCommand(router.parseUrl('/error')))));
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Keep it lightweight**: Fetch only critical data.
|
||||
- **Provide feedback**: Listen to router events to show a global loading bar during navigation, as the UI stays on the old page until the resolver finishes.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Define Routes
|
||||
|
||||
Routes are objects that define which component should render for a specific URL path.
|
||||
|
||||
## Basic Configuration
|
||||
|
||||
Define routes in a `Routes` array and provide them using `provideRouter` in your `appConfig`.
|
||||
|
||||
```ts
|
||||
// app.routes.ts
|
||||
export const routes: Routes = [
|
||||
{path: '', component: HomePage},
|
||||
{path: 'admin', component: AdminPage},
|
||||
];
|
||||
|
||||
// app.config.ts
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideRouter(routes)],
|
||||
};
|
||||
```
|
||||
|
||||
## URL Paths
|
||||
|
||||
- **Static**: Matches an exact string (e.g., `'admin'`).
|
||||
- **Route Parameters**: Dynamic segments prefixed with a colon (e.g., `'user/:id'`).
|
||||
- **Wildcard**: Matches any URL using `**`. Useful for "Not Found" pages. **Always place at the end of the array.**
|
||||
|
||||
## Matching Strategy
|
||||
|
||||
Angular uses a **first-match wins** strategy. Specific routes must come before less specific ones.
|
||||
|
||||
## Redirects
|
||||
|
||||
Use `redirectTo` to point one path to another.
|
||||
|
||||
```ts
|
||||
{ path: 'articles', redirectTo: '/blog' },
|
||||
{ path: 'blog', component: Blog },
|
||||
```
|
||||
|
||||
## Page Titles
|
||||
|
||||
Associate titles with routes for accessibility. Titles can be static or dynamic (via `ResolveFn` or a custom `TitleStrategy`).
|
||||
|
||||
```ts
|
||||
{ path: 'home', component: Home, title: 'Home Page' }
|
||||
```
|
||||
|
||||
## Route Data and Providers
|
||||
|
||||
- **Static Data**: Attach metadata using the `data` property.
|
||||
- **Route Providers**: Scope dependencies to a specific route and its children using the `providers` array.
|
||||
|
||||
## Nested (Child) Routes
|
||||
|
||||
Define sub-views using the `children` property. Parent components must include a `<router-outlet />`.
|
||||
|
||||
```ts
|
||||
{
|
||||
path: 'product/:id',
|
||||
component: Product,
|
||||
children: [
|
||||
{ path: 'info', component: ProductInfo },
|
||||
{ path: 'reviews', component: ProductReviews },
|
||||
],
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,72 @@
|
||||
# Defining Dependency Providers
|
||||
|
||||
Angular offers automatic and manual ways to provide dependencies to its Dependency Injection (DI) system.
|
||||
|
||||
## Automatic Provision
|
||||
|
||||
The most common way to provide a service is using `providedIn: 'root'` on an `@Injectable()`.
|
||||
|
||||
### InjectionToken
|
||||
|
||||
Use `InjectionToken` for non-class dependencies (configuration objects, functions, primitives). An `InjectionToken` can also be automatically provided.
|
||||
|
||||
```ts
|
||||
import {InjectionToken} from '@angular/core';
|
||||
|
||||
export interface AppConfig {
|
||||
apiUrl: string;
|
||||
}
|
||||
|
||||
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config', {
|
||||
providedIn: 'root',
|
||||
factory: () => ({apiUrl: 'https://api.example.com'}),
|
||||
});
|
||||
```
|
||||
|
||||
## Manual Provision
|
||||
|
||||
You use the `providers` array when a service lacks `providedIn`, when you want a new instance for a specific component, or when configuring runtime values.
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
providers: [
|
||||
// Shorthand for { provide: LocalService, useClass: LocalService }
|
||||
LocalService,
|
||||
|
||||
// useClass: Swap implementations
|
||||
{provide: Logger, useClass: BetterLogger},
|
||||
|
||||
// useValue: Provide static values
|
||||
{provide: API_URL_TOKEN, useValue: 'https://api.example.com'},
|
||||
|
||||
// useFactory: Generate value dynamically
|
||||
{
|
||||
provide: ApiClient,
|
||||
useFactory: (http = inject(HttpClient)) => new ApiClient(http),
|
||||
},
|
||||
|
||||
// useExisting: Create an alias
|
||||
{provide: OldLogger, useExisting: NewLogger},
|
||||
|
||||
// multi: Provide multiple values for the same token as an array
|
||||
{provide: INTERCEPTOR_TOKEN, useClass: AuthInterceptor, multi: true},
|
||||
],
|
||||
})
|
||||
export class Example {}
|
||||
```
|
||||
|
||||
## Scopes of Providers
|
||||
|
||||
- **Application Bootstrap**: Global singletons. Use for HTTP clients, logging, or app-wide config.
|
||||
- **Component/Directive**: Isolated instances. Use for component-specific state or forms. Services are destroyed when the component is destroyed.
|
||||
- **Route**: Feature-specific services loaded only with specific routes.
|
||||
|
||||
## Library Pattern: `provide*` functions
|
||||
|
||||
Library authors should export functions that return provider arrays to encapsulate configuration:
|
||||
|
||||
```ts
|
||||
export function provideAnalytics(config: AnalyticsConfig): Provider[] {
|
||||
return [{provide: ANALYTICS_CONFIG, useValue: config}, AnalyticsService];
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,120 @@
|
||||
# Dependency Injection (DI) Fundamentals
|
||||
|
||||
Dependency Injection (DI) is a design pattern used to organize and share code across an application by allowing you to "inject" features into different parts. This improves code maintainability, scalability, and testability.
|
||||
|
||||
## How DI Works in Angular
|
||||
|
||||
There are two primary ways code interacts with Angular's DI system:
|
||||
|
||||
1. **Providing**: Making values (objects, functions, primitives) available to the DI system.
|
||||
2. **Injecting**: Asking the DI system for those values.
|
||||
|
||||
Angular components, directives, and services automatically participate in DI.
|
||||
|
||||
## Services
|
||||
|
||||
A **service** is the most common way to share data and functionality across an application. It is a TypeScript class decorated with `@Injectable()`.
|
||||
|
||||
### Creating a Service
|
||||
|
||||
Use the `providedIn: 'root'` option in the `@Injectable` decorator to make the service a singleton available throughout the entire application. This is the recommended approach for most services.
|
||||
|
||||
```ts
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root', // Makes this a singleton available everywhere
|
||||
})
|
||||
export class AnalyticsLogger {
|
||||
trackEvent(category: string, value: string) {
|
||||
console.log('Analytics event logged:', {category, value});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Common uses for services include:
|
||||
|
||||
- Data clients (API calls)
|
||||
- State management
|
||||
- Authentication and authorization
|
||||
- Logging and error handling
|
||||
- Utility functions
|
||||
|
||||
## Injecting Dependencies
|
||||
|
||||
Use Angular's `inject()` function to request dependencies.
|
||||
|
||||
### The `inject()` Function
|
||||
|
||||
You can use the `inject()` function to get an instance of a service (or any other provided token).
|
||||
|
||||
```ts
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {Router} from '@angular/router';
|
||||
import {AnalyticsLogger} from './analytics-logger.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-navbar',
|
||||
template: `<a href="#" (click)="navigateToDetail($event)">Detail Page</a>`,
|
||||
})
|
||||
export class Navbar {
|
||||
// Injecting dependencies using class field initializers
|
||||
private router = inject(Router);
|
||||
private analytics = inject(AnalyticsLogger);
|
||||
|
||||
navigateToDetail(event: Event) {
|
||||
event.preventDefault();
|
||||
this.analytics.trackEvent('navigation', '/details');
|
||||
this.router.navigate(['/details']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Where can `inject()` be used? (Injection Context)
|
||||
|
||||
You can call `inject()` in an **injection context**. The most common injection contexts are during the construction of a component, directive, or service.
|
||||
|
||||
Valid places to call `inject()`:
|
||||
|
||||
1. **Class field initializers** (Recommended)
|
||||
2. **Constructor body**
|
||||
3. **Route guards and resolvers** (which are executed in an injection context)
|
||||
4. **Factory functions** used in providers
|
||||
|
||||
```typescript
|
||||
import {Component, Directive, Injectable, inject, ElementRef} from '@angular/core';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
|
||||
// 1. In a Component (Field Initializer & Constructor)
|
||||
@Component({
|
||||
/*...*/
|
||||
})
|
||||
export class Example {
|
||||
private service1 = inject(MyService); // Valid field initializer
|
||||
|
||||
private service2: MyService;
|
||||
constructor() {
|
||||
this.service2 = inject(MyService); // Valid constructor body
|
||||
}
|
||||
}
|
||||
|
||||
// 2. In a Directive
|
||||
@Directive({
|
||||
/*...*/
|
||||
})
|
||||
export class MyDirective {
|
||||
private element = inject(ElementRef); // Valid field initializer
|
||||
}
|
||||
|
||||
// 3. In a Service
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class MyService {
|
||||
private http = inject(HttpClient); // Valid field initializer
|
||||
}
|
||||
|
||||
// 4. In a Route Guard (Functional)
|
||||
export const authGuard = () => {
|
||||
const auth = inject(AuthService); // Valid route guard
|
||||
return auth.isAuthenticated();
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
# End-to-End (E2E) Testing
|
||||
|
||||
Use E2E tests to cover critical user journeys in a real browser. Prefer the framework already configured in the Angular workspace, such as Cypress or Playwright.
|
||||
|
||||
## Running E2E Tests
|
||||
|
||||
Check `package.json` and `angular.json` for the project-specific command. Common patterns include:
|
||||
|
||||
```shell
|
||||
npm run e2e
|
||||
pnpm e2e
|
||||
ng e2e
|
||||
```
|
||||
|
||||
When the app must be built or served first, use the existing project scripts instead of inventing a parallel test entrypoint.
|
||||
|
||||
## Test Structure
|
||||
|
||||
- Keep E2E specs close to the configured test framework, such as `cypress/e2e/` or `e2e/`.
|
||||
- Put reusable login/setup helpers in the framework support directory.
|
||||
- Keep fixtures explicit and small enough that each test can explain the user state it depends on.
|
||||
|
||||
### Cypress Example
|
||||
|
||||
```typescript
|
||||
describe('Login flow', () => {
|
||||
it('redirects to dashboard on valid credentials', () => {
|
||||
cy.visit('/login');
|
||||
cy.get('[data-cy=email]').type('user@example.com');
|
||||
cy.get('[data-cy=password]').type('password123');
|
||||
cy.get('[data-cy=submit]').click();
|
||||
cy.url().should('include', '/dashboard');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Playwright Example
|
||||
|
||||
```typescript
|
||||
import {expect, test} from '@playwright/test';
|
||||
|
||||
test('redirects to dashboard on valid credentials', async ({page}) => {
|
||||
await page.goto('/login');
|
||||
await page.getByLabel('Email').fill('user@example.com');
|
||||
await page.getByLabel('Password').fill('password123');
|
||||
await page.getByRole('button', {name: 'Sign in'}).click();
|
||||
await expect(page).toHaveURL(/dashboard/);
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Prefer accessible locators (`getByRole`, `getByLabel`) or stable `data-*` attributes.
|
||||
- Avoid selectors that depend on CSS classes, DOM depth, or incidental text.
|
||||
- Wait for specific UI states, routes, or network responses instead of arbitrary sleeps.
|
||||
- Keep smoke tests short and reserve full workflow coverage for the highest-value paths.
|
||||
@@ -0,0 +1,83 @@
|
||||
# Side Effects with `effect` and `afterRenderEffect`
|
||||
|
||||
In Angular, an **effect** is an operation that runs whenever one or more signal values it tracks change.
|
||||
|
||||
## When to use `effect`
|
||||
|
||||
Effects are intended for syncing signal state to imperative, non-signal APIs.
|
||||
|
||||
**Valid Use Cases:**
|
||||
|
||||
- Logging analytics.
|
||||
- Syncing state to `localStorage` or `sessionStorage`.
|
||||
- Performing custom rendering to a `<canvas>` or 3rd-party charting library.
|
||||
|
||||
**CRITICAL RULE: DO NOT use effects to propagate state.**
|
||||
If you find yourself using `.set()` or `.update()` on a signal _inside_ an effect to keep two signals in sync, you are making a mistake. This causes `ExpressionChangedAfterItHasBeenChecked` errors and infinite loops. **Always use `computed()` or `linkedSignal()` for state derivation.**
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Effects execute asynchronously during the change detection process. They always run at least once.
|
||||
|
||||
```ts
|
||||
import { Component, signal, effect } from '@angular/core';
|
||||
|
||||
@Component({...})
|
||||
export class Example {
|
||||
count = signal(0);
|
||||
|
||||
constructor() {
|
||||
// Effect must be created in an injection context (e.g., a constructor)
|
||||
effect((onCleanup) => {
|
||||
console.log(`Count changed to ${this.count()}`);
|
||||
|
||||
const timer = setTimeout(() => console.log('Timer finished'), 1000);
|
||||
|
||||
// Cleanup function runs before the next execution, or when destroyed
|
||||
onCleanup(() => clearTimeout(timer));
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## DOM Manipulation with `afterRenderEffect`
|
||||
|
||||
Standard `effect` runs _before_ Angular updates the DOM. If you need to manually inspect or modify the DOM based on a signal change (e.g., integrating a 3rd party UI library), use `afterRenderEffect`.
|
||||
|
||||
`afterRenderEffect` runs after Angular has finished rendering the DOM.
|
||||
|
||||
### Render Phases
|
||||
|
||||
To prevent reflows (forced layout thrashing), `afterRenderEffect` forces you to divide your DOM reads and writes into specific phases.
|
||||
|
||||
```ts
|
||||
import { Component, afterRenderEffect, viewChild, ElementRef } from '@angular/core';
|
||||
|
||||
@Component({...})
|
||||
export class Chart {
|
||||
canvas = viewChild.required<ElementRef>('canvas');
|
||||
|
||||
constructor() {
|
||||
afterRenderEffect({
|
||||
// 1. Read from the DOM
|
||||
earlyRead: () => {
|
||||
return this.canvas().nativeElement.getBoundingClientRect().width;
|
||||
},
|
||||
// 2. Write to the DOM (receives the result of the previous phase)
|
||||
write: (width) => {
|
||||
// NEVER read from the DOM in the write phase.
|
||||
setupChart(this.canvas().nativeElement, width);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available Phases (executed in this order):**
|
||||
|
||||
1. `earlyRead`
|
||||
2. `write` (Never read here)
|
||||
3. `mixedReadWrite` (Avoid if possible)
|
||||
4. `read` (Never write here)
|
||||
|
||||
_Note: `afterRenderEffect` only runs on the client, never during Server-Side Rendering (SSR)._
|
||||
@@ -0,0 +1,43 @@
|
||||
# Hierarchical Injectors
|
||||
|
||||
Angular's dependency injection system is hierarchical, meaning services can be scoped to different levels of the application.
|
||||
|
||||
## Types of Injector Hierarchies
|
||||
|
||||
1. **`EnvironmentInjector` Hierarchy**: Configured via `@Injectable({ providedIn: 'root' })` or `ApplicationConfig.providers` during bootstrap. These are global singletons.
|
||||
2. **`ElementInjector` Hierarchy**: Created implicitly at each DOM element. Configured via the `providers` or `viewProviders` array in `@Component()` or `@Directive()`.
|
||||
|
||||
## Resolution Rules
|
||||
|
||||
When a dependency is requested, Angular resolves it in two phases:
|
||||
|
||||
1. It searches up the **`ElementInjector`** tree, starting from the requesting component/directive up to the root element.
|
||||
2. If not found, it searches the **`EnvironmentInjector`** tree, starting from the closest environment injector up to the root.
|
||||
3. If still not found, it throws an error (unless marked optional).
|
||||
|
||||
## Resolution Modifiers
|
||||
|
||||
You can alter how Angular searches for a dependency using the options object in `inject()`:
|
||||
|
||||
- **`optional`**: If the dependency isn't found, return `null` instead of throwing an error.
|
||||
- **`self`**: Only check the current `ElementInjector`. Do not look up the parent tree.
|
||||
- **`skipSelf`**: Start searching in the parent `ElementInjector`, skipping the current element.
|
||||
- **`host`**: Stop searching when reaching the host component's view boundary.
|
||||
|
||||
```ts
|
||||
@Component({...})
|
||||
export class Example {
|
||||
// Returns null if not found instead of crashing
|
||||
optionalService = inject(MyService, { optional: true });
|
||||
|
||||
// Skips this component's providers, looks at parent
|
||||
parentService = inject(ParentService, { skipSelf: true });
|
||||
}
|
||||
```
|
||||
|
||||
## `providers` vs `viewProviders`
|
||||
|
||||
When providing a service at the component level:
|
||||
|
||||
- **`providers`**: The service is available to the component, its view (template), and any **projected content** (`<ng-content>`).
|
||||
- **`viewProviders`**: The service is available to the component and its view, but **NOT** to projected content. Use this to isolate services from content passed in by consumers.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Component Host Elements
|
||||
|
||||
The **host element** is the DOM element that matches a component's selector. The component's template renders inside this element.
|
||||
|
||||
## Binding to the Host Element
|
||||
|
||||
Use the `host` property in the `@Component` decorator to bind properties, attributes, styles, and events to the host element. This is the **preferred approach** over legacy decorators.
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
selector: 'custom-slider',
|
||||
host: {
|
||||
'role': 'slider', // Static attribute
|
||||
'[attr.aria-valuenow]': 'value', // Attribute binding
|
||||
'[class.active]': 'isActive()', // Class binding
|
||||
'[style.color]': 'color()', // Style binding
|
||||
'[tabIndex]': 'disabled ? -1 : 0', // Property binding
|
||||
'(keydown)': 'onKeyDown($event)', // Event binding
|
||||
},
|
||||
})
|
||||
export class CustomSlider {
|
||||
value = 0;
|
||||
disabled = false;
|
||||
isActive = signal(false);
|
||||
color = signal('blue');
|
||||
|
||||
onKeyDown(event: KeyboardEvent) {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Legacy Decorators
|
||||
|
||||
`@HostBinding` and `@HostListener` are supported for backwards compatibility but should be avoided in new code.
|
||||
|
||||
```ts
|
||||
export class CustomSlider {
|
||||
@HostBinding('tabIndex')
|
||||
get tabIndex() {
|
||||
return this.disabled ? -1 : 0;
|
||||
}
|
||||
|
||||
@HostListener('keydown', ['$event'])
|
||||
onKeyDown(event: KeyboardEvent) {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Binding Collisions
|
||||
|
||||
If both the component (host binding) and the consumer (template binding) bind to the same property:
|
||||
|
||||
1. **Static vs Static**: The instance (consumer) binding wins.
|
||||
2. **Static vs Dynamic**: The dynamic binding wins.
|
||||
3. **Dynamic vs Dynamic**: The component's host binding wins.
|
||||
|
||||
## Injecting Host Attributes
|
||||
|
||||
Use `HostAttributeToken` with the `inject` function to read static attributes from the host element at construction time.
|
||||
|
||||
```ts
|
||||
import {Component, HostAttributeToken, inject} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-btn',
|
||||
template: `<ng-content />`,
|
||||
})
|
||||
export class AppButton {
|
||||
// Throws error if 'type' is missing unless injected with { optional: true }
|
||||
type = inject(new HostAttributeToken('type'));
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```html
|
||||
<app-btn type="primary">Click Me</app-btn>
|
||||
```
|
||||
@@ -0,0 +1,63 @@
|
||||
# Injection Context
|
||||
|
||||
The `inject()` function can only be used when code is executing within an **injection context**.
|
||||
|
||||
## Where is an Injection Context Available?
|
||||
|
||||
An injection context is automatically available in:
|
||||
|
||||
1. **Field initializers** of classes instantiated by DI (`@Injectable`, `@Component`, `@Directive`, `@Pipe`).
|
||||
2. **Constructors** of classes instantiated by DI.
|
||||
3. **Factory functions** specified in `useFactory` or `InjectionToken` configurations.
|
||||
4. **Functional APIs** executed by Angular (e.g., functional route guards, resolvers, interceptors).
|
||||
|
||||
```ts
|
||||
@Component({...})
|
||||
export class Example {
|
||||
// Valid: Field initializer
|
||||
private router = inject(Router);
|
||||
|
||||
constructor() {
|
||||
// Valid: Constructor
|
||||
const http = inject(HttpClient);
|
||||
}
|
||||
|
||||
onClick() {
|
||||
// Invalid: Not an injection context
|
||||
// const auth = inject(AuthService);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `runInInjectionContext`
|
||||
|
||||
If you need to run a function within an injection context (often needed for dynamic component creation or testing), use `runInInjectionContext`. This requires access to an existing injector (like `EnvironmentInjector` or `Injector`).
|
||||
|
||||
```ts
|
||||
import {Injectable, inject, EnvironmentInjector, runInInjectionContext} from '@angular/core';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class MyService {
|
||||
private injector = inject(EnvironmentInjector);
|
||||
|
||||
doSomethingDynamic() {
|
||||
runInInjectionContext(this.injector, () => {
|
||||
// Now valid to use inject() here
|
||||
const router = inject(Router);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `assertInInjectionContext`
|
||||
|
||||
Use `assertInInjectionContext` in utility functions to guarantee they are called from a valid context. It throws a clear error if not.
|
||||
|
||||
```ts
|
||||
import {assertInInjectionContext, inject, ElementRef} from '@angular/core';
|
||||
|
||||
export function injectNativeElement<T extends Element>(): T {
|
||||
assertInInjectionContext(injectNativeElement);
|
||||
return inject(ElementRef).nativeElement;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,101 @@
|
||||
# Inputs
|
||||
|
||||
Inputs allow data to flow from a parent component to a child component. Angular recommends using the signal-based `input` API for modern applications.
|
||||
|
||||
## Signal-based Inputs
|
||||
|
||||
Declare inputs using the `input()` function. This returns an `InputSignal`.
|
||||
|
||||
```ts
|
||||
import {Component, input, computed} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user',
|
||||
template: `<p>User: {{ name() }} ({{ age() }})</p>`,
|
||||
})
|
||||
export class User {
|
||||
// Optional input with default value
|
||||
name = input('Guest');
|
||||
|
||||
// Required input
|
||||
age = input.required<number>();
|
||||
|
||||
// Inputs are reactive signals
|
||||
label = computed(() => `Name: ${this.name()}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Usage in Template
|
||||
|
||||
```html
|
||||
<app-user [name]="userName" [age]="25" />
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The `input` function accepts a config object:
|
||||
|
||||
- **Alias**: Change the property name used in templates.
|
||||
- **Transform**: Modify the value before it reaches the component.
|
||||
|
||||
```ts
|
||||
import { input, booleanAttribute } from '@angular/core';
|
||||
|
||||
@Component({...})
|
||||
export class CustomButton {
|
||||
// Alias example
|
||||
label = input('', { alias: 'btnLabel' });
|
||||
|
||||
// Transform example using built-in helper
|
||||
disabled = input(false, { transform: booleanAttribute });
|
||||
}
|
||||
```
|
||||
|
||||
## Model Inputs (Two-Way Binding)
|
||||
|
||||
Use `model()` to create an input that supports two-way data binding.
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
selector: 'custom-counter',
|
||||
template: `<button (click)="increment()">+</button>`,
|
||||
})
|
||||
export class CustomCounter {
|
||||
value = model(0);
|
||||
|
||||
increment() {
|
||||
this.value.update((v) => v + 1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```html
|
||||
<!-- Two-way binding with a signal -->
|
||||
<custom-counter [(value)]="mySignal" />
|
||||
|
||||
<!-- Two-way binding with a plain property -->
|
||||
<custom-counter [(value)]="myProperty" />
|
||||
```
|
||||
|
||||
## Decorator-based Inputs (@Input)
|
||||
|
||||
The legacy API remains supported but is not recommended for new code.
|
||||
|
||||
```ts
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
@Component({...})
|
||||
export class Legacy {
|
||||
@Input({ required: true }) value = 0;
|
||||
@Input({ transform: trimString }) label = '';
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Prefer Signals**: Use `input()` instead of `@Input()` for better reactivity and type safety.
|
||||
- **Required Inputs**: Use `input.required()` for mandatory data to get build-time errors.
|
||||
- **Pure Transforms**: Ensure input transform functions are pure and statically analyzable.
|
||||
- **Avoid Collisions**: Do not use input names that collide with standard DOM properties (e.g., `id`, `title`).
|
||||
@@ -0,0 +1,59 @@
|
||||
# Dependent State with `linkedSignal`
|
||||
|
||||
The `linkedSignal` function lets you create writable state that is intrinsically linked to some other state. It is perfect for state that needs a default value derived from an input or another signal, but can still be independently modified by the user.
|
||||
|
||||
If the source state changes, the `linkedSignal` resets to a new computed value.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
When you only need to recompute based on a source, pass a computation function. `linkedSignal` works like `computed`, but the resulting signal is writable (you can call `.set()` or `.update()` on it).
|
||||
|
||||
```ts
|
||||
import { Component, signal, linkedSignal } from '@angular/core';
|
||||
|
||||
@Component({...})
|
||||
export class ShippingMethodPicker {
|
||||
shippingOptions = signal(['Ground', 'Air', 'Sea']);
|
||||
|
||||
// Defaults to the first option.
|
||||
// If shippingOptions changes, selectedOption resets to the new first option.
|
||||
selectedOption = linkedSignal(() => this.shippingOptions()[0]);
|
||||
|
||||
changeShipping(index: number) {
|
||||
// We can still manually update this signal!
|
||||
this.selectedOption.set(this.shippingOptions()[index]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Usage: Accounting for Previous State
|
||||
|
||||
Sometimes, when the source state changes, you want to preserve the user's manual selection if it is still valid. To do this, use the object syntax providing `source` and `computation`.
|
||||
|
||||
The `computation` function receives the new value of the source, and a `previous` object containing the previous source value and the previous `linkedSignal` value.
|
||||
|
||||
```ts
|
||||
interface ShippingMethod { id: number; name: string; }
|
||||
|
||||
@Component({...})
|
||||
export class ShippingMethodPicker {
|
||||
shippingOptions = signal<ShippingMethod[]>([
|
||||
{id: 0, name: 'Ground'}, {id: 1, name: 'Air'}, {id: 2, name: 'Sea'}
|
||||
]);
|
||||
|
||||
selectedOption = linkedSignal<ShippingMethod[], ShippingMethod>({
|
||||
source: this.shippingOptions,
|
||||
computation: (newOptions, previous) => {
|
||||
// If the newly loaded options still contain the user's previously
|
||||
// selected option, keep it selected. Otherwise, reset to the first option.
|
||||
return newOptions.find(opt => opt.id === previous?.value.id) ?? newOptions[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### When to use `linkedSignal` vs `computed` vs `effect`
|
||||
|
||||
- Use `computed`: When state is **strictly** derived from other state and should never be manually updated.
|
||||
- Use `linkedSignal`: When state is derived from other state, but the user **must** be able to override or manually update it.
|
||||
- **Never** use `effect` to sync one piece of state to another. That is an anti-pattern. Use `computed` or `linkedSignal` instead.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Route Loading Strategies
|
||||
|
||||
Angular supports two main strategies for loading routes and components to balance initial load time and navigation responsiveness.
|
||||
|
||||
## Eager Loading
|
||||
|
||||
Components are bundled into the initial JavaScript payload and are available immediately.
|
||||
|
||||
```ts
|
||||
{ path: 'home', component: Home }
|
||||
```
|
||||
|
||||
- **Pros**: Seamless transitions.
|
||||
- **Cons**: Increases initial bundle size.
|
||||
|
||||
## Lazy Loading
|
||||
|
||||
Components or routes are loaded only when the user navigates to them. This creates separate JavaScript "chunks".
|
||||
|
||||
### Lazy Loading Components
|
||||
|
||||
Use `loadComponent` to fetch the component on demand.
|
||||
|
||||
```ts
|
||||
{
|
||||
path: 'admin',
|
||||
loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)`,
|
||||
}
|
||||
```
|
||||
|
||||
### Lazy Loading Child Routes
|
||||
|
||||
Use `loadChildren` to fetch a set of routes.
|
||||
|
||||
```ts
|
||||
{
|
||||
path: 'settings',
|
||||
loadChildren: () => import('./settings/settings.routes'),
|
||||
}
|
||||
```
|
||||
|
||||
## Injection Context and Lazy Loading
|
||||
|
||||
Loader functions run within the **injection context** of the current route. This allows you to call `inject()` to make context-aware loading decisions.
|
||||
|
||||
```ts
|
||||
{
|
||||
path: 'dashboard',
|
||||
loadComponent: () => {
|
||||
const flags = inject(FeatureFlags);
|
||||
return flags.isPremium
|
||||
? import('./premium-dashboard')
|
||||
: import('./basic-dashboard');
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Recommendation
|
||||
|
||||
- Use **Eager Loading** for the primary landing pages.
|
||||
- Use **Lazy Loading** for all other feature areas to keep the initial bundle small.
|
||||
@@ -0,0 +1,108 @@
|
||||
# Angular CLI MCP Server
|
||||
|
||||
The Angular CLI includes a Model Context Protocol (MCP) server that enables AI assistants (like Cursor, Gemini CLI, JetBrains AI, etc.) to interact directly with the Angular CLI. It provides tools for code generation, modernizing code, fetching examples, and running builds/tests.
|
||||
|
||||
## Available Tools (Default)
|
||||
|
||||
When the MCP server is enabled, AI agents have access to the following tools:
|
||||
|
||||
| Name | Description |
|
||||
| :-------------------------- | :-------------------------------------------------------------------------------------------------------- |
|
||||
| `ai_tutor` | Launches an interactive AI-powered Angular tutor. |
|
||||
| `find_examples` | Finds authoritative, best-practice code examples for modern Angular features. |
|
||||
| `get_best_practices` | Retrieves the Angular Best Practices Guide (crucial for standalone components, typed forms, etc.). |
|
||||
| `list_projects` | Lists all applications and libraries in the workspace by reading `angular.json`. |
|
||||
| `onpush_zoneless_migration` | Analyzes code and provides a plan to migrate it to `OnPush` change detection (prerequisite for zoneless). |
|
||||
| `search_documentation` | Searches the official documentation at `https://angular.dev`. |
|
||||
|
||||
## Experimental Tools
|
||||
|
||||
Some tools must be enabled explicitly using the `--experimental-tool` (or `-E`) flag.
|
||||
|
||||
| Name | Description |
|
||||
| :------------------------- | :----------------------------------------------------------------------- |
|
||||
| `build` | Performs a one-off build using `ng build`. |
|
||||
| `devserver.start` | Asynchronously starts a dev server (`ng serve`). Returns immediately. |
|
||||
| `devserver.stop` | Stops the dev server. |
|
||||
| `devserver.wait_for_build` | Returns the logs of the most recent build in a running dev server. |
|
||||
| `e2e` | Executes end-to-end tests. |
|
||||
| `modernize` | Performs code migrations to align with latest best practices and syntax. |
|
||||
| `test` | Runs the project's unit tests. |
|
||||
|
||||
## Configuration
|
||||
|
||||
To use the MCP server, you configure your host environment (IDE or CLI) to run `npx @angular/cli mcp`.
|
||||
|
||||
### Antigravity IDE
|
||||
|
||||
Create a file named `.antigravity/mcp.json` in your project's root:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"angular-cli": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@angular/cli", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Gemini CLI
|
||||
|
||||
Create `.gemini/settings.json` in the project root:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"angular-cli": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@angular/cli", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cursor
|
||||
|
||||
Create `.cursor/mcp.json` in the project root (or globally at `~/.cursor/mcp.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"angular-cli": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@angular/cli", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### VS Code
|
||||
|
||||
Create `.vscode/mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"angular-cli": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@angular/cli", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Command Options
|
||||
|
||||
You can pass arguments to the MCP server in the `args` array of your configuration:
|
||||
|
||||
- `--read-only`: Only registers tools that do not modify the project.
|
||||
- `--local-only`: Only registers tools that do not require an internet connection.
|
||||
- `--experimental-tool` (`-E`): Enables specific experimental tools (e.g., `-E build`, `-E devserver`).
|
||||
|
||||
Example for read-only mode with experimental tools enabled:
|
||||
|
||||
```json
|
||||
"args": ["-y", "@angular/cli", "mcp", "--read-only", "-E", "build", "-E", "modernize"]
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
# Navigate to Routes
|
||||
|
||||
Angular provides both declarative and programmatic ways to navigate between routes.
|
||||
|
||||
## Declarative Navigation (`RouterLink`)
|
||||
|
||||
Use the `RouterLink` directive on anchor elements.
|
||||
|
||||
```ts
|
||||
import {RouterLink, RouterLinkActive} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
imports: [RouterLink, RouterLinkActive],
|
||||
template: `
|
||||
<nav>
|
||||
<a routerLink="/dashboard" routerLinkActive="active-link">Dashboard</a>
|
||||
<a [routerLink]="['/user', userId]">Profile</a>
|
||||
</nav>
|
||||
`,
|
||||
})
|
||||
export class Nav {
|
||||
userId = '123';
|
||||
}
|
||||
```
|
||||
|
||||
- **Absolute Paths**: Start with `/` (e.g., `/settings`).
|
||||
- **Relative Paths**: No leading `/`. Use `../` to go up a level.
|
||||
|
||||
## Programmatic Navigation (`Router`)
|
||||
|
||||
Inject the `Router` service to navigate via TypeScript code.
|
||||
|
||||
### `router.navigate()`
|
||||
|
||||
Uses an array of commands.
|
||||
|
||||
```ts
|
||||
private router = inject(Router);
|
||||
private route = inject(ActivatedRoute);
|
||||
|
||||
// Standard navigation
|
||||
this.router.navigate(['/profile']);
|
||||
|
||||
// With parameters
|
||||
this.router.navigate(['/search'], {
|
||||
queryParams: { q: 'angular' },
|
||||
fragment: 'results'
|
||||
});
|
||||
|
||||
// Relative navigation
|
||||
this.router.navigate(['edit'], { relativeTo: this.route });
|
||||
```
|
||||
|
||||
### `router.navigateByUrl()`
|
||||
|
||||
Uses a string path. Ideal for absolute navigation or full URLs.
|
||||
|
||||
```ts
|
||||
this.router.navigateByUrl('/products/123?view=details');
|
||||
|
||||
// Replace current entry in history
|
||||
this.router.navigateByUrl('/login', {replaceUrl: true});
|
||||
```
|
||||
|
||||
## URL Parameters
|
||||
|
||||
- **Route Params**: Part of the path (e.g., `/user/123`).
|
||||
- **Query Params**: After the `?` (e.g., `/search?q=query`).
|
||||
- **Matrix Params**: Scoped to a segment (e.g., `/products;category=books`).
|
||||
@@ -0,0 +1,86 @@
|
||||
# Outputs (Custom Events)
|
||||
|
||||
Outputs allow a child component to emit custom events that a parent component can listen to. Angular recommends using the new `output()` function for modern applications.
|
||||
|
||||
## Function-based outputs
|
||||
|
||||
Declare outputs using the `output()` function. This returns an `OutputEmitterRef`.
|
||||
|
||||
```ts
|
||||
import {Component, output} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'custom-slider',
|
||||
template: `<button (click)="changeValue(50)">Set to 50</button>`,
|
||||
})
|
||||
export class CustomSlider {
|
||||
// Output without event data
|
||||
panelClosed = output<void>();
|
||||
|
||||
// Output with event data (number)
|
||||
valueChanged = output<number>();
|
||||
|
||||
changeValue(newValue: number) {
|
||||
this.valueChanged.emit(newValue);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage in Template
|
||||
|
||||
Bind to the output event using parentheses `()`. If the event emits data, access it using the special `$event` variable.
|
||||
|
||||
```html
|
||||
<custom-slider (panelClosed)="savePanelState()" (valueChanged)="logValue($event)" />
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The `output` function accepts a config object to specify an alias.
|
||||
|
||||
```ts
|
||||
@Component({...})
|
||||
export class CustomSlider {
|
||||
// The event is named 'valueChanged' in the template,
|
||||
// but accessed as 'changed' in the component class.
|
||||
changed = output<number>({ alias: 'valueChanged' });
|
||||
}
|
||||
```
|
||||
|
||||
## Programmatic Subscription
|
||||
|
||||
When creating components dynamically, you can subscribe to outputs programmatically:
|
||||
|
||||
```ts
|
||||
const componentRef = viewContainerRef.createComponent(CustomSlider);
|
||||
|
||||
const subscription = componentRef.instance.valueChanged.subscribe((val) => {
|
||||
console.log('Value changed:', val);
|
||||
});
|
||||
|
||||
// Clean up manually if needed (Angular cleans up destroyed components automatically)
|
||||
subscription.unsubscribe();
|
||||
```
|
||||
|
||||
## Decorator-based Outputs (@Output)
|
||||
|
||||
The legacy API uses the `@Output()` decorator with an `EventEmitter`. It remains supported but is not recommended for new code.
|
||||
|
||||
```ts
|
||||
import { Component, Output, EventEmitter } from '@angular/core';
|
||||
|
||||
@Component({...})
|
||||
export class LegacyExample {
|
||||
@Output() valueChanged = new EventEmitter<number>();
|
||||
|
||||
// With alias
|
||||
@Output('customEventName') changed = new EventEmitter<void>();
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Prefer `output()`**: Use the function-based `output()` instead of `@Output()` and `EventEmitter`.
|
||||
- **Naming**: Use `camelCase` for output names. Avoid prefixing with `on` (e.g., use `valueChanged` instead of `onValueChanged`).
|
||||
- **No DOM Bubbling**: Angular custom events do not bubble up the DOM tree like native events.
|
||||
- **Avoid Collisions**: Do not choose names that collide with native DOM events (like `click` or `submit`).
|
||||
@@ -0,0 +1,122 @@
|
||||
# Reactive Forms
|
||||
|
||||
Reactive forms provide a model-driven approach to handling form inputs. They are built around observable streams and provide synchronous access to the data model, making them more scalable and testable than template-driven forms.
|
||||
|
||||
## Core Classes
|
||||
|
||||
Reactive forms are built using these fundamental classes from `@angular/forms`:
|
||||
|
||||
- `FormControl`: Manages the value and validity of an individual input.
|
||||
- `FormGroup`: Manages a group of controls (an object-like structure).
|
||||
- `FormArray`: Manages a numerically indexed array of controls.
|
||||
- `FormBuilder`: A service that provides factory methods for creating control instances.
|
||||
|
||||
## Setup
|
||||
|
||||
Import `ReactiveFormsModule` into your component.
|
||||
|
||||
```ts
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {ReactiveFormsModule, FormGroup, FormControl, Validators, FormBuilder} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-profile-editor',
|
||||
imports: [ReactiveFormsModule],
|
||||
templateUrl: './profile-editor.component.html',
|
||||
})
|
||||
export class ProfileEditor {
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
// Using FormBuilder for concise definition
|
||||
profileForm = this.fb.group({
|
||||
firstName: ['', Validators.required],
|
||||
lastName: [''],
|
||||
address: this.fb.group({
|
||||
street: [''],
|
||||
city: [''],
|
||||
}),
|
||||
aliases: this.fb.array([this.fb.control('')]),
|
||||
});
|
||||
|
||||
onSubmit() {
|
||||
console.warn(this.profileForm.value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Template Binding
|
||||
|
||||
Use directives to bind the model to the view:
|
||||
|
||||
- `[formGroup]`: Binds a `FormGroup` to a `<form>` or `<div>`.
|
||||
- `formControlName`: Binds a named control within a group to an input.
|
||||
- `formGroupName`: Binds a nested `FormGroup`.
|
||||
- `formArrayName`: Binds a nested `FormArray`.
|
||||
- `[formControl]`: Binds a standalone `FormControl`.
|
||||
|
||||
```html
|
||||
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
|
||||
<input type="text" formControlName="firstName" />
|
||||
|
||||
<div formGroupName="address">
|
||||
<input type="text" formControlName="street" />
|
||||
</div>
|
||||
|
||||
<div formArrayName="aliases">
|
||||
@for (alias of aliases.controls; track $index) {
|
||||
<input type="text" [formControlName]="$index" />
|
||||
}
|
||||
</div>
|
||||
|
||||
<button type="submit" [disabled]="!profileForm.valid">Submit</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
## Accessing Controls
|
||||
|
||||
Use getters for easy access to controls, especially for `FormArray`.
|
||||
|
||||
```ts
|
||||
get aliases() {
|
||||
return this.profileForm.get('aliases') as FormArray;
|
||||
}
|
||||
|
||||
addAlias() {
|
||||
this.aliases.push(this.fb.control(''));
|
||||
}
|
||||
```
|
||||
|
||||
## Updating Values
|
||||
|
||||
- `patchValue()`: Updates only the specified properties. Fails silently on structural mismatches.
|
||||
- `setValue()`: Replaces the entire model. Strictly enforces the form structure.
|
||||
|
||||
```ts
|
||||
updateProfile() {
|
||||
this.profileForm.patchValue({
|
||||
firstName: 'Nancy',
|
||||
address: { street: '123 Drew Street' }
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Unified Change Events
|
||||
|
||||
Modern Angular (v18+) provides a single `events` observable on all controls to track value, status, pristine, touched, reset, and submit events.
|
||||
|
||||
```ts
|
||||
import {ValueChangeEvent, StatusChangeEvent} from '@angular/forms';
|
||||
|
||||
this.profileForm.events.subscribe((event) => {
|
||||
if (event instanceof ValueChangeEvent) {
|
||||
console.log('New value:', event.value);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Manual State Management
|
||||
|
||||
- `markAsTouched()` / `markAllAsTouched()`: Useful for showing validation errors on submit.
|
||||
- `markAsDirty()` / `markAsPristine()`: Tracks if the value has been modified.
|
||||
- `updateValueAndValidity()`: Manually triggers recalculation of value and status.
|
||||
- Options `{ emitEvent: false }` or `{ onlySelf: true }` can be passed to most methods to control propagation.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Rendering Strategies
|
||||
|
||||
Angular supports multiple rendering strategies to optimize for SEO, performance, and interactivity.
|
||||
|
||||
## 1. Client-Side Rendering (CSR)
|
||||
|
||||
**Default Strategy.** Content is rendered entirely in the browser.
|
||||
|
||||
- **Use case**: Interactive dashboards, internal tools.
|
||||
- **Pros**: Simplest to configure, low server cost.
|
||||
- **Cons**: Poor SEO, slower initial content visibility (must wait for JS).
|
||||
|
||||
## 2. Static Site Generation (SSG / Prerendering)
|
||||
|
||||
Content is pre-rendered into static HTML files at **build time**.
|
||||
|
||||
- **Use case**: Marketing pages, blogs, documentation.
|
||||
- **Pros**: Fastest initial load, excellent SEO, CDN-friendly.
|
||||
- **Cons**: Requires rebuild for content updates, not for user-specific data.
|
||||
|
||||
## 3. Server-Side Rendering (SSR)
|
||||
|
||||
Content is rendered on the server for the **initial request**. Subsequent navigations happen client-side (SPA style).
|
||||
|
||||
- **Use case**: E-commerce product pages, news sites, personalized dynamic content.
|
||||
- **Pros**: Excellent SEO, fast initial content visibility.
|
||||
- **Cons**: Requires a server (Node.js), higher server cost/latency.
|
||||
|
||||
## Hydration
|
||||
|
||||
Hydration is the process of making server-rendered HTML interactive in the browser.
|
||||
|
||||
- **Full Hydration**: The entire app becomes interactive at once.
|
||||
- **Incremental Hydration**: (Advanced) Parts become interactive as needed using `@defer` blocks.
|
||||
- **Event Replay**: Captures and replays user events that happened before hydration finished.
|
||||
|
||||
## Decision Matrix
|
||||
|
||||
| Requirement | Strategy |
|
||||
| :------------------------------ | :------------------- |
|
||||
| **SEO + Static Content** | SSG |
|
||||
| **SEO + Dynamic Content** | SSR |
|
||||
| **No SEO + High Interactivity** | CSR |
|
||||
| **Mixed** | Hybrid (Route-based) |
|
||||
@@ -0,0 +1,77 @@
|
||||
# Async Reactivity with `resource`
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The `resource` API is currently experimental in Angular.
|
||||
|
||||
A `Resource` incorporates asynchronous data fetching into Angular's signal-based reactivity. It executes an async loader function whenever its dependencies change, exposing the status and result as synchronous signals.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
The `resource` function accepts an options object with two main properties:
|
||||
|
||||
1. `params`: A reactive computation (like `computed`). When signals read here change, the resource re-fetches.
|
||||
2. `loader`: An async function that fetches data based on the parameters.
|
||||
|
||||
```ts
|
||||
import { Component, resource, signal, computed } from '@angular/core';
|
||||
|
||||
@Component({...})
|
||||
export class UserProfile {
|
||||
userId = signal('123');
|
||||
|
||||
userResource = resource({
|
||||
// Reactively tracking userId
|
||||
params: () => ({ id: this.userId() }),
|
||||
|
||||
// Executes whenever params change
|
||||
loader: async ({ params, abortSignal }) => {
|
||||
const response = await fetch(`/api/users/${params.id}`, { signal: abortSignal });
|
||||
if (!response.ok) throw new Error('Network error');
|
||||
return response.json();
|
||||
}
|
||||
});
|
||||
|
||||
// Use the resource value in computed signals
|
||||
userName = computed(() => {
|
||||
if (this.userResource.hasValue()) {
|
||||
return this.userResource.value()?.name;
|
||||
} else {
|
||||
return 'Loading...';
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Aborting Requests
|
||||
|
||||
If the `params` signal changes while a previous loader is still running, the `Resource` will attempt to abort the outstanding request using the provided `abortSignal`. **Always pass `abortSignal` to your `fetch` calls.**
|
||||
|
||||
## Reloading Data
|
||||
|
||||
You can imperatively force the resource to re-run the loader without the params changing by calling `.reload()`.
|
||||
|
||||
```ts
|
||||
this.userResource.reload();
|
||||
```
|
||||
|
||||
## Resource Status Signals
|
||||
|
||||
The `Resource` object provides several signals to read its current state:
|
||||
|
||||
- `value()`: The resolved data, or `undefined`.
|
||||
- `hasValue()`: Type-guard boolean. `true` if a value exists.
|
||||
- `isLoading()`: Boolean indicating if the loader is currently running.
|
||||
- `error()`: The error thrown by the loader, or `undefined`.
|
||||
- `status()`: A string constant representing the exact state (`'idle'`, `'loading'`, `'resolved'`, `'error'`, `'reloading'`, `'local'`).
|
||||
|
||||
## Local Mutation
|
||||
|
||||
You can optimistically update the resource's value directly. This changes the status to `'local'`.
|
||||
|
||||
```ts
|
||||
this.userResource.value.set({name: 'Optimistic Update'});
|
||||
```
|
||||
|
||||
## Reactive Data Fetching with `httpResource`
|
||||
|
||||
If you are using Angular's `HttpClient`, prefer using `httpResource`. It is a specialized wrapper that leverages the Angular HTTP stack (including interceptors) while providing the same signal-based resource API.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Route Transition Animations
|
||||
|
||||
Angular Router supports the browser's **View Transitions API** for smooth visual transitions between routes.
|
||||
|
||||
## Enabling View Transitions
|
||||
|
||||
Add `withViewTransitions()` to your router configuration.
|
||||
|
||||
```ts
|
||||
provideRouter(routes, withViewTransitions());
|
||||
```
|
||||
|
||||
This is a **progressive enhancement**. In browsers that don't support the API, the router will still work but without the transition animation.
|
||||
|
||||
## How it Works
|
||||
|
||||
1. Browser takes a screenshot of the old state.
|
||||
2. Router updates the DOM (activates new component).
|
||||
3. Browser takes a screenshot of the new state.
|
||||
4. Browser animates between the two states.
|
||||
|
||||
## Customizing with CSS
|
||||
|
||||
Transitions are customized in **global CSS files** (not component-scoped CSS).
|
||||
|
||||
Use the `::view-transition-old()` and `::view-transition-new()` pseudo-elements.
|
||||
|
||||
```css
|
||||
/* Example: Cross-fade + Slide */
|
||||
::view-transition-old(root) {
|
||||
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out;
|
||||
}
|
||||
::view-transition-new(root) {
|
||||
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in;
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Control
|
||||
|
||||
Use `onViewTransitionCreated` to skip transitions or customize behavior based on the navigation context.
|
||||
|
||||
```ts
|
||||
withViewTransitions({
|
||||
onViewTransitionCreated: ({transition, from, to}) => {
|
||||
// Skip animation for specific routes
|
||||
if (to.url === '/no-animation') {
|
||||
transition.skipTransition();
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Global Styles**: Always define transition animations in `styles.css` to avoid view encapsulation issues.
|
||||
- **View Transition Names**: Assign unique `view-transition-name` to elements that should transition smoothly across routes (e.g., a header image).
|
||||
@@ -0,0 +1,52 @@
|
||||
# Route Guards
|
||||
|
||||
Route guards control whether a user can navigate to or leave a route.
|
||||
|
||||
## Types of Guards
|
||||
|
||||
- **`CanActivate`**: Can the user access this route? (e.g., Auth check).
|
||||
- **`CanActivateChild`**: Can the user access children of this route?
|
||||
- **`CanDeactivate`**: Can the user leave this route? (e.g., Unsaved changes).
|
||||
- **`CanMatch`**: Should this route even be considered for matching? (e.g., Feature flags). If it returns `false`, the router continues checking other routes.
|
||||
|
||||
## Creating a Guard
|
||||
|
||||
Guards are typically functional since Angular 15.
|
||||
|
||||
```ts
|
||||
export const authGuard: CanActivateFn = (route, state) => {
|
||||
const authService = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
if (authService.isLoggedIn()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Redirect to login
|
||||
return router.parseUrl('/login');
|
||||
};
|
||||
```
|
||||
|
||||
## Applying Guards
|
||||
|
||||
Add them to the route configuration as an array. They execute in order.
|
||||
|
||||
```ts
|
||||
{
|
||||
path: 'admin',
|
||||
component: Admin,
|
||||
canActivate: [authGuard],
|
||||
canActivateChild: [adminChildGuard],
|
||||
canDeactivate: [unsavedChangesGuard]
|
||||
}
|
||||
```
|
||||
|
||||
## Return Values
|
||||
|
||||
- `boolean`: `true` to allow, `false` to block.
|
||||
- `UrlTree` or `RedirectCommand`: Redirect to a different route.
|
||||
- `Observable` or `Promise`: Resolves to the above types.
|
||||
|
||||
## Security Note
|
||||
|
||||
**Client-side guards are NOT a substitute for server-side security.** Always verify permissions on the server.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Router Lifecycle and Events
|
||||
|
||||
Angular Router emits events through the `Router.events` observable, allowing you to track the navigation lifecycle from start to finish.
|
||||
|
||||
## Common Router Events (Chronological)
|
||||
|
||||
1. **`NavigationStart`**: Navigation begins.
|
||||
2. **`RoutesRecognized`**: Router matches the URL to a route.
|
||||
3. **`GuardsCheckStart` / `End`**: Evaluation of `canActivate`, `canMatch`, etc.
|
||||
4. **`ResolveStart` / `End`**: Data resolution phase (fetching data via resolvers).
|
||||
5. **`NavigationEnd`**: Navigation completed successfully.
|
||||
6. **`NavigationCancel`**: Navigation canceled (e.g., guard returned `false`).
|
||||
7. **`NavigationError`**: Navigation failed (e.g., error in resolver).
|
||||
|
||||
## Subscribing to Events
|
||||
|
||||
Inject the `Router` and filter the `events` observable.
|
||||
|
||||
```ts
|
||||
import {Router, NavigationStart, NavigationEnd} from '@angular/router';
|
||||
|
||||
export class MyService {
|
||||
private router = inject(Router);
|
||||
|
||||
constructor() {
|
||||
this.router.events.pipe(filter((e) => e instanceof NavigationEnd)).subscribe((event) => {
|
||||
console.log('Navigated to:', event.url);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
Enable detailed console logging of all routing events during application bootstrap.
|
||||
|
||||
```ts
|
||||
provideRouter(routes, withDebugTracing());
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
- **Loading Indicators**: Show a spinner when `NavigationStart` fires and hide it on `NavigationEnd`/`Cancel`/`Error`.
|
||||
- **Analytics**: Track page views by listening for `NavigationEnd`.
|
||||
- **Scroll Management**: Respond to `Scroll` events for custom scroll behavior.
|
||||
@@ -0,0 +1,87 @@
|
||||
# Testing with the RouterTestingHarness
|
||||
|
||||
When testing components that involve routing, it is crucial **not to mock the Router or related services**. Instead, use the `RouterTestingHarness`, which provides a robust and reliable way to test routing logic in an environment that closely mirrors a real application.
|
||||
|
||||
Using the harness ensures you are testing the actual router configuration, guards, and resolvers, leading to more meaningful tests.
|
||||
|
||||
## Setting Up for Router Testing
|
||||
|
||||
The `RouterTestingHarness` is the primary tool for testing routing scenarios. You also need to provide your test routes using the `provideRouter` function in your `TestBed` configuration.
|
||||
|
||||
### Example Setup
|
||||
|
||||
```ts
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {provideRouter} from '@angular/router';
|
||||
import {RouterTestingHarness} from '@angular/router/testing';
|
||||
import {Dashboard} from './dashboard.component';
|
||||
import {HeroDetail} from './hero-detail.component';
|
||||
|
||||
describe('Dashboard Component Routing', () => {
|
||||
let harness: RouterTestingHarness;
|
||||
|
||||
beforeEach(async () => {
|
||||
// 1. Configure TestBed with test routes
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
// Use provideRouter with your test-specific routes
|
||||
provideRouter([
|
||||
{path: '', component: Dashboard},
|
||||
{path: 'heroes/:id', component: HeroDetail},
|
||||
]),
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
// 2. Create the RouterTestingHarness
|
||||
harness = await RouterTestingHarness.create();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Key Concepts
|
||||
|
||||
1. **`provideRouter([...])`**: Provide a test-specific routing configuration. This should include the routes necessary for the component-under-test to function correctly.
|
||||
2. **`RouterTestingHarness.create()`**: Asynchronously creates and initializes the harness and performs an initial navigation to the root URL (`/`).
|
||||
|
||||
## Writing Router Tests
|
||||
|
||||
Once the harness is created, you can use it to drive navigation and make assertions on the state of the router and the activated components.
|
||||
|
||||
### Example: Testing Navigation
|
||||
|
||||
```ts
|
||||
it('should navigate to a hero detail when a hero is selected', async () => {
|
||||
// 1. Navigate to the initial component and get its instance
|
||||
const dashboard = await harness.navigateByUrl('/', Dashboard);
|
||||
|
||||
// Suppose the dashboard has a method to select a hero
|
||||
const heroToSelect = {id: 42, name: 'Test Hero'};
|
||||
dashboard.selectHero(heroToSelect);
|
||||
|
||||
// Wait for stability after the action that triggers navigation
|
||||
await harness.fixture.whenStable();
|
||||
|
||||
// 2. Assert on the URL
|
||||
expect(harness.router.url).toEqual('/heroes/42');
|
||||
|
||||
// 3. Get the activated component after navigation
|
||||
const heroDetail = await harness.getHarness(HeroDetail);
|
||||
|
||||
// 4. Assert on the state of the new component
|
||||
expect(await heroDetail.componentInstance.hero.name).toBe('Test Hero');
|
||||
});
|
||||
|
||||
it('should get the activated component directly', async () => {
|
||||
// Navigate and get the component instance in one step
|
||||
const dashboardInstance = await harness.navigateByUrl('/', Dashboard);
|
||||
|
||||
expect(dashboardInstance).toBeInstanceOf(Dashboard);
|
||||
});
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
- **Navigate with the Harness:** Always use `harness.navigateByUrl()` to simulate navigation. This method returns a promise that resolves with the instance of the activated component.
|
||||
- **Access the Router State:** Use `harness.router` to access the live router instance and assert on its state (e.g., `harness.router.url`).
|
||||
- **Get Activated Components:** Use `harness.getHarness(ComponentType)` to get an instance of a component harness for the currently activated routed component, or `harness.routeDebugElement` to get the `DebugElement`.
|
||||
- **Wait for Stability:** After performing an action that causes navigation, always `await harness.fixture.whenStable()` to ensure the routing is complete before making assertions.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Show Routes with Outlets
|
||||
|
||||
The `RouterOutlet` directive is a placeholder where Angular renders the component for the current URL.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Include `<router-outlet />` in your template. Angular inserts the routed component as a sibling immediately following the outlet.
|
||||
|
||||
```html
|
||||
<app-header /> <router-outlet />
|
||||
<!-- Route content appears here -->
|
||||
<app-footer />
|
||||
```
|
||||
|
||||
## Nested Outlets
|
||||
|
||||
Child routes require their own `<router-outlet />` within the parent component's template.
|
||||
|
||||
```ts
|
||||
// Parent component template
|
||||
<h1>Settings</h1>
|
||||
<router-outlet /> <!-- Child components like Profile or Security render here -->
|
||||
```
|
||||
|
||||
## Named Outlets (Secondary Routes)
|
||||
|
||||
Pages can have multiple outlets. Assign a `name` to an outlet to target it specifically. The default name is `'primary'`.
|
||||
|
||||
```html
|
||||
<router-outlet />
|
||||
<!-- Primary -->
|
||||
<router-outlet name="sidebar" />
|
||||
<!-- Secondary -->
|
||||
```
|
||||
|
||||
Define the `outlet` in the route config:
|
||||
|
||||
```ts
|
||||
{
|
||||
path: 'chat',
|
||||
component: Chat,
|
||||
outlet: 'sidebar'
|
||||
}
|
||||
```
|
||||
|
||||
## Outlet Lifecycle Events
|
||||
|
||||
`RouterOutlet` emits events when components are changed:
|
||||
|
||||
- `activate`: New component instantiated.
|
||||
- `deactivate`: Component destroyed.
|
||||
- `attach` / `detach`: Used with `RouteReuseStrategy`.
|
||||
|
||||
```html
|
||||
<router-outlet (activate)="onActivate($event)" />
|
||||
```
|
||||
|
||||
## Passing Data via `routerOutletData`
|
||||
|
||||
You can pass contextual data to the routed component using the `routerOutletData` input. The component accesses this via the `ROUTER_OUTLET_DATA` injection token as a signal.
|
||||
|
||||
```ts
|
||||
// In Parent
|
||||
<router-outlet [routerOutletData]="{ theme: 'dark' }" />
|
||||
|
||||
// In Routed Component
|
||||
outletData = inject(ROUTER_OUTLET_DATA) as Signal<{ theme: string }>;
|
||||
```
|
||||
@@ -0,0 +1,795 @@
|
||||
# Signal Forms
|
||||
|
||||
Signal Forms are recommended for new forms when the target Angular version supports them. They provide a reactive, type-safe, and model-driven way to manage form state using Angular Signals.
|
||||
|
||||
When using Signal Forms, do not use `null` as a value or type of any fields.
|
||||
|
||||
## Imports
|
||||
|
||||
You can import the following from `@angular/forms/signals`:
|
||||
|
||||
```ts
|
||||
import {
|
||||
form,
|
||||
FormField,
|
||||
submit,
|
||||
// Rules for field state
|
||||
disabled,
|
||||
hidden,
|
||||
readonly,
|
||||
debounce,
|
||||
// Schema helpers
|
||||
applyWhen,
|
||||
applyEach,
|
||||
schema,
|
||||
// Custom validation
|
||||
validate,
|
||||
validateHttp,
|
||||
validateStandardSchema,
|
||||
// Metadata
|
||||
metadata,
|
||||
} from '@angular/forms/signals';
|
||||
```
|
||||
|
||||
## Creating a Form
|
||||
|
||||
Use the `form()` function with a Signal model. The structure of the form is derived directly from the model.
|
||||
|
||||
```ts
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {form, FormField} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
// ...
|
||||
imports: [FormField],
|
||||
})
|
||||
export class Example {
|
||||
// 1. Define your model with initial values (avoid undefined)
|
||||
userModel = signal({
|
||||
name: '', // CRITICAL: NEVER use null or undefined as initial values
|
||||
email: '',
|
||||
age: 0, // Use 0 for numbers, NOT null
|
||||
address: {
|
||||
street: '',
|
||||
city: '',
|
||||
},
|
||||
hobbies: [] as string[], // Use [] for arrays, NOT null
|
||||
});
|
||||
|
||||
// WRONG - DO NOT DO THIS:
|
||||
// badModel = signal({
|
||||
// name: null, // ERROR: use '' instead
|
||||
// age: null, // ERROR: use 0 instead
|
||||
// items: null // ERROR: use [] instead
|
||||
// });
|
||||
|
||||
// 2. Create the form
|
||||
userForm = form(this.userModel);
|
||||
}
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
Import validators from `@angular/forms/signals`.
|
||||
|
||||
```ts
|
||||
import {required, email, min, max, minLength, maxLength, pattern} from '@angular/forms/signals';
|
||||
```
|
||||
|
||||
Use them in the schema function passed to `form()`:
|
||||
|
||||
```ts
|
||||
userForm = form(this.userModel, (schemaPath) => {
|
||||
// Required
|
||||
required(schemaPath.name, {message: 'Name is required'});
|
||||
|
||||
// Conditional required.
|
||||
required(schemaPath.name, {
|
||||
when({valueOf}) {
|
||||
return valueOf(schemaPath.age) > 10;
|
||||
},
|
||||
});
|
||||
// when is only available for required
|
||||
// Do NOT do this: pattern(p.name, /xxx/, {when /* ERROR */)
|
||||
|
||||
// Email
|
||||
email(schemaPath.email, {message: 'Invalid email'});
|
||||
|
||||
// Min/Max for numbers
|
||||
min(schemaPath.age, 18);
|
||||
max(schemaPath.age, 100);
|
||||
|
||||
// MinLength/MaxLength for strings/arrays
|
||||
minLength(schemaPath.password, 8);
|
||||
maxLength(schemaPath.description, 500);
|
||||
|
||||
// Pattern (Regex)
|
||||
pattern(schemaPath.zipCode, /^\d{5}$/);
|
||||
});
|
||||
```
|
||||
|
||||
## FieldState vs FormField: The Parental Requirement
|
||||
|
||||
It's important to understand the difference between **FormField** (the structure) and **FieldState** (the actual data/signals).
|
||||
|
||||
**RULE**: You must **CALL** a field as a function to access its state signals (valid, touched, dirty, hidden, etc.).
|
||||
|
||||
```ts
|
||||
// f is a FormField (structural)
|
||||
const f = form(signal({cat: {name: 'pirojok-the-cat', age: 5}}));
|
||||
|
||||
f.cat.name; // FormField: You can't get flags from here!
|
||||
f.cat.name.touched(); // ERROR: touched() does not exist on FormField
|
||||
|
||||
f.cat.name(); // FieldState: Calling it gives you access to signals
|
||||
f.cat.name().touched(); // VALID: Accessing the signal
|
||||
f.cat().name.touched(); // ERROR: f.cat() is state, it doesn't have children!
|
||||
```
|
||||
|
||||
Similarly in a template:
|
||||
|
||||
```html
|
||||
<!-- WRONG: Property 'hidden' does not exist on type 'FormField' -->
|
||||
@if (bookingForm.hotelDetails.hidden()) { ... }
|
||||
|
||||
<!-- RIGHT: Call it first -->
|
||||
@if (bookingForm.hotelDetails().hidden()) { ... }
|
||||
```
|
||||
|
||||
## Disabled / Readonly / Hidden
|
||||
|
||||
Control field status using rules in the schema.
|
||||
|
||||
```ts
|
||||
import {disabled, readonly, hidden} from '@angular/forms/signals';
|
||||
|
||||
userForm = form(this.userModel, (schemaPath) => {
|
||||
// Conditionally disabled
|
||||
disabled(schemaPath.password, ({valueOf}) => !valueOf(schemaPath.createAccount));
|
||||
|
||||
// Conditionally hidden (does NOT remove from model, just marks as hidden)
|
||||
hidden(schemaPath.shippingAddress, ({valueOf}) => valueOf(schemaPath.sameAsBilling));
|
||||
|
||||
// Readonly
|
||||
readonly(schemaPath.username);
|
||||
});
|
||||
```
|
||||
|
||||
## Binding
|
||||
|
||||
Import `FormField` and use the `[formField]` directive.
|
||||
|
||||
```ts
|
||||
import {FormField} from '@angular/forms/signals';
|
||||
```
|
||||
|
||||
All props on state, such as `disabled`, `hidden`, `readonly` and `name` are bound automatically.
|
||||
Do _NOT_ bind the `name` field.
|
||||
|
||||
**CRITICAL: FORBIDDEN ATTRIBUTES**
|
||||
When using `[formField]`, you MUST NOT set the following attributes in the template (either static or bound):
|
||||
|
||||
- `min`, `max` (Use validators in the schema instead)
|
||||
- `value`, `[value]`, `[attr.value]` (Already handled by `[formField]`)
|
||||
- `[attr.min]`, `[attr.max]`
|
||||
- `[disabled]`, `[readonly]` (Already handled by `[formField]`)
|
||||
|
||||
Do NOT do this: `<input min="1" [formField]>` or `<input [value]="val" [formField]>`.
|
||||
|
||||
```html
|
||||
<!-- Input -->
|
||||
<input [formField]="userForm.name" />
|
||||
|
||||
<!-- Checkbox -->
|
||||
<input type="checkbox" [formField]="userForm.isAdmin" />
|
||||
|
||||
<!-- Select -->
|
||||
<select [formField]="userForm.country">
|
||||
<option value="us">US</option>
|
||||
</select>
|
||||
|
||||
<!-- userForm.name can NOT be nullable, because input does not accept null-->
|
||||
<input [formField]="userForm.name" />
|
||||
```
|
||||
|
||||
## Reactive Forms
|
||||
|
||||
**Do NOT import** `FormControl`, `FormGroup`, `FormArray`, or `FormBuilder` from `@angular/forms`. Signal Forms replace these concepts entirely.
|
||||
Signal forms does NOT have a builder.
|
||||
|
||||
## Accessing State
|
||||
|
||||
Each field in the form is a function that returns its state.
|
||||
|
||||
```ts
|
||||
// Access the field by calling it
|
||||
const emailState = this.userForm.email();
|
||||
|
||||
// Value (WritableSignal)
|
||||
const value = this.userForm().value();
|
||||
|
||||
// Validation State (Signals)
|
||||
const isValid = this.userForm().valid();
|
||||
const isInvalid = this.userForm().invalid();
|
||||
const errors = this.userForm().errors(); // Array of errors
|
||||
const isPending = this.userForm().pending(); // Async validation pending
|
||||
|
||||
// Interaction State (Signals)
|
||||
const isTouched = this.userForm().touched();
|
||||
const isDirty = this.userForm().dirty();
|
||||
|
||||
// Availability State (Signals)
|
||||
const isDisabled = this.userForm().disabled();
|
||||
const isHidden = this.userForm().hidden();
|
||||
const isReadonly = this.userForm().readonly();
|
||||
```
|
||||
|
||||
IMPORTANT!: Make sure to call the field to get it state.
|
||||
|
||||
```ts
|
||||
form().invalid()
|
||||
form.field().dirty()
|
||||
form.field.subfield().touched()
|
||||
form.a.b.c.d().value()
|
||||
form.address.ssn().pending()
|
||||
form().reset()
|
||||
|
||||
// The only exception is length:
|
||||
form.children.length
|
||||
form.length // NOTE: no parenthesis!
|
||||
form.client.addresses.length // No "()"
|
||||
|
||||
@for (income of form.addresses; track $index) {/**/}
|
||||
```
|
||||
|
||||
## Submitting
|
||||
|
||||
Use the `submit()` function. It automatically marks all fields as touched before running the action.
|
||||
|
||||
**CRITICAL**: The callback to `submit()` MUST be `async` and MUST return a Promise.
|
||||
|
||||
```ts
|
||||
import { submit } from '@angular/forms/signals';
|
||||
|
||||
// CORRECT - async callback
|
||||
onSubmit() {
|
||||
submit(this.userForm, async () => {
|
||||
// This only runs if the form is valid
|
||||
await this.apiService.save(this.userModel());
|
||||
console.log('Saved!');
|
||||
});
|
||||
}
|
||||
|
||||
// WRONG - missing async keyword
|
||||
onSubmit() {
|
||||
submit(this.userForm, () => { // ERROR: must be async
|
||||
console.log('Saved!');
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Handling Errors
|
||||
|
||||
`field().errors()` returns the errors array of ValidationError:
|
||||
|
||||
```ts
|
||||
interface ValidationError {
|
||||
readonly kind: string;
|
||||
readonly message?: string;
|
||||
}
|
||||
```
|
||||
|
||||
Do _NOT_ return null from validators.
|
||||
When there are no errors, return undefined
|
||||
|
||||
### Context
|
||||
|
||||
Functions passed to rules like `validate()`, `disabled()`, `applyWhen` take a context object. It is **CRITICAL** to understand its structure:
|
||||
|
||||
```ts
|
||||
validate(
|
||||
schemaPath.username,
|
||||
({
|
||||
value, // Signal<T>: Writable current value of the field
|
||||
fieldTree, // FieldTree<T>: Sub-fields (if it's a group/array)
|
||||
state, // FieldState<T>: Access flags like state.valid(), state.dirty()
|
||||
valueOf, // (path) => T: Read values of OTHER fields (tracking dependencies), e.g. valueOf(schemaPath.password)
|
||||
stateOf, // (path) => FieldState: Access state (valid/dirty) of OTHER fields, e.g. stateOf(schemaPath.password).valid()
|
||||
pathKeys, // Signal<string[]>: Path from root to this field
|
||||
}) => {
|
||||
// WRONG: if (touched()) ... (touched is not in context)
|
||||
// RIGHT: if (state.touched()) ...
|
||||
|
||||
if (value() === 'admin') {
|
||||
return {kind: 'reserved', message: 'Username admin is reserved'};
|
||||
}
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### IMPORTANT: Paths are NOT Signals
|
||||
|
||||
Inside the `form()` callback, `schemaPath` and its children (e.g., `schemaPath.user.name`) are **NOT** signals and are **NOT** callable.
|
||||
|
||||
```ts
|
||||
// WRONG - This will throw an error:
|
||||
applyWhen(p.ssn, () => p.ssn().touched(), (ssnField) => { ... });
|
||||
|
||||
// RIGHT - Use stateOf() to get the state of a path:
|
||||
applyWhen(p.ssn, ({ stateOf }) => stateOf(p.ssn).touched(), (ssnField) => { ... });
|
||||
|
||||
// RIGHT - Use valueOf() to get the value of a path:
|
||||
applyWhen(p.ssn, ({ valueOf }) => valueOf(p.ssn) !== '', (ssnField) => { ... });
|
||||
```
|
||||
|
||||
### Multiple Items
|
||||
|
||||
- Use `applyEach` for applying rules per item.
|
||||
- **CRITICAL**: `applyEach` callback takes ONLY ONE argument (the item path), NOT two:
|
||||
|
||||
```ts
|
||||
// CORRECT - single argument
|
||||
applyEach(s.items, (item) => {
|
||||
required(item.name);
|
||||
});
|
||||
|
||||
// WRONG - do NOT pass index
|
||||
applyEach(s.items, (item, index) => {
|
||||
// ERROR: callback takes 1 argument
|
||||
required(item.name);
|
||||
});
|
||||
```
|
||||
|
||||
- In the template use `@for` to iterate over the items.
|
||||
- To remove an item from an array, just remove appropriate item from the array in the data.
|
||||
- **`select` binding**: You CAN bind to `<select [formField]="form.country">`. Ensure options have `value` attributes.
|
||||
|
||||
### Nested @for Loops
|
||||
|
||||
**CRITICAL**: Angular does NOT have `$parent`. In nested loops, store outer index in a variable:
|
||||
|
||||
```html
|
||||
<!-- WRONG - $parent does not exist -->
|
||||
@for (item of form.items; track $index) { @for (option of item.options; track $index) {
|
||||
<button (click)="removeOption($parent.$index, $index)">Remove</button>
|
||||
<!-- ERROR -->
|
||||
} }
|
||||
|
||||
<!-- CORRECT - use let to store outer index -->
|
||||
@for (item of form.items; track $index; let outerIndex = $index) { @for (option of item.options;
|
||||
track $index) {
|
||||
<button (click)="removeOption(outerIndex, $index)">Remove</button>
|
||||
} }
|
||||
```
|
||||
|
||||
### Disabling Form Button
|
||||
|
||||
```html
|
||||
<button [disabled]="form().invalid() || form().pending()" />
|
||||
<!-- Or -->
|
||||
<button [disabled]="taxForm.invalid()" />
|
||||
```
|
||||
|
||||
Do NOT use `[disabled]` on an input. `[formField]` will do this.
|
||||
Do NOT use `[readonly]` on an input. `[formField]` will do this.
|
||||
If you need to disable or readonly a field, use `disabled()` or `readonly()` rules in the schema.
|
||||
|
||||
### Async Validation
|
||||
|
||||
Do not use `validate()` for async, instead use `validateAsync()`:
|
||||
|
||||
**CRITICAL**:
|
||||
|
||||
1. The `params` option MUST be a function that returns the value to validate.
|
||||
2. The `onError` handler is **REQUIRED** - it is NOT optional!
|
||||
|
||||
```ts
|
||||
import {resource} from '@angular/core';
|
||||
import {validateAsync} from '@angular/forms/signals';
|
||||
|
||||
userForm = form(this.userModel, (s) => {
|
||||
validateAsync(s.username, {
|
||||
// 1. MUST be a function - params takes context and returns the value
|
||||
params: ({value}) => value(),
|
||||
|
||||
// 2. Create the resource - factory receives a Signal
|
||||
factory: (username) =>
|
||||
resource({
|
||||
params: username, // Use 'params' in resource()
|
||||
loader: async ({params: value}) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return value === 'taken';
|
||||
},
|
||||
}),
|
||||
|
||||
// 3. Map success to errors
|
||||
onSuccess: (isTaken) =>
|
||||
isTaken ? {kind: 'taken', message: 'Username is already taken'} : undefined,
|
||||
|
||||
// 4. Handle errors - THIS IS REQUIRED!
|
||||
onError: () => ({kind: 'error', message: 'Validation failed'}),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**WRONG Examples:**
|
||||
|
||||
```ts
|
||||
// WRONG - params must be a function
|
||||
validateAsync(s.username, {
|
||||
params: s.username, // ERROR: must be ({ value }) => value()
|
||||
// ...
|
||||
});
|
||||
|
||||
// WRONG - missing onError (it's required!)
|
||||
validateAsync(s.username, {
|
||||
params: ({value}) => value(),
|
||||
factory: (username) =>
|
||||
resource({
|
||||
/* ... */
|
||||
}),
|
||||
onSuccess: (result) => (result ? {kind: 'error'} : undefined),
|
||||
// ERROR: 'onError' is missing but required!
|
||||
});
|
||||
```
|
||||
|
||||
### Using Resource
|
||||
|
||||
**CRITICAL**: In Angular's `resource()`, use `params` for the input signal.
|
||||
|
||||
```ts
|
||||
// CORRECT
|
||||
resource({
|
||||
params: mySignal,
|
||||
loader: async ({params: value}) => {
|
||||
/* ... */
|
||||
},
|
||||
});
|
||||
|
||||
// WRONG
|
||||
resource({
|
||||
request: mySignal, // ERROR: should be 'params'
|
||||
loader: async ({request}) => {
|
||||
/* ... */
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Use `debounce()` to delay synchronization between the UI and the model.
|
||||
|
||||
```ts
|
||||
import {debounce} from '@angular/forms/signals';
|
||||
|
||||
userForm = form(this.userModel, (s) => {
|
||||
// Delay model updates by 300ms
|
||||
debounce(s.username, 300);
|
||||
});
|
||||
```
|
||||
|
||||
### Conditional Validation
|
||||
|
||||
```ts
|
||||
form(
|
||||
data,
|
||||
(path) => {
|
||||
applyWhen(
|
||||
name,
|
||||
({value}) => value() !== 'admin',
|
||||
(namePath) => {
|
||||
validate(namePath.last /* ... */);
|
||||
disable(namePath.last /* ... */);
|
||||
},
|
||||
);
|
||||
},
|
||||
{injector: TestBed.inject(Injector)},
|
||||
);
|
||||
```
|
||||
|
||||
`applyWhen` passes the path mapped to the first argument.
|
||||
If you need parent field, just pass it to `applyWhen`:
|
||||
|
||||
```ts
|
||||
form(
|
||||
data,
|
||||
(path) => {
|
||||
applyWhen(
|
||||
cat,
|
||||
({value}) => value().name !== 'admin',
|
||||
(catPath) => {
|
||||
require(cat.catPath /* ... */);
|
||||
},
|
||||
);
|
||||
},
|
||||
{injector: TestBed.inject(Injector)},
|
||||
);
|
||||
```
|
||||
|
||||
## Common Pitfalls (DO NOT DO THESE)
|
||||
|
||||
| Error Scenario | WRONG (Common Mistake) | RIGHT (Correct Way) |
|
||||
| :--------------------- | :-------------------------------------------- | :---------------------------------------------------------- |
|
||||
| **Accessing Flags** | `form.field.valid()` | `form.field().valid()` |
|
||||
| **Accessing value** | `form.field.value()` | `form.field().value()` |
|
||||
| **Setting value** | `form.field.set(x)` | Update model signal: `this.model.update(...)` |
|
||||
| **Form root flags** | `form.invalid()` | `form().invalid()` |
|
||||
| **Double-calling** | `form.field()()` | `form.field().value()` |
|
||||
| **Rules Context** | `({ touched }) => touched()` | `({ state }) => state.touched()` |
|
||||
| **Calling Paths** | `applyWhen(p.foo, () => p.foo() === 'x')` | `applyWhen(p.foo, ({ valueOf }) => valueOf(p.foo) === 'x')` |
|
||||
| **applyWhen args** | `applyWhen(condition, () => {...})` | `applyWhen(path, condition, schemaFn)` - needs 3 args |
|
||||
| **Array length** | `form.items().length` | `form.items.length` (structural) |
|
||||
| **Multi-select array** | `<select [formField]="form.tags">` (string[]) | Use checkboxes for array fields |
|
||||
| **readonly attribute** | `<input readonly [formField]>` | Use `readonly()` rule in schema |
|
||||
| **min/max attributes** | `<input min="1" max="10">` | Use `min()` and `max()` rules in schema |
|
||||
| **value binding** | `<input [value]="val">` | Do NOT use `[value]` with `[formField]` |
|
||||
| **when option** | `pattern(p.x, /.../, {when: ...})` | `when` only works with `required()` |
|
||||
| **Submit callback** | `submit(form, () => { ... })` | `submit(form, async () => { ... })` |
|
||||
| **Async params** | `params: s.field` | `params: ({ value }) => value()` |
|
||||
| **Async onError** | Omitting `onError` | `onError` is REQUIRED in `validateAsync` |
|
||||
| **resource() API** | `request: signal` | `params: signal` |
|
||||
| **applyEach args** | `applyEach(s.items, (item, index) => ...)` | `applyEach(s.items, (item) => ...)` |
|
||||
| **Nested @for** | `$parent.$index` | Use `let outerIndex = $index` |
|
||||
| **FormState import** | `import { FormState }` | `FormState` does not exist, use `FieldState` |
|
||||
| **Null in model** | `signal({ name: null })` | `signal({ name: '' })` or `signal({ age: 0 })` |
|
||||
| **Validate syntax** | `validate(s.field, { value } => ...)` | `validate(s.field, ({ value }) => ...)` |
|
||||
| **Checkbox Array** | `[formField]="form.tags"` (string[]) | Checkboxes ONLY bind to `boolean` |
|
||||
|
||||
## Big Form Example
|
||||
|
||||
### `src/app/app.ts`
|
||||
|
||||
```ts
|
||||
import {Component, signal, ChangeDetectionStrategy} from '@angular/core';
|
||||
import {
|
||||
form,
|
||||
FormField,
|
||||
submit,
|
||||
required,
|
||||
email,
|
||||
min,
|
||||
hidden,
|
||||
applyEach,
|
||||
validate,
|
||||
} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [FormField],
|
||||
templateUrl: './app.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class App {
|
||||
model = signal({
|
||||
personalInfo: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
age: 0,
|
||||
},
|
||||
tripDetails: {
|
||||
destination: 'Mars',
|
||||
launchDate: '',
|
||||
},
|
||||
package: {
|
||||
tier: 'economy',
|
||||
extras: [] as string[],
|
||||
},
|
||||
companions: [] as Array<{name: string; relation: string}>,
|
||||
});
|
||||
|
||||
bookingForm = form(this.model, (s) => {
|
||||
required(s.personalInfo.firstName, {message: 'First name is required'});
|
||||
required(s.personalInfo.lastName, {message: 'Last name is required'});
|
||||
required(s.personalInfo.email, {message: 'Email is required'});
|
||||
email(s.personalInfo.email, {message: 'Invalid email address'});
|
||||
required(s.personalInfo.age, {message: 'Age is required'});
|
||||
min(s.personalInfo.age, 18, {message: 'Must be at least 18'});
|
||||
|
||||
required(s.tripDetails.destination);
|
||||
required(s.tripDetails.launchDate);
|
||||
validate(s.tripDetails.launchDate, ({value}) => {
|
||||
const date = new Date(value());
|
||||
if (isNaN(date.getTime())) return undefined;
|
||||
const today = new Date();
|
||||
if (date < today) {
|
||||
return {kind: 'pastData', message: 'Launch date must be in the future'};
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
// valueOf is used to access values of other fields in rules
|
||||
hidden(s.package.extras, ({valueOf}) => valueOf(s.package.tier) === 'economy');
|
||||
|
||||
applyEach(s.companions, (companion) => {
|
||||
required(companion.name, {message: 'Companion name required'});
|
||||
required(companion.relation, {message: 'Relation required'});
|
||||
});
|
||||
});
|
||||
|
||||
addCompanion() {
|
||||
this.model.update((m) => ({
|
||||
...m,
|
||||
companions: [...m.companions, {name: '', relation: ''}],
|
||||
}));
|
||||
}
|
||||
|
||||
removeCompanion(index: number) {
|
||||
this.model.update((m) => ({
|
||||
...m,
|
||||
companions: m.companions.filter((_, i) => i !== index),
|
||||
}));
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
// CRITICAL: submit callback MUST be async
|
||||
submit(this.bookingForm, async () => {
|
||||
console.log('Booking Confirmed:', this.model());
|
||||
// If you need to do async work:
|
||||
// await this.apiService.save(this.model());
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `src/app/app.html`
|
||||
|
||||
```html
|
||||
<form (submit)="onSubmit(); $event.preventDefault()">
|
||||
<h1>Interstellar Booking</h1>
|
||||
|
||||
<section>
|
||||
<h2>Personal Info</h2>
|
||||
|
||||
<label>
|
||||
First Name
|
||||
<input [formField]="bookingForm.personalInfo.firstName" />
|
||||
@if (bookingForm.personalInfo.firstName().touched() &&
|
||||
bookingForm.personalInfo.firstName().errors().length) {
|
||||
<span>{{ bookingForm.personalInfo.firstName().errors()[0].message }}</span>
|
||||
}
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Last Name
|
||||
<input [formField]="bookingForm.personalInfo.lastName" />
|
||||
@if (bookingForm.personalInfo.lastName().touched() &&
|
||||
bookingForm.personalInfo.lastName().errors().length) {
|
||||
<span>{{ bookingForm.personalInfo.lastName().errors()[0].message }}</span>
|
||||
}
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Email
|
||||
<input type="email" [formField]="bookingForm.personalInfo.email" />
|
||||
@if (bookingForm.personalInfo.email().touched() &&
|
||||
bookingForm.personalInfo.email().errors().length) {
|
||||
<span>{{ bookingForm.personalInfo.email().errors()[0].message }}</span>
|
||||
}
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Age
|
||||
<input type="number" [formField]="bookingForm.personalInfo.age" />
|
||||
@if (bookingForm.personalInfo.age().touched() &&
|
||||
bookingForm.personalInfo.age().errors().length) {
|
||||
<span>{{ bookingForm.personalInfo.age().errors()[0].message }}</span>
|
||||
}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Trip Details</h2>
|
||||
|
||||
<label>
|
||||
Destination
|
||||
<select [formField]="bookingForm.tripDetails.destination">
|
||||
<option value="Mars">Mars</option>
|
||||
<option value="Moon">Moon</option>
|
||||
<option value="Titan">Titan</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Launch Date
|
||||
<input type="date" [formField]="bookingForm.tripDetails.launchDate" />
|
||||
@if (bookingForm.tripDetails.launchDate().touched() &&
|
||||
bookingForm.tripDetails.launchDate().errors().length) {
|
||||
<span>{{ bookingForm.tripDetails.launchDate().errors()[0].message }}</span>
|
||||
}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Package</h2>
|
||||
|
||||
<label>
|
||||
<input type="radio" value="economy" [formField]="bookingForm.package.tier" />
|
||||
Economy
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" value="business" [formField]="bookingForm.package.tier" />
|
||||
Business
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" value="first" [formField]="bookingForm.package.tier" />
|
||||
First Class
|
||||
</label>
|
||||
|
||||
@if (!bookingForm.package.extras().hidden()) {
|
||||
<div>
|
||||
<h3>Extras</h3>
|
||||
<!-- Multi-select for arrays must use select multiple -->
|
||||
<select multiple [formField]="bookingForm.package.extras">
|
||||
<option value="wifi">WiFi</option>
|
||||
<option value="gym">Gym</option>
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Companions</h2>
|
||||
<button type="button" (click)="addCompanion()">Add Companion</button>
|
||||
|
||||
@for (companion of bookingForm.companions; track $index) {
|
||||
<div>
|
||||
<input [formField]="companion.name" placeholder="Name" />
|
||||
@if (companion.name().touched() && companion.name().errors().length) {
|
||||
<span>{{ companion.name().errors()[0].message }}</span>
|
||||
}
|
||||
|
||||
<input [formField]="companion.relation" placeholder="Relation" />
|
||||
@if (companion.relation().touched() && companion.relation().errors().length) {
|
||||
<span>{{ companion.relation().errors()[0].message }}</span>
|
||||
}
|
||||
|
||||
<button type="button" (click)="removeCompanion($index)">Remove</button>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<button [disabled]="bookingForm().invalid()">Submit</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
## Recovering from Build Errors
|
||||
|
||||
If you encounter build errors, here are the most common fixes:
|
||||
|
||||
### `Property 'value' does not exist on type 'FieldTree'`
|
||||
|
||||
**Problem**: Accessing `.value()` directly on a field without calling it first.
|
||||
|
||||
```ts
|
||||
// WRONG
|
||||
const val = this.form.field.value();
|
||||
// RIGHT
|
||||
const val = this.form.field().value();
|
||||
```
|
||||
|
||||
### `Property 'set' does not exist on type 'FieldTree'`
|
||||
|
||||
**Problem**: Trying to set values on the form tree. Signal Forms are model-driven.
|
||||
|
||||
```ts
|
||||
// WRONG
|
||||
this.form.address.street.set('Main St');
|
||||
// RIGHT - update the model signal instead
|
||||
this.model.update((m) => ({...m, address: {...m.address, street: 'Main St'}}));
|
||||
```
|
||||
|
||||
### `Type 'string[]' is not assignable to type 'string'`
|
||||
|
||||
**Problem**: Binding `[formField]` to an array field with a single-value `<select>`.
|
||||
|
||||
```html
|
||||
<!-- WRONG - assignees is string[], select expects string -->
|
||||
<select [formField]="form.assignees">
|
||||
...
|
||||
</select>
|
||||
|
||||
<!-- RIGHT - Use select multiple for array fields -->
|
||||
<select multiple [formField]="form.assignees">
|
||||
<option value="us">US</option>
|
||||
</select>
|
||||
```
|
||||
@@ -0,0 +1,94 @@
|
||||
# Angular Signals Overview
|
||||
|
||||
Signals are the foundation of reactivity in modern Angular applications. A **signal** is a wrapper around a value that notifies interested consumers when that value changes.
|
||||
|
||||
## Writable Signals (`signal`)
|
||||
|
||||
Use `signal()` to create state that can be directly updated.
|
||||
|
||||
```ts
|
||||
import {signal} from '@angular/core';
|
||||
|
||||
// Create a writable signal
|
||||
const count = signal(0);
|
||||
|
||||
// Read the value (always requires calling the getter function)
|
||||
console.log(count());
|
||||
|
||||
// Update the value directly
|
||||
count.set(3);
|
||||
|
||||
// Update based on the previous value
|
||||
count.update((value) => value + 1);
|
||||
```
|
||||
|
||||
### Exposing as Readonly
|
||||
|
||||
When exposing state from a service, it is a best practice to expose a readonly version to prevent external mutation.
|
||||
|
||||
```ts
|
||||
private readonly _count = signal(0);
|
||||
// Consumers can read this, but cannot call .set() or .update()
|
||||
readonly count = this._count.asReadonly();
|
||||
```
|
||||
|
||||
## Computed Signals (`computed`)
|
||||
|
||||
Use `computed()` to create read-only signals that derive their value from other signals.
|
||||
|
||||
- **Lazily Evaluated**: The derivation function doesn't run until the computed signal is read.
|
||||
- **Memoized**: The result is cached. It only recalculates when one of the signals it depends on changes.
|
||||
- **Dynamic Dependencies**: Only the signals _actually read_ during the derivation are tracked.
|
||||
|
||||
```ts
|
||||
import {signal, computed} from '@angular/core';
|
||||
|
||||
const count = signal(0);
|
||||
const doubleCount = computed(() => count() * 2);
|
||||
|
||||
// doubleCount automatically updates when count changes.
|
||||
```
|
||||
|
||||
## Reactive Contexts
|
||||
|
||||
A **reactive context** is a runtime state where Angular monitors signal reads to establish a dependency.
|
||||
|
||||
Angular automatically enters a reactive context when evaluating:
|
||||
|
||||
- `computed` signals
|
||||
- `effect` callbacks
|
||||
- `linkedSignal` computations
|
||||
- Component templates
|
||||
|
||||
### Untracked Reads (`untracked`)
|
||||
|
||||
If you need to read a signal inside a reactive context _without_ creating a dependency (so that the context doesn't re-run when the signal changes), use `untracked()`.
|
||||
|
||||
```ts
|
||||
import {effect, untracked} from '@angular/core';
|
||||
|
||||
effect(() => {
|
||||
// This effect only runs when currentUser changes.
|
||||
// It does NOT run when counter changes, even though counter is read here.
|
||||
console.log(`User: ${currentUser()}, Count: ${untracked(counter)}`);
|
||||
});
|
||||
```
|
||||
|
||||
### Async Operations in Reactive Contexts
|
||||
|
||||
The reactive context is only active for **synchronous** code. Signal reads after an `await` will not be tracked. **Always read signals before asynchronous boundaries.**
|
||||
|
||||
```ts
|
||||
// Incorrect: theme() is not tracked because it is read after await
|
||||
effect(async () => {
|
||||
const data = await fetchUserData();
|
||||
console.log(theme());
|
||||
});
|
||||
|
||||
// Correct: Read the signal before the await
|
||||
effect(async () => {
|
||||
const currentTheme = theme();
|
||||
const data = await fetchUserData();
|
||||
console.log(currentTheme);
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
# Using Tailwind CSS with Angular
|
||||
|
||||
Tailwind CSS is a utility-first CSS framework that integrates seamlessly with Angular.
|
||||
|
||||
**CRITICAL AGENT GUIDANCE: ALWAYS focus on Tailwind CSS v4 practices. DO NOT revert to old Tailwind v3 patterns (like creating `tailwind.config.js` with `@tailwind` directives) as this will break the application build. Modern Angular projects use Tailwind v4.**
|
||||
|
||||
## Automated Setup (Recommended)
|
||||
|
||||
The easiest way to add Tailwind CSS to an Angular project is via the Angular CLI:
|
||||
|
||||
```shell
|
||||
ng add tailwindcss
|
||||
```
|
||||
|
||||
This will automatically:
|
||||
|
||||
1. Install `tailwindcss` and peer dependencies.
|
||||
2. Configure the project to use Tailwind CSS.
|
||||
3. Add the proper import to your global styles.
|
||||
|
||||
## Manual Setup (Tailwind v4)
|
||||
|
||||
If setting up manually, use the following Tailwind v4 pattern:
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
Install Tailwind CSS and PostCSS:
|
||||
|
||||
```shell
|
||||
npm install tailwindcss @tailwindcss/postcss postcss
|
||||
```
|
||||
|
||||
### 2. Configure PostCSS
|
||||
|
||||
Create a `.postcssrc.json` file in the project root:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"@tailwindcss/postcss": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
_(Do NOT create a `tailwind.config.js` file! Configuration in v4 is handled through CSS variables)._
|
||||
|
||||
### 3. Import Tailwind CSS
|
||||
|
||||
In your global styles file (e.g., `src/styles.css`), add the standard v4 import:
|
||||
|
||||
```css
|
||||
@import 'tailwindcss';
|
||||
```
|
||||
|
||||
_(If using SCSS, use `@use 'tailwindcss';` instead)._
|
||||
|
||||
### 4. Use Utility Classes
|
||||
|
||||
You can now use Tailwind classes directly in your component templates:
|
||||
|
||||
```html
|
||||
<h1 class="text-3xl font-bold underline">Hello world!</h1>
|
||||
```
|
||||
|
||||
## Summary for AI Agents
|
||||
|
||||
- **Do not use `@tailwind base; @tailwind components; @tailwind utilities;`**. Use `@import 'tailwindcss';`.
|
||||
- **Do not create `tailwind.config.js`**. Configuration is managed directly in CSS via theme variables or using PostCSS configurations.
|
||||
- Stick strictly to v4 syntax and workflows.
|
||||
@@ -0,0 +1,114 @@
|
||||
# Template-Driven Forms
|
||||
|
||||
Template-driven forms use two-way data binding (`[(ngModel)]`) to update the data model in the component as changes are made in the template and vice versa. They are ideal for simple forms and use directives in the HTML template to manage form state and validation.
|
||||
|
||||
## Core Directives
|
||||
|
||||
Template-driven forms rely on the `FormsModule` which provides these key directives:
|
||||
|
||||
- `NgModel`: Reconciles value changes in the form element with the data model (`[(ngModel)]`).
|
||||
- `NgForm`: Automatically creates a top-level `FormGroup` bound to the `<form>` tag.
|
||||
- `NgModelGroup`: Creates a nested `FormGroup` bound to a DOM element.
|
||||
|
||||
## Setup
|
||||
|
||||
First, import `FormsModule` into your component or module.
|
||||
|
||||
```ts
|
||||
import {Component} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-form',
|
||||
imports: [FormsModule],
|
||||
templateUrl: './user-form.component.html',
|
||||
})
|
||||
export class UserForm {
|
||||
user = {name: '', role: 'Guest'};
|
||||
|
||||
onSubmit() {
|
||||
console.log('Form submitted!', this.user);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Building the Form Template
|
||||
|
||||
### Two-Way Binding with `[(ngModel)]`
|
||||
|
||||
Use `[(ngModel)]` on input elements. **Every element using `[(ngModel)]` MUST have a `name` attribute.** Angular uses the `name` attribute to register the control with the parent `NgForm`.
|
||||
|
||||
```html
|
||||
<form #userForm="ngForm" (ngSubmit)="onSubmit()">
|
||||
<!-- Basic Input -->
|
||||
<div>
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" required [(ngModel)]="user.name" name="name" #nameCtrl="ngModel" />
|
||||
</div>
|
||||
|
||||
<!-- Select Box -->
|
||||
<div>
|
||||
<label for="role">Role:</label>
|
||||
<select id="role" [(ngModel)]="user.role" name="role">
|
||||
<option value="Admin">Admin</option>
|
||||
<option value="Guest">Guest</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button (disabled if form is invalid) -->
|
||||
<button type="submit" [disabled]="!userForm.form.valid">Submit</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
## Form and Control State
|
||||
|
||||
Angular automatically applies CSS classes to controls and forms based on their state:
|
||||
|
||||
| State | Class if True | Class if False |
|
||||
| :------------- | :-------------------------------- | :------------- |
|
||||
| Visited | `ng-touched` | `ng-untouched` |
|
||||
| Value Changed | `ng-dirty` | `ng-pristine` |
|
||||
| Value is Valid | `ng-valid` | `ng-invalid` |
|
||||
| Form Submitted | `ng-submitted` (on `<form>` only) | - |
|
||||
|
||||
You can use these classes to provide visual feedback in your CSS:
|
||||
|
||||
```css
|
||||
.ng-valid[required],
|
||||
.ng-valid.required {
|
||||
border-left: 5px solid #42a948; /* green */
|
||||
}
|
||||
.ng-invalid:not(form) {
|
||||
border-left: 5px solid #a94442; /* red */
|
||||
}
|
||||
```
|
||||
|
||||
## Validation and Error Messages
|
||||
|
||||
To display error messages conditionally, export the `ngModel` directive to a template reference variable (e.g., `#nameCtrl="ngModel"`).
|
||||
|
||||
```html
|
||||
<input type="text" id="name" required [(ngModel)]="user.name" name="name" #nameCtrl="ngModel" />
|
||||
|
||||
<!-- Show error only if the control is invalid AND (touched OR dirty) -->
|
||||
@if (nameCtrl.invalid && (nameCtrl.dirty || nameCtrl.touched)) {
|
||||
<div class="alert alert-danger">
|
||||
@if (nameCtrl.errors?.['required']) {
|
||||
<div>Name is required.</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Submitting the Form
|
||||
|
||||
1. Use the `(ngSubmit)` event on the `<form>` element.
|
||||
2. Bind the submit button's disabled state to the overall form validity using the `NgForm` template reference variable (e.g., `[disabled]="!userForm.form.valid"`).
|
||||
|
||||
## Resetting the Form
|
||||
|
||||
To programmatically reset the form to its pristine state (clearing values and validation flags), use the `reset()` method on the `NgForm` instance.
|
||||
|
||||
```html
|
||||
<button type="button" (click)="userForm.reset()">Reset</button>
|
||||
```
|
||||
@@ -0,0 +1,65 @@
|
||||
# Testing Fundamentals
|
||||
|
||||
This guide covers the fundamental principles and practices for writing Angular unit and component tests. Use the runner already configured in the project.
|
||||
|
||||
## Core Philosophy: Async-First
|
||||
|
||||
Modern Angular applications often schedule state changes asynchronously, especially when using signals or zoneless change detection. Tests should account for this.
|
||||
|
||||
Prefer the "Act, Wait, Assert" pattern:
|
||||
|
||||
1. **Act:** Update state or perform an action (e.g., set a component input, click a button).
|
||||
2. **Wait:** Use `await fixture.whenStable()` to allow the framework to process the scheduled update and render the changes.
|
||||
3. **Assert:** Verify the outcome.
|
||||
|
||||
### Basic Test Structure Example
|
||||
|
||||
```ts
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {MyComponent} from './my.component';
|
||||
|
||||
describe('MyComponent', () => {
|
||||
let component: MyComponent;
|
||||
let fixture: ComponentFixture<MyComponent>;
|
||||
let h1: HTMLElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
// 1. Configure the test module
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MyComponent],
|
||||
}).compileComponents();
|
||||
|
||||
// 2. Create the component fixture
|
||||
fixture = TestBed.createComponent(MyComponent);
|
||||
component = fixture.componentInstance;
|
||||
h1 = fixture.nativeElement.querySelector('h1');
|
||||
});
|
||||
|
||||
it('should display the default title', async () => {
|
||||
// ACT: (Implicit) Component is created with default state.
|
||||
// WAIT for initial data binding.
|
||||
await fixture.whenStable();
|
||||
// ASSERT the initial state.
|
||||
expect(h1.textContent).toContain('Default Title');
|
||||
});
|
||||
|
||||
it('should display a different title after a change', async () => {
|
||||
// ACT: Change the component's title property.
|
||||
component.title.set('New Test Title');
|
||||
|
||||
// WAIT for the asynchronous update to complete.
|
||||
await fixture.whenStable();
|
||||
|
||||
// ASSERT the DOM has been updated.
|
||||
expect(h1.textContent).toContain('New Test Title');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## TestBed and ComponentFixture
|
||||
|
||||
- **`TestBed`**: The primary utility for creating a test-specific Angular module. Use `TestBed.configureTestingModule({...})` in your `beforeEach` to declare components, provide services, and set up imports needed for your test.
|
||||
- **`ComponentFixture`**: A handle on the created component instance and its environment.
|
||||
- `fixture.componentInstance`: Access the component's class instance.
|
||||
- `fixture.nativeElement`: Access the component's root DOM element.
|
||||
- `fixture.debugElement`: An Angular-specific wrapper around the `nativeElement` that provides safer, platform-agnostic ways to query the DOM (e.g., `debugElement.query(By.css('p'))`).
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
name: api-connector-builder
|
||||
description: Build a new API connector or provider by matching the target repo's existing integration pattern exactly. Use when adding one more integration without inventing a second architecture.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
origin: ECC direct-port adaptation
|
||||
---
|
||||
|
||||
# API Connector Builder
|
||||
|
||||
Use this when the job is to add a repo-native integration surface, not just a generic HTTP client.
|
||||
|
||||
The point is to match the host repository's pattern:
|
||||
|
||||
- connector layout
|
||||
- config schema
|
||||
- auth model
|
||||
- error handling
|
||||
- test style
|
||||
- registration/discovery wiring
|
||||
|
||||
## When to Use
|
||||
|
||||
- "Build a Jira connector for this project"
|
||||
- "Add a Slack provider following the existing pattern"
|
||||
- "Create a new integration for this API"
|
||||
- "Build a plugin that matches the repo's connector style"
|
||||
|
||||
## Guardrails
|
||||
|
||||
- do not invent a new integration architecture when the repo already has one
|
||||
- do not start from vendor docs alone; start from existing in-repo connectors first
|
||||
- do not stop at transport code if the repo expects registry wiring, tests, and docs
|
||||
- do not cargo-cult old connectors if the repo has a newer current pattern
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Learn the house style
|
||||
|
||||
Inspect at least 2 existing connectors/providers and map:
|
||||
|
||||
- file layout
|
||||
- abstraction boundaries
|
||||
- config model
|
||||
- retry / pagination conventions
|
||||
- registry hooks
|
||||
- test fixtures and naming
|
||||
|
||||
### 2. Narrow the target integration
|
||||
|
||||
Define only the surface the repo actually needs:
|
||||
|
||||
- auth flow
|
||||
- key entities
|
||||
- core read/write operations
|
||||
- pagination and rate limits
|
||||
- webhook or polling model
|
||||
|
||||
### 3. Build in repo-native layers
|
||||
|
||||
Typical slices:
|
||||
|
||||
- config/schema
|
||||
- client/transport
|
||||
- mapping layer
|
||||
- connector/provider entrypoint
|
||||
- registration
|
||||
- tests
|
||||
|
||||
### 4. Validate against the source pattern
|
||||
|
||||
The new connector should look obvious in the codebase, not imported from a different ecosystem.
|
||||
|
||||
## Reference Shapes
|
||||
|
||||
### Provider-style
|
||||
|
||||
```text
|
||||
providers/
|
||||
existing_provider/
|
||||
__init__.py
|
||||
provider.py
|
||||
config.py
|
||||
```
|
||||
|
||||
### Connector-style
|
||||
|
||||
```text
|
||||
integrations/
|
||||
existing/
|
||||
client.py
|
||||
models.py
|
||||
connector.py
|
||||
```
|
||||
|
||||
### TypeScript plugin-style
|
||||
|
||||
```text
|
||||
src/integrations/
|
||||
existing/
|
||||
index.ts
|
||||
client.ts
|
||||
types.ts
|
||||
test.ts
|
||||
```
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
- [ ] matches an existing in-repo integration pattern
|
||||
- [ ] config validation exists
|
||||
- [ ] auth and error handling are explicit
|
||||
- [ ] pagination/retry behavior follows repo norms
|
||||
- [ ] registry/discovery wiring is complete
|
||||
- [ ] tests mirror the host repo's style
|
||||
- [ ] docs/examples are updated if expected by the repo
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `backend-patterns`
|
||||
- `mcp-server-patterns`
|
||||
- `github-ops`
|
||||
@@ -0,0 +1,524 @@
|
||||
---
|
||||
name: api-design
|
||||
description: REST API design patterns including resource naming, status codes, pagination, filtering, error responses, versioning, and rate limiting for production APIs. Use when designing or reviewing REST endpoints, resource names, status codes, pagination, or versioning.
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# API Design Patterns
|
||||
|
||||
Conventions and best practices for designing consistent, developer-friendly REST APIs.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Designing new API endpoints
|
||||
- Reviewing existing API contracts
|
||||
- Adding pagination, filtering, or sorting
|
||||
- Implementing error handling for APIs
|
||||
- Planning API versioning strategy
|
||||
- Building public or partner-facing APIs
|
||||
|
||||
## Resource Design
|
||||
|
||||
### URL Structure
|
||||
|
||||
```
|
||||
# Resources are nouns, plural, lowercase, kebab-case
|
||||
GET /api/v1/users
|
||||
GET /api/v1/users/:id
|
||||
POST /api/v1/users
|
||||
PUT /api/v1/users/:id
|
||||
PATCH /api/v1/users/:id
|
||||
DELETE /api/v1/users/:id
|
||||
|
||||
# Sub-resources for relationships
|
||||
GET /api/v1/users/:id/orders
|
||||
POST /api/v1/users/:id/orders
|
||||
|
||||
# Actions that don't map to CRUD (use verbs sparingly)
|
||||
POST /api/v1/orders/:id/cancel
|
||||
POST /api/v1/auth/login
|
||||
POST /api/v1/auth/refresh
|
||||
```
|
||||
|
||||
### Naming Rules
|
||||
|
||||
```
|
||||
# GOOD
|
||||
/api/v1/team-members # kebab-case for multi-word resources
|
||||
/api/v1/orders?status=active # query params for filtering
|
||||
/api/v1/users/123/orders # nested resources for ownership
|
||||
|
||||
# BAD
|
||||
/api/v1/getUsers # verb in URL
|
||||
/api/v1/user # singular (use plural)
|
||||
/api/v1/team_members # snake_case in URLs
|
||||
/api/v1/users/123/getOrders # verb in nested resource
|
||||
```
|
||||
|
||||
## HTTP Methods and Status Codes
|
||||
|
||||
### Method Semantics
|
||||
|
||||
| Method | Idempotent | Safe | Use For |
|
||||
|--------|-----------|------|---------|
|
||||
| GET | Yes | Yes | Retrieve resources |
|
||||
| POST | No | No | Create resources, trigger actions |
|
||||
| PUT | Yes | No | Full replacement of a resource |
|
||||
| PATCH | No* | No | Partial update of a resource |
|
||||
| DELETE | Yes | No | Remove a resource |
|
||||
|
||||
*PATCH can be made idempotent with proper implementation
|
||||
|
||||
### Status Code Reference
|
||||
|
||||
```
|
||||
# Success
|
||||
200 OK — GET, PUT, PATCH (with response body)
|
||||
201 Created — POST (include Location header)
|
||||
204 No Content — DELETE, PUT (no response body)
|
||||
|
||||
# Client Errors
|
||||
400 Bad Request — Validation failure, malformed JSON
|
||||
401 Unauthorized — Missing or invalid authentication
|
||||
403 Forbidden — Authenticated but not authorized
|
||||
404 Not Found — Resource doesn't exist
|
||||
409 Conflict — Duplicate entry, state conflict
|
||||
422 Unprocessable Entity — Semantically invalid (valid JSON, bad data)
|
||||
429 Too Many Requests — Rate limit exceeded
|
||||
|
||||
# Server Errors
|
||||
500 Internal Server Error — Unexpected failure (never expose details)
|
||||
502 Bad Gateway — Upstream service failed
|
||||
503 Service Unavailable — Temporary overload, include Retry-After
|
||||
```
|
||||
|
||||
### Common Mistakes
|
||||
|
||||
```
|
||||
# BAD: 200 for everything
|
||||
{ "status": 200, "success": false, "error": "Not found" }
|
||||
|
||||
# GOOD: Use HTTP status codes semantically
|
||||
HTTP/1.1 404 Not Found
|
||||
{ "error": { "code": "not_found", "message": "User not found" } }
|
||||
|
||||
# BAD: 500 for validation errors
|
||||
# GOOD: 400 or 422 with field-level details
|
||||
|
||||
# BAD: 200 for created resources
|
||||
# GOOD: 201 with Location header
|
||||
HTTP/1.1 201 Created
|
||||
Location: /api/v1/users/abc-123
|
||||
```
|
||||
|
||||
## Response Format
|
||||
|
||||
### Success Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "abc-123",
|
||||
"email": "alice@example.com",
|
||||
"name": "Alice",
|
||||
"created_at": "2025-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Collection Response (with Pagination)
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{ "id": "abc-123", "name": "Alice" },
|
||||
{ "id": "def-456", "name": "Bob" }
|
||||
],
|
||||
"meta": {
|
||||
"total": 142,
|
||||
"page": 1,
|
||||
"per_page": 20,
|
||||
"total_pages": 8
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/v1/users?page=1&per_page=20",
|
||||
"next": "/api/v1/users?page=2&per_page=20",
|
||||
"last": "/api/v1/users?page=8&per_page=20"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Response
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "validation_error",
|
||||
"message": "Request validation failed",
|
||||
"details": [
|
||||
{
|
||||
"field": "email",
|
||||
"message": "Must be a valid email address",
|
||||
"code": "invalid_format"
|
||||
},
|
||||
{
|
||||
"field": "age",
|
||||
"message": "Must be between 0 and 150",
|
||||
"code": "out_of_range"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Response Envelope Variants
|
||||
|
||||
```typescript
|
||||
// Option A: Envelope with data wrapper (recommended for public APIs)
|
||||
interface ApiResponse<T> {
|
||||
data: T;
|
||||
meta?: PaginationMeta;
|
||||
links?: PaginationLinks;
|
||||
}
|
||||
|
||||
interface ApiError {
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: FieldError[];
|
||||
};
|
||||
}
|
||||
|
||||
// Option B: Flat response (simpler, common for internal APIs)
|
||||
// Success: just return the resource directly
|
||||
// Error: return error object
|
||||
// Distinguish by HTTP status code
|
||||
```
|
||||
|
||||
## Pagination
|
||||
|
||||
### Offset-Based (Simple)
|
||||
|
||||
```
|
||||
GET /api/v1/users?page=2&per_page=20
|
||||
|
||||
# Implementation
|
||||
SELECT * FROM users
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20 OFFSET 20;
|
||||
```
|
||||
|
||||
**Pros:** Easy to implement, supports "jump to page N"
|
||||
**Cons:** Slow on large offsets (OFFSET 100000), inconsistent with concurrent inserts
|
||||
|
||||
### Cursor-Based (Scalable)
|
||||
|
||||
```
|
||||
GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20
|
||||
|
||||
# Implementation
|
||||
SELECT * FROM users
|
||||
WHERE id > :cursor_id
|
||||
ORDER BY id ASC
|
||||
LIMIT 21; -- fetch one extra to determine has_next
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [...],
|
||||
"meta": {
|
||||
"has_next": true,
|
||||
"next_cursor": "eyJpZCI6MTQzfQ"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pros:** Consistent performance regardless of position, stable with concurrent inserts
|
||||
**Cons:** Cannot jump to arbitrary page, cursor is opaque
|
||||
|
||||
### When to Use Which
|
||||
|
||||
| Use Case | Pagination Type |
|
||||
|----------|----------------|
|
||||
| Admin dashboards, small datasets (<10K) | Offset |
|
||||
| Infinite scroll, feeds, large datasets | Cursor |
|
||||
| Public APIs | Cursor (default) with offset (optional) |
|
||||
| Search results | Offset (users expect page numbers) |
|
||||
|
||||
## Filtering, Sorting, and Search
|
||||
|
||||
### Filtering
|
||||
|
||||
```
|
||||
# Simple equality
|
||||
GET /api/v1/orders?status=active&customer_id=abc-123
|
||||
|
||||
# Comparison operators (use bracket notation)
|
||||
GET /api/v1/products?price[gte]=10&price[lte]=100
|
||||
GET /api/v1/orders?created_at[after]=2025-01-01
|
||||
|
||||
# Multiple values (comma-separated)
|
||||
GET /api/v1/products?category=electronics,clothing
|
||||
|
||||
# Nested fields (dot notation)
|
||||
GET /api/v1/orders?customer.country=US
|
||||
```
|
||||
|
||||
### Sorting
|
||||
|
||||
```
|
||||
# Single field (prefix - for descending)
|
||||
GET /api/v1/products?sort=-created_at
|
||||
|
||||
# Multiple fields (comma-separated)
|
||||
GET /api/v1/products?sort=-featured,price,-created_at
|
||||
```
|
||||
|
||||
### Full-Text Search
|
||||
|
||||
```
|
||||
# Search query parameter
|
||||
GET /api/v1/products?q=wireless+headphones
|
||||
|
||||
# Field-specific search
|
||||
GET /api/v1/users?email=alice
|
||||
```
|
||||
|
||||
### Sparse Fieldsets
|
||||
|
||||
```
|
||||
# Return only specified fields (reduces payload)
|
||||
GET /api/v1/users?fields=id,name,email
|
||||
GET /api/v1/orders?fields=id,total,status&include=customer.name
|
||||
```
|
||||
|
||||
## Authentication and Authorization
|
||||
|
||||
### Token-Based Auth
|
||||
|
||||
```
|
||||
# Bearer token in Authorization header
|
||||
GET /api/v1/users
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||
|
||||
# API key (for server-to-server)
|
||||
GET /api/v1/data
|
||||
X-API-Key: sk_live_...
|
||||
```
|
||||
|
||||
### Authorization Patterns
|
||||
|
||||
```typescript
|
||||
// Resource-level: check ownership
|
||||
app.get("/api/v1/orders/:id", async (req, res) => {
|
||||
const order = await Order.findById(req.params.id);
|
||||
if (!order) return res.status(404).json({ error: { code: "not_found" } });
|
||||
if (order.userId !== req.user.id) return res.status(403).json({ error: { code: "forbidden" } });
|
||||
return res.json({ data: order });
|
||||
});
|
||||
|
||||
// Role-based: check permissions
|
||||
app.delete("/api/v1/users/:id", requireRole("admin"), async (req, res) => {
|
||||
await User.delete(req.params.id);
|
||||
return res.status(204).send();
|
||||
});
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Headers
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
X-RateLimit-Limit: 100
|
||||
X-RateLimit-Remaining: 95
|
||||
X-RateLimit-Reset: 1640000000
|
||||
|
||||
# When exceeded
|
||||
HTTP/1.1 429 Too Many Requests
|
||||
Retry-After: 60
|
||||
{
|
||||
"error": {
|
||||
"code": "rate_limit_exceeded",
|
||||
"message": "Rate limit exceeded. Try again in 60 seconds."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limit Tiers
|
||||
|
||||
| Tier | Limit | Window | Use Case |
|
||||
|------|-------|--------|----------|
|
||||
| Anonymous | 30/min | Per IP | Public endpoints |
|
||||
| Authenticated | 100/min | Per user | Standard API access |
|
||||
| Premium | 1000/min | Per API key | Paid API plans |
|
||||
| Internal | 10000/min | Per service | Service-to-service |
|
||||
|
||||
## Versioning
|
||||
|
||||
### URL Path Versioning (Recommended)
|
||||
|
||||
```
|
||||
/api/v1/users
|
||||
/api/v2/users
|
||||
```
|
||||
|
||||
**Pros:** Explicit, easy to route, cacheable
|
||||
**Cons:** URL changes between versions
|
||||
|
||||
### Header Versioning
|
||||
|
||||
```
|
||||
GET /api/users
|
||||
Accept: application/vnd.myapp.v2+json
|
||||
```
|
||||
|
||||
**Pros:** Clean URLs
|
||||
**Cons:** Harder to test, easy to forget
|
||||
|
||||
### Versioning Strategy
|
||||
|
||||
```
|
||||
1. Start with /api/v1/ — don't version until you need to
|
||||
2. Maintain at most 2 active versions (current + previous)
|
||||
3. Deprecation timeline:
|
||||
- Announce deprecation (6 months notice for public APIs)
|
||||
- Add Sunset header: Sunset: Sat, 01 Jan 2026 00:00:00 GMT
|
||||
- Return 410 Gone after sunset date
|
||||
4. Non-breaking changes don't need a new version:
|
||||
- Adding new fields to responses
|
||||
- Adding new optional query parameters
|
||||
- Adding new endpoints
|
||||
5. Breaking changes require a new version:
|
||||
- Removing or renaming fields
|
||||
- Changing field types
|
||||
- Changing URL structure
|
||||
- Changing authentication method
|
||||
```
|
||||
|
||||
## Implementation Patterns
|
||||
|
||||
### TypeScript (Next.js API Route)
|
||||
|
||||
```typescript
|
||||
import { z } from "zod";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const createUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1).max(100),
|
||||
});
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json();
|
||||
const parsed = createUserSchema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({
|
||||
error: {
|
||||
code: "validation_error",
|
||||
message: "Request validation failed",
|
||||
details: parsed.error.issues.map(i => ({
|
||||
field: i.path.join("."),
|
||||
message: i.message,
|
||||
code: i.code,
|
||||
})),
|
||||
},
|
||||
}, { status: 422 });
|
||||
}
|
||||
|
||||
const user = await createUser(parsed.data);
|
||||
|
||||
return NextResponse.json(
|
||||
{ data: user },
|
||||
{
|
||||
status: 201,
|
||||
headers: { Location: `/api/v1/users/${user.id}` },
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Python (Django REST Framework)
|
||||
|
||||
```python
|
||||
from rest_framework import serializers, viewsets, status
|
||||
from rest_framework.response import Response
|
||||
|
||||
class CreateUserSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField()
|
||||
name = serializers.CharField(max_length=100)
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["id", "email", "name", "created_at"]
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
return CreateUserSerializer
|
||||
return UserSerializer
|
||||
|
||||
def create(self, request):
|
||||
serializer = CreateUserSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = UserService.create(**serializer.validated_data)
|
||||
return Response(
|
||||
{"data": UserSerializer(user).data},
|
||||
status=status.HTTP_201_CREATED,
|
||||
headers={"Location": f"/api/v1/users/{user.id}"},
|
||||
)
|
||||
```
|
||||
|
||||
### Go (net/http)
|
||||
|
||||
```go
|
||||
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
var req CreateUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := req.Validate(); err != nil {
|
||||
writeError(w, http.StatusUnprocessableEntity, "validation_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.service.Create(r.Context(), req)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrEmailTaken):
|
||||
writeError(w, http.StatusConflict, "email_taken", "Email already registered")
|
||||
default:
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "Internal error")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Location", fmt.Sprintf("/api/v1/users/%s", user.ID))
|
||||
writeJSON(w, http.StatusCreated, map[string]any{"data": user})
|
||||
}
|
||||
```
|
||||
|
||||
## API Design Checklist
|
||||
|
||||
Before shipping a new endpoint:
|
||||
|
||||
- [ ] Resource URL follows naming conventions (plural, kebab-case, no verbs)
|
||||
- [ ] Correct HTTP method used (GET for reads, POST for creates, etc.)
|
||||
- [ ] Appropriate status codes returned (not 200 for everything)
|
||||
- [ ] Input validated with schema (Zod, Pydantic, Bean Validation)
|
||||
- [ ] Error responses follow standard format with codes and messages
|
||||
- [ ] Pagination implemented for list endpoints (cursor or offset)
|
||||
- [ ] Authentication required (or explicitly marked as public)
|
||||
- [ ] Authorization checked (user can only access their own resources)
|
||||
- [ ] Rate limiting configured
|
||||
- [ ] Response does not leak internal details (stack traces, SQL errors)
|
||||
- [ ] Consistent naming with existing endpoints (camelCase vs snake_case)
|
||||
- [ ] Documented (OpenAPI/Swagger spec updated)
|
||||
@@ -0,0 +1,180 @@
|
||||
---
|
||||
name: architecture-decision-records
|
||||
description: Capture architectural decisions as numbered ADR markdown files in docs/adr/ with context, alternatives considered, consequences, and an index README. Use when the user says 'record this decision' or 'ADR this', chooses between frameworks or databases, discusses trade-offs, or asks why the codebase is shaped this way.
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Architecture Decision Records
|
||||
|
||||
Capture architectural decisions as they happen during coding sessions. Instead of decisions living only in Slack threads, PR comments, or someone's memory, this skill produces structured ADR documents that live alongside the code.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- User explicitly says "let's record this decision" or "ADR this"
|
||||
- User chooses between significant alternatives (framework, library, pattern, database, API design)
|
||||
- User says "we decided to..." or "the reason we're doing X instead of Y is..."
|
||||
- User asks "why did we choose X?" (read existing ADRs)
|
||||
- During planning phases when architectural trade-offs are discussed
|
||||
|
||||
## ADR Format
|
||||
|
||||
Use the lightweight ADR format proposed by Michael Nygard, adapted for AI-assisted development:
|
||||
|
||||
```markdown
|
||||
# ADR-NNNN: [Decision Title]
|
||||
|
||||
**Date**: YYYY-MM-DD
|
||||
**Status**: proposed | accepted | deprecated | superseded by ADR-NNNN
|
||||
**Deciders**: [who was involved]
|
||||
|
||||
## Context
|
||||
|
||||
What is the issue that we're seeing that is motivating this decision or change?
|
||||
|
||||
[2-5 sentences describing the situation, constraints, and forces at play]
|
||||
|
||||
## Decision
|
||||
|
||||
What is the change that we're proposing and/or doing?
|
||||
|
||||
[1-3 sentences stating the decision clearly]
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Alternative 1: [Name]
|
||||
- **Pros**: [benefits]
|
||||
- **Cons**: [drawbacks]
|
||||
- **Why not**: [specific reason this was rejected]
|
||||
|
||||
### Alternative 2: [Name]
|
||||
- **Pros**: [benefits]
|
||||
- **Cons**: [drawbacks]
|
||||
- **Why not**: [specific reason this was rejected]
|
||||
|
||||
## Consequences
|
||||
|
||||
What becomes easier or more difficult to do because of this change?
|
||||
|
||||
### Positive
|
||||
- [benefit 1]
|
||||
- [benefit 2]
|
||||
|
||||
### Negative
|
||||
- [trade-off 1]
|
||||
- [trade-off 2]
|
||||
|
||||
### Risks
|
||||
- [risk and mitigation]
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Capturing a New ADR
|
||||
|
||||
When a decision moment is detected:
|
||||
|
||||
1. **Initialize (first time only)** — if `docs/adr/` does not exist, ask the user for confirmation before creating the directory, a `README.md` seeded with the index table header (see ADR Index Format below), and a blank `template.md` for manual use. Do not create files without explicit consent.
|
||||
2. **Identify the decision** — extract the core architectural choice being made
|
||||
3. **Gather context** — what problem prompted this? What constraints exist?
|
||||
4. **Document alternatives** — what other options were considered? Why were they rejected?
|
||||
5. **State consequences** — what are the trade-offs? What becomes easier/harder?
|
||||
6. **Assign a number** — scan existing ADRs in `docs/adr/` and increment
|
||||
7. **Confirm and write** — present the draft ADR to the user for review. Only write to `docs/adr/NNNN-decision-title.md` after explicit approval. If the user declines, discard the draft without writing any files.
|
||||
8. **Update the index** — append to `docs/adr/README.md`
|
||||
|
||||
### Reading Existing ADRs
|
||||
|
||||
When a user asks "why did we choose X?":
|
||||
|
||||
1. Check if `docs/adr/` exists — if not, respond: "No ADRs found in this project. Would you like to start recording architectural decisions?"
|
||||
2. If it exists, scan `docs/adr/README.md` index for relevant entries
|
||||
3. Read matching ADR files and present the Context and Decision sections
|
||||
4. If no match is found, respond: "No ADR found for that decision. Would you like to record one now?"
|
||||
|
||||
### ADR Directory Structure
|
||||
|
||||
```
|
||||
docs/
|
||||
└── adr/
|
||||
├── README.md ← index of all ADRs
|
||||
├── 0001-use-nextjs.md
|
||||
├── 0002-postgres-over-mongo.md
|
||||
├── 0003-rest-over-graphql.md
|
||||
└── template.md ← blank template for manual use
|
||||
```
|
||||
|
||||
### ADR Index Format
|
||||
|
||||
```markdown
|
||||
# Architecture Decision Records
|
||||
|
||||
| ADR | Title | Status | Date |
|
||||
|-----|-------|--------|------|
|
||||
| [0001](0001-use-nextjs.md) | Use Next.js as frontend framework | accepted | 2026-01-15 |
|
||||
| [0002](0002-postgres-over-mongo.md) | PostgreSQL over MongoDB for primary datastore | accepted | 2026-01-20 |
|
||||
| [0003](0003-rest-over-graphql.md) | REST API over GraphQL | accepted | 2026-02-01 |
|
||||
```
|
||||
|
||||
## Decision Detection Signals
|
||||
|
||||
Watch for these patterns in conversation that indicate an architectural decision:
|
||||
|
||||
**Explicit signals**
|
||||
- "Let's go with X"
|
||||
- "We should use X instead of Y"
|
||||
- "The trade-off is worth it because..."
|
||||
- "Record this as an ADR"
|
||||
|
||||
**Implicit signals** (suggest recording an ADR — do not auto-create without user confirmation)
|
||||
- Comparing two frameworks or libraries and reaching a conclusion
|
||||
- Making a database schema design choice with stated rationale
|
||||
- Choosing between architectural patterns (monolith vs microservices, REST vs GraphQL)
|
||||
- Deciding on authentication/authorization strategy
|
||||
- Selecting deployment infrastructure after evaluating alternatives
|
||||
|
||||
## What Makes a Good ADR
|
||||
|
||||
### Do
|
||||
- **Be specific** — "Use Prisma ORM" not "use an ORM"
|
||||
- **Record the why** — the rationale matters more than the what
|
||||
- **Include rejected alternatives** — future developers need to know what was considered
|
||||
- **State consequences honestly** — every decision has trade-offs
|
||||
- **Keep it short** — an ADR should be readable in 2 minutes
|
||||
- **Use present tense** — "We use X" not "We will use X"
|
||||
|
||||
### Don't
|
||||
- Record trivial decisions — variable naming or formatting choices don't need ADRs
|
||||
- Write essays — if the context section exceeds 10 lines, it's too long
|
||||
- Omit alternatives — "we just picked it" is not a valid rationale
|
||||
- Backfill without marking it — if recording a past decision, note the original date
|
||||
- Let ADRs go stale — superseded decisions should reference their replacement
|
||||
|
||||
## ADR Lifecycle
|
||||
|
||||
```
|
||||
proposed → accepted → [deprecated | superseded by ADR-NNNN]
|
||||
```
|
||||
|
||||
- **proposed**: decision is under discussion, not yet committed
|
||||
- **accepted**: decision is in effect and being followed
|
||||
- **deprecated**: decision is no longer relevant (e.g., feature removed)
|
||||
- **superseded**: a newer ADR replaces this one (always link the replacement)
|
||||
|
||||
## Categories of Decisions Worth Recording
|
||||
|
||||
| Category | Examples |
|
||||
|----------|---------|
|
||||
| **Technology choices** | Framework, language, database, cloud provider |
|
||||
| **Architecture patterns** | Monolith vs microservices, event-driven, CQRS |
|
||||
| **API design** | REST vs GraphQL, versioning strategy, auth mechanism |
|
||||
| **Data modeling** | Schema design, normalization decisions, caching strategy |
|
||||
| **Infrastructure** | Deployment model, CI/CD pipeline, monitoring stack |
|
||||
| **Security** | Auth strategy, encryption approach, secret management |
|
||||
| **Testing** | Test framework, coverage targets, E2E vs integration balance |
|
||||
| **Process** | Branching strategy, review process, release cadence |
|
||||
|
||||
## Integration with Other Skills
|
||||
|
||||
- **Planner agent**: when the planner proposes architecture changes, suggest creating an ADR
|
||||
- **Code reviewer agent**: flag PRs that introduce architectural changes without a corresponding ADR
|
||||
@@ -0,0 +1,562 @@
|
||||
---
|
||||
name: backend-patterns
|
||||
description: Backend architecture patterns, API design, database optimization, and server-side best practices for Node.js, Express, and Next.js API routes. Use when building or reviewing Node.js, Express, or Next.js API routes and their data access.
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Backend Development Patterns
|
||||
|
||||
Backend architecture patterns and best practices for scalable server-side applications.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Designing REST or GraphQL API endpoints
|
||||
- Implementing repository, service, or controller layers
|
||||
- Optimizing database queries (N+1, indexing, connection pooling)
|
||||
- Adding caching (Redis, in-memory, HTTP cache headers)
|
||||
- Setting up background jobs or async processing
|
||||
- Structuring error handling and validation for APIs
|
||||
- Building middleware (auth, logging, rate limiting)
|
||||
|
||||
## API Design Patterns
|
||||
|
||||
### RESTful API Structure
|
||||
|
||||
```typescript
|
||||
// PASS: Resource-based URLs
|
||||
GET /api/markets # List resources
|
||||
GET /api/markets/:id # Get single resource
|
||||
POST /api/markets # Create resource
|
||||
PUT /api/markets/:id # Replace resource
|
||||
PATCH /api/markets/:id # Update resource
|
||||
DELETE /api/markets/:id # Delete resource
|
||||
|
||||
// PASS: Query parameters for filtering, sorting, pagination
|
||||
GET /api/markets?status=active&sort=volume&limit=20&offset=0
|
||||
```
|
||||
|
||||
### Repository Pattern
|
||||
|
||||
```typescript
|
||||
// Abstract data access logic
|
||||
interface MarketRepository {
|
||||
findAll(filters?: MarketFilters): Promise<Market[]>
|
||||
findById(id: string): Promise<Market | null>
|
||||
create(data: CreateMarketDto): Promise<Market>
|
||||
update(id: string, data: UpdateMarketDto): Promise<Market>
|
||||
delete(id: string): Promise<void>
|
||||
}
|
||||
|
||||
class SupabaseMarketRepository implements MarketRepository {
|
||||
async findAll(filters?: MarketFilters): Promise<Market[]> {
|
||||
let query = supabase.from('markets').select('*')
|
||||
|
||||
if (filters?.status) {
|
||||
query = query.eq('status', filters.status)
|
||||
}
|
||||
|
||||
if (filters?.limit) {
|
||||
query = query.limit(filters.limit)
|
||||
}
|
||||
|
||||
const { data, error } = await query
|
||||
|
||||
if (error) throw new Error(error.message)
|
||||
return data
|
||||
}
|
||||
|
||||
// Other methods...
|
||||
}
|
||||
```
|
||||
|
||||
### Service Layer Pattern
|
||||
|
||||
```typescript
|
||||
// Business logic separated from data access
|
||||
class MarketService {
|
||||
constructor(private marketRepo: MarketRepository) {}
|
||||
|
||||
async searchMarkets(query: string, limit: number = 10): Promise<Market[]> {
|
||||
// Business logic
|
||||
const embedding = await generateEmbedding(query)
|
||||
const results = await this.vectorSearch(embedding, limit)
|
||||
|
||||
// Fetch full data
|
||||
const markets = await this.marketRepo.findByIds(results.map(r => r.id))
|
||||
|
||||
// Sort by similarity
|
||||
return markets.sort((a, b) => {
|
||||
const scoreA = results.find(r => r.id === a.id)?.score || 0
|
||||
const scoreB = results.find(r => r.id === b.id)?.score || 0
|
||||
return scoreA - scoreB
|
||||
})
|
||||
}
|
||||
|
||||
private async vectorSearch(embedding: number[], limit: number) {
|
||||
// Vector search implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware Pattern
|
||||
|
||||
```typescript
|
||||
// Request/response processing pipeline
|
||||
export function withAuth(handler: NextApiHandler): NextApiHandler {
|
||||
return async (req, res) => {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '')
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Unauthorized' })
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await verifyToken(token)
|
||||
req.user = user
|
||||
return handler(req, res)
|
||||
} catch (error) {
|
||||
return res.status(401).json({ error: 'Invalid token' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
export default withAuth(async (req, res) => {
|
||||
// Handler has access to req.user
|
||||
})
|
||||
```
|
||||
|
||||
## Database Patterns
|
||||
|
||||
### Query Optimization
|
||||
|
||||
```typescript
|
||||
// PASS: GOOD: Select only needed columns
|
||||
const { data } = await supabase
|
||||
.from('markets')
|
||||
.select('id, name, status, volume')
|
||||
.eq('status', 'active')
|
||||
.order('volume', { ascending: false })
|
||||
.limit(10)
|
||||
|
||||
// FAIL: BAD: Select everything
|
||||
const { data } = await supabase
|
||||
.from('markets')
|
||||
.select('*')
|
||||
```
|
||||
|
||||
### N+1 Query Prevention
|
||||
|
||||
```typescript
|
||||
// FAIL: BAD: N+1 query problem
|
||||
const markets = await getMarkets()
|
||||
for (const market of markets) {
|
||||
market.creator = await getUser(market.creator_id) // N queries
|
||||
}
|
||||
|
||||
// PASS: GOOD: Batch fetch
|
||||
const markets = await getMarkets()
|
||||
const creatorIds = markets.map(m => m.creator_id)
|
||||
const creators = await getUsers(creatorIds) // 1 query
|
||||
const creatorMap = new Map(creators.map(c => [c.id, c]))
|
||||
|
||||
markets.forEach(market => {
|
||||
market.creator = creatorMap.get(market.creator_id)
|
||||
})
|
||||
```
|
||||
|
||||
### Transaction Pattern
|
||||
|
||||
```typescript
|
||||
async function createMarketWithPosition(
|
||||
marketData: CreateMarketDto,
|
||||
positionData: CreatePositionDto
|
||||
) {
|
||||
// Use Supabase transaction
|
||||
const { data, error } = await supabase.rpc('create_market_with_position', {
|
||||
market_data: marketData,
|
||||
position_data: positionData
|
||||
})
|
||||
|
||||
if (error) throw new Error('Transaction failed')
|
||||
return data
|
||||
}
|
||||
|
||||
// SQL function in Supabase
|
||||
CREATE OR REPLACE FUNCTION create_market_with_position(
|
||||
market_data jsonb,
|
||||
position_data jsonb
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
-- Start transaction automatically
|
||||
INSERT INTO markets VALUES (market_data);
|
||||
INSERT INTO positions VALUES (position_data);
|
||||
RETURN jsonb_build_object('success', true);
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
-- Rollback happens automatically
|
||||
RETURN jsonb_build_object('success', false, 'error', SQLERRM);
|
||||
END;
|
||||
$$;
|
||||
```
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
### Redis Caching Layer
|
||||
|
||||
```typescript
|
||||
class CachedMarketRepository implements MarketRepository {
|
||||
constructor(
|
||||
private baseRepo: MarketRepository,
|
||||
private redis: RedisClient
|
||||
) {}
|
||||
|
||||
async findById(id: string): Promise<Market | null> {
|
||||
// Check cache first
|
||||
const cached = await this.redis.get(`market:${id}`)
|
||||
|
||||
if (cached) {
|
||||
return JSON.parse(cached)
|
||||
}
|
||||
|
||||
// Cache miss - fetch from database
|
||||
const market = await this.baseRepo.findById(id)
|
||||
|
||||
if (market) {
|
||||
// Cache for 5 minutes
|
||||
await this.redis.setex(`market:${id}`, 300, JSON.stringify(market))
|
||||
}
|
||||
|
||||
return market
|
||||
}
|
||||
|
||||
async invalidateCache(id: string): Promise<void> {
|
||||
await this.redis.del(`market:${id}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cache-Aside Pattern
|
||||
|
||||
```typescript
|
||||
async function getMarketWithCache(id: string): Promise<Market> {
|
||||
const cacheKey = `market:${id}`
|
||||
|
||||
// Try cache
|
||||
const cached = await redis.get(cacheKey)
|
||||
if (cached) return JSON.parse(cached)
|
||||
|
||||
// Cache miss - fetch from DB
|
||||
const market = await db.markets.findUnique({ where: { id } })
|
||||
|
||||
if (!market) throw new Error('Market not found')
|
||||
|
||||
// Update cache
|
||||
await redis.setex(cacheKey, 300, JSON.stringify(market))
|
||||
|
||||
return market
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling Patterns
|
||||
|
||||
### Centralized Error Handler
|
||||
|
||||
```typescript
|
||||
class ApiError extends Error {
|
||||
constructor(
|
||||
public statusCode: number,
|
||||
public message: string,
|
||||
public isOperational = true
|
||||
) {
|
||||
super(message)
|
||||
Object.setPrototypeOf(this, ApiError.prototype)
|
||||
}
|
||||
}
|
||||
|
||||
export function errorHandler(error: unknown, req: Request): Response {
|
||||
if (error instanceof ApiError) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: error.statusCode })
|
||||
}
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Validation failed',
|
||||
details: error.issues
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// Log unexpected errors
|
||||
console.error('Unexpected error:', error)
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
}, { status: 500 })
|
||||
}
|
||||
|
||||
// Usage
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const data = await fetchData()
|
||||
return NextResponse.json({ success: true, data })
|
||||
} catch (error) {
|
||||
return errorHandler(error, request)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Retry with Exponential Backoff
|
||||
|
||||
```typescript
|
||||
async function fetchWithRetry<T>(
|
||||
fn: () => Promise<T>,
|
||||
maxRetries = 3
|
||||
): Promise<T> {
|
||||
let lastError: Error
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (error) {
|
||||
lastError = error as Error
|
||||
|
||||
if (i < maxRetries - 1) {
|
||||
// Exponential backoff: 1s, 2s, 4s
|
||||
const delay = Math.pow(2, i) * 1000
|
||||
await new Promise(resolve => setTimeout(resolve, delay))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!
|
||||
}
|
||||
|
||||
// Usage
|
||||
const data = await fetchWithRetry(() => fetchFromAPI())
|
||||
```
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
### JWT Token Validation
|
||||
|
||||
```typescript
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
interface JWTPayload {
|
||||
userId: string
|
||||
email: string
|
||||
role: 'admin' | 'user'
|
||||
}
|
||||
|
||||
export function verifyToken(token: string): JWTPayload {
|
||||
try {
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload
|
||||
return payload
|
||||
} catch (error) {
|
||||
throw new ApiError(401, 'Invalid token')
|
||||
}
|
||||
}
|
||||
|
||||
export async function requireAuth(request: Request) {
|
||||
const token = request.headers.get('authorization')?.replace('Bearer ', '')
|
||||
|
||||
if (!token) {
|
||||
throw new ApiError(401, 'Missing authorization token')
|
||||
}
|
||||
|
||||
return verifyToken(token)
|
||||
}
|
||||
|
||||
// Usage in API route
|
||||
export async function GET(request: Request) {
|
||||
const user = await requireAuth(request)
|
||||
|
||||
const data = await getDataForUser(user.userId)
|
||||
|
||||
return NextResponse.json({ success: true, data })
|
||||
}
|
||||
```
|
||||
|
||||
### Role-Based Access Control
|
||||
|
||||
```typescript
|
||||
type Permission = 'read' | 'write' | 'delete' | 'admin'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
role: 'admin' | 'moderator' | 'user'
|
||||
}
|
||||
|
||||
const rolePermissions: Record<User['role'], Permission[]> = {
|
||||
admin: ['read', 'write', 'delete', 'admin'],
|
||||
moderator: ['read', 'write', 'delete'],
|
||||
user: ['read', 'write']
|
||||
}
|
||||
|
||||
export function hasPermission(user: User, permission: Permission): boolean {
|
||||
return rolePermissions[user.role].includes(permission)
|
||||
}
|
||||
|
||||
export function requirePermission(permission: Permission) {
|
||||
return (handler: (request: Request, user: User) => Promise<Response>) => {
|
||||
return async (request: Request) => {
|
||||
const user = await requireAuth(request)
|
||||
|
||||
if (!hasPermission(user, permission)) {
|
||||
throw new ApiError(403, 'Insufficient permissions')
|
||||
}
|
||||
|
||||
return handler(request, user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage - HOF wraps the handler
|
||||
export const DELETE = requirePermission('delete')(
|
||||
async (request: Request, user: User) => {
|
||||
// Handler receives authenticated user with verified permission
|
||||
return new Response('Deleted', { status: 200 })
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
Rate limiting must use a shared store such as Redis, a gateway, or the
|
||||
platform's native limiter. Do not use per-process in-memory counters for
|
||||
production APIs: they reset on deploy, split across replicas, and fail open in
|
||||
serverless or multi-instance environments.
|
||||
|
||||
Keep the backend layer responsible for choosing the integration point and error
|
||||
shape; use `api-design` for the HTTP contract and `security-review` for abuse
|
||||
case review.
|
||||
|
||||
## Background Jobs & Queues
|
||||
|
||||
### Simple Queue Pattern
|
||||
|
||||
```typescript
|
||||
class JobQueue<T> {
|
||||
private queue: T[] = []
|
||||
private processing = false
|
||||
|
||||
async add(job: T): Promise<void> {
|
||||
this.queue.push(job)
|
||||
|
||||
if (!this.processing) {
|
||||
this.process()
|
||||
}
|
||||
}
|
||||
|
||||
private async process(): Promise<void> {
|
||||
this.processing = true
|
||||
|
||||
while (this.queue.length > 0) {
|
||||
const job = this.queue.shift()!
|
||||
|
||||
try {
|
||||
await this.execute(job)
|
||||
} catch (error) {
|
||||
console.error('Job failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
this.processing = false
|
||||
}
|
||||
|
||||
private async execute(job: T): Promise<void> {
|
||||
// Job execution logic
|
||||
}
|
||||
}
|
||||
|
||||
// Usage for indexing markets
|
||||
interface IndexJob {
|
||||
marketId: string
|
||||
}
|
||||
|
||||
const indexQueue = new JobQueue<IndexJob>()
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { marketId } = await request.json()
|
||||
|
||||
// Add to queue instead of blocking
|
||||
await indexQueue.add({ marketId })
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Job queued' })
|
||||
}
|
||||
```
|
||||
|
||||
## Logging & Monitoring
|
||||
|
||||
### Structured Logging
|
||||
|
||||
```typescript
|
||||
interface LogContext {
|
||||
userId?: string
|
||||
requestId?: string
|
||||
method?: string
|
||||
path?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
class Logger {
|
||||
log(level: 'info' | 'warn' | 'error', message: string, context?: LogContext) {
|
||||
const entry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
message,
|
||||
...context
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(entry))
|
||||
}
|
||||
|
||||
info(message: string, context?: LogContext) {
|
||||
this.log('info', message, context)
|
||||
}
|
||||
|
||||
warn(message: string, context?: LogContext) {
|
||||
this.log('warn', message, context)
|
||||
}
|
||||
|
||||
error(message: string, error: Error, context?: LogContext) {
|
||||
this.log('error', message, {
|
||||
...context,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new Logger()
|
||||
|
||||
// Usage
|
||||
export async function GET(request: Request) {
|
||||
const requestId = crypto.randomUUID()
|
||||
|
||||
logger.info('Fetching markets', {
|
||||
requestId,
|
||||
method: 'GET',
|
||||
path: '/api/markets'
|
||||
})
|
||||
|
||||
try {
|
||||
const markets = await fetchMarkets()
|
||||
return NextResponse.json({ success: true, data: markets })
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch markets', error as Error, { requestId })
|
||||
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Remember**: Backend patterns enable scalable, maintainable server-side applications. Choose patterns that fit your complexity level.
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
name: benchmark-optimization-loop
|
||||
description: Convert 'make it faster' requests into a bounded measured optimization loop — baseline first, generate one-hypothesis variants, benchmark each against a correctness gate, and promote the fastest safe variant with reproducible commands. Use when asked to speed something up, try many variants, run recursive optimization, benchmark latency/throughput/cost, or pick the best implementation by repeated measured tests.
|
||||
license: MIT
|
||||
metadata:
|
||||
origin: ECC
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
# Benchmark Optimization Loop
|
||||
|
||||
Use this skill to convert "make it 20x faster" or "try 50 recursive
|
||||
optimizations" into a bounded measured loop that can actually improve a system.
|
||||
|
||||
## Required Baseline
|
||||
|
||||
Do not optimize until these exist:
|
||||
|
||||
- the operation being optimized;
|
||||
- the correctness gate that must stay green;
|
||||
- the metric: wall time, p95 latency, rows/sec, cost/run, memory, error rate;
|
||||
- the current baseline;
|
||||
- the search budget: max variants, max time, max spend, max data impact.
|
||||
|
||||
If the user asks for an unrealistic target, keep the ambition but make the loop
|
||||
bounded and measurable.
|
||||
|
||||
## Loop
|
||||
|
||||
1. Measure the baseline.
|
||||
2. Identify bottlenecks from evidence.
|
||||
3. Generate variants that test one hypothesis each.
|
||||
4. Run variants with the same input shape.
|
||||
5. Reject variants that fail correctness, safety, or reproducibility.
|
||||
6. Promote the fastest safe variant.
|
||||
7. Codify the winning path in a script, command, test, config, or doc.
|
||||
8. Rerun the baseline and winner to confirm the delta.
|
||||
|
||||
## Variant Table
|
||||
|
||||
Track variants like this:
|
||||
|
||||
```text
|
||||
Variant | Hypothesis | Command | Time | Correct? | Notes
|
||||
baseline | current path | npm run job | 120s | yes | stable
|
||||
batch-500 | fewer round trips | npm run job -- --batch 500 | 42s | yes | winner
|
||||
parallel-8 | more workers | npm run job -- --workers 8 | 31s | no | rate limited
|
||||
```
|
||||
|
||||
## Recursive Search
|
||||
|
||||
For recursive or hyperparameter work:
|
||||
|
||||
- persist every run to a ledger;
|
||||
- compare against the prior accepted winner, not only the previous run;
|
||||
- keep a holdout or replay check;
|
||||
- stop when improvement is within noise, correctness fails, cost exceeds the
|
||||
budget, or the search starts changing more variables than it can explain.
|
||||
|
||||
Use phrases like "best measured safe variant" instead of "global optimum" unless
|
||||
the search space was actually exhaustive.
|
||||
|
||||
## Promotion Gate
|
||||
|
||||
A variant cannot become the new default until:
|
||||
|
||||
- correctness tests pass;
|
||||
- the performance delta is repeated or explained;
|
||||
- rollback is obvious;
|
||||
- the change is encoded in source control or a durable runbook;
|
||||
- the final summary includes exact commands and measurements.
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
name: benchmark
|
||||
description: Measure performance baselines and detect regressions across browser Core Web Vitals (LCP, INP, CLS, page weight), API endpoint latency percentiles, and build/test feedback times, with before/after comparison stored in git-tracked .ecc/benchmarks JSON. Use when checking page speed, responding to 'it feels slow' reports, verifying launch performance targets, or comparing stack alternatives.
|
||||
license: MIT
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Benchmark — Performance Baseline & Regression Detection
|
||||
|
||||
## When to Use
|
||||
|
||||
- Before and after a PR to measure performance impact
|
||||
- Setting up performance baselines for a project
|
||||
- When users report "it feels slow"
|
||||
- Before a launch — ensure you meet performance targets
|
||||
- Comparing your stack against alternatives
|
||||
|
||||
## How It Works
|
||||
|
||||
### Mode 1: Page Performance
|
||||
|
||||
Measures real browser metrics via browser MCP:
|
||||
|
||||
```
|
||||
1. Navigate to each target URL
|
||||
2. Measure Core Web Vitals:
|
||||
- LCP (Largest Contentful Paint) — target < 2.5s
|
||||
- CLS (Cumulative Layout Shift) — target < 0.1
|
||||
- INP (Interaction to Next Paint) — target < 200ms
|
||||
- FCP (First Contentful Paint) — target < 1.8s
|
||||
- TTFB (Time to First Byte) — target < 800ms
|
||||
3. Measure resource sizes:
|
||||
- Total page weight (target < 1MB)
|
||||
- JS bundle size (target < 200KB gzipped)
|
||||
- CSS size
|
||||
- Image weight
|
||||
- Third-party script weight
|
||||
4. Count network requests
|
||||
5. Check for render-blocking resources
|
||||
```
|
||||
|
||||
### Mode 2: API Performance
|
||||
|
||||
Benchmarks API endpoints:
|
||||
|
||||
```
|
||||
1. Hit each endpoint 100 times
|
||||
2. Measure: p50, p95, p99 latency
|
||||
3. Track: response size, status codes
|
||||
4. Test under load: 10 concurrent requests
|
||||
5. Compare against SLA targets
|
||||
```
|
||||
|
||||
### Mode 3: Build Performance
|
||||
|
||||
Measures development feedback loop:
|
||||
|
||||
```
|
||||
1. Cold build time
|
||||
2. Hot reload time (HMR)
|
||||
3. Test suite duration
|
||||
4. TypeScript check time
|
||||
5. Lint time
|
||||
6. Docker build time
|
||||
```
|
||||
|
||||
### Mode 4: Before/After Comparison
|
||||
|
||||
Run before and after a change to measure impact:
|
||||
|
||||
```
|
||||
/benchmark baseline # saves current metrics
|
||||
# ... make changes ...
|
||||
/benchmark compare # compares against baseline
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
| Metric | Before | After | Delta | Verdict |
|
||||
|--------|--------|-------|-------|---------|
|
||||
| LCP | 1.2s | 1.4s | +200ms | WARNING: WARN |
|
||||
| Bundle | 180KB | 175KB | -5KB | ✓ BETTER |
|
||||
| Build | 12s | 14s | +2s | WARNING: WARN |
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Stores baselines in `.ecc/benchmarks/` as JSON. Git-tracked so the team shares baselines.
|
||||
|
||||
## Integration
|
||||
|
||||
- CI: run `/benchmark compare` on every PR
|
||||
- Pair with `/canary-watch` for post-deploy monitoring
|
||||
- Pair with `/browser-qa` for full pre-ship checklist
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: blueprint
|
||||
description: "Turn a one-line objective into a step-by-step construction plan for multi-session, multi-agent engineering projects: one-PR-sized steps with self-contained context briefs, dependency graph with parallel-step detection, adversarial review gate, and plan mutation protocol. Use when planning a large feature, refactor, or roadmap that spans multiple PRs or sessions; not for single-PR tasks or when the user says \"just do it\"."
|
||||
metadata:
|
||||
origin: community
|
||||
---
|
||||
|
||||
# Blueprint — Construction Plan Generator
|
||||
|
||||
Turn a one-line objective into a step-by-step construction plan that any coding agent can execute cold.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Breaking a large feature into multiple PRs with clear dependency order
|
||||
- Planning a refactor or migration that spans multiple sessions
|
||||
- Coordinating parallel workstreams across sub-agents
|
||||
- Any task where context loss between sessions would cause rework
|
||||
|
||||
**Do not use** for tasks completable in a single PR, fewer than 3 tool calls, or when the user says "just do it."
|
||||
|
||||
## How It Works
|
||||
|
||||
Blueprint runs a 5-phase pipeline:
|
||||
|
||||
1. **Research** — Pre-flight checks (git, gh auth, remote, default branch), then reads project structure, existing plans, and memory files to gather context.
|
||||
2. **Design** — Breaks the objective into one-PR-sized steps (3–12 typical). Assigns dependency edges, parallel/serial ordering, model tier (strongest vs default), and rollback strategy per step.
|
||||
3. **Draft** — Writes a self-contained Markdown plan file to `plans/`. Every step includes a context brief, task list, verification commands, and exit criteria — so a fresh agent can execute any step without reading prior steps.
|
||||
4. **Review** — Delegates adversarial review to a strongest-model sub-agent (e.g., Opus) against a checklist and anti-pattern catalog. Fixes all critical findings before finalizing.
|
||||
5. **Register** — Saves the plan, updates memory index, and presents the step count and parallelism summary to the user.
|
||||
|
||||
Blueprint detects git/gh availability automatically. With git + GitHub CLI, it generates full branch/PR/CI workflow plans. Without them, it switches to direct mode (edit-in-place, no branches).
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic usage
|
||||
|
||||
```
|
||||
/blueprint myapp "migrate database to PostgreSQL"
|
||||
```
|
||||
|
||||
Produces `plans/myapp-migrate-database-to-postgresql.md` with steps like:
|
||||
- Step 1: Add PostgreSQL driver and connection config
|
||||
- Step 2: Create migration scripts for each table
|
||||
- Step 3: Update repository layer to use new driver
|
||||
- Step 4: Add integration tests against PostgreSQL
|
||||
- Step 5: Remove old database code and config
|
||||
|
||||
### Multi-agent project
|
||||
|
||||
```
|
||||
/blueprint chatbot "extract LLM providers into a plugin system"
|
||||
```
|
||||
|
||||
Produces a plan with parallel steps where possible (e.g., "implement Anthropic plugin" and "implement OpenAI plugin" run in parallel after the plugin interface step is done), model tier assignments (strongest for the interface design step, default for implementation), and invariants verified after every step (e.g., "all existing tests pass", "no provider imports in core").
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Cold-start execution** — Every step includes a self-contained context brief. No prior context needed.
|
||||
- **Adversarial review gate** — Every plan is reviewed by a strongest-model sub-agent against a checklist covering completeness, dependency correctness, and anti-pattern detection.
|
||||
- **Branch/PR/CI workflow** — Built into every step. Degrades gracefully to direct mode when git/gh is absent.
|
||||
- **Parallel step detection** — Dependency graph identifies steps with no shared files or output dependencies.
|
||||
- **Plan mutation protocol** — Steps can be split, inserted, skipped, reordered, or abandoned with formal protocols and audit trail.
|
||||
- **Zero runtime risk** — Pure Markdown skill. The entire repository contains only `.md` files — no hooks, no shell scripts, no executable code, no `package.json`, no build step. Nothing runs on install or invocation beyond Claude Code's native Markdown skill loader.
|
||||
|
||||
## Installation
|
||||
|
||||
This skill ships with Everything Claude Code. No separate installation is needed when ECC is installed.
|
||||
|
||||
### Full ECC install
|
||||
|
||||
If you are working from the ECC repository checkout, verify the skill is present with:
|
||||
|
||||
```bash
|
||||
test -f skills/blueprint/SKILL.md
|
||||
```
|
||||
|
||||
To update later, review the ECC diff before updating:
|
||||
|
||||
```bash
|
||||
cd /path/to/everything-claude-code
|
||||
git fetch origin main
|
||||
git log --oneline HEAD..origin/main # review new commits before updating
|
||||
git checkout <reviewed-full-sha> # pin to a specific reviewed commit
|
||||
```
|
||||
|
||||
### Vendored standalone install
|
||||
|
||||
If you are vendoring only this skill outside the full ECC install, copy the reviewed file from the ECC repository into `~/.claude/skills/blueprint/SKILL.md`. Vendored copies do not have a git remote, so update them by re-copying the file from a reviewed ECC commit rather than running `git pull`.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Claude Code (for `/blueprint` slash command)
|
||||
- Git + GitHub CLI (optional — enables full branch/PR/CI workflow; Blueprint detects absence and auto-switches to direct mode)
|
||||
|
||||
## Source
|
||||
|
||||
Inspired by antbotlab/blueprint — upstream project and reference design.
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: bun-runtime
|
||||
description: Bun as runtime, package manager, bundler, and test runner. When to choose Bun vs Node, migration notes, and Vercel support.
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Bun Runtime
|
||||
|
||||
Bun is a fast all-in-one JavaScript runtime and toolkit: runtime, package manager, bundler, and test runner.
|
||||
|
||||
## When to Use
|
||||
|
||||
- **Prefer Bun** for: new JS/TS projects, scripts where install/run speed matters, Vercel deployments with Bun runtime, and when you want a single toolchain (run + install + test + build).
|
||||
- **Prefer Node** for: maximum ecosystem compatibility, legacy tooling that assumes Node, or when a dependency has known Bun issues.
|
||||
|
||||
Use when: adopting Bun, migrating from Node, writing or debugging Bun scripts/tests, or configuring Bun on Vercel or other platforms.
|
||||
|
||||
## How It Works
|
||||
|
||||
- **Runtime**: Drop-in Node-compatible runtime (built on JavaScriptCore, implemented in Zig).
|
||||
- **Package manager**: `bun install` is significantly faster than npm/yarn. Lockfile is `bun.lock` (text) by default in current Bun; older versions used `bun.lockb` (binary).
|
||||
- **Bundler**: Built-in bundler and transpiler for apps and libraries.
|
||||
- **Test runner**: Built-in `bun test` with Jest-like API.
|
||||
|
||||
**Migration from Node**: Replace `node script.js` with `bun run script.js` or `bun script.js`. Run `bun install` in place of `npm install`; most packages work. Use `bun run` for npm scripts; `bun x` for npx-style one-off runs. Node built-ins are supported; prefer Bun APIs where they exist for better performance.
|
||||
|
||||
**Vercel**: Set runtime to Bun in project settings. Build: `bun run build` or `bun build ./src/index.ts --outdir=dist`. Install: `bun install --frozen-lockfile` for reproducible deploys.
|
||||
|
||||
## Examples
|
||||
|
||||
### Run and install
|
||||
|
||||
```bash
|
||||
# Install dependencies (creates/updates bun.lock or bun.lockb)
|
||||
bun install
|
||||
|
||||
# Run a script or file
|
||||
bun run dev
|
||||
bun run src/index.ts
|
||||
bun src/index.ts
|
||||
```
|
||||
|
||||
### Scripts and env
|
||||
|
||||
```bash
|
||||
bun run --env-file=.env dev
|
||||
FOO=bar bun run script.ts
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
bun test
|
||||
bun test --watch
|
||||
```
|
||||
|
||||
```typescript
|
||||
// test/example.test.ts
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
test("add", () => {
|
||||
expect(1 + 2).toBe(3);
|
||||
});
|
||||
```
|
||||
|
||||
### Runtime API
|
||||
|
||||
```typescript
|
||||
const file = Bun.file("package.json");
|
||||
const json = await file.json();
|
||||
|
||||
Bun.serve({
|
||||
port: 3000,
|
||||
fetch(req) {
|
||||
return new Response("Hello");
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Commit the lockfile (`bun.lock` or `bun.lockb`) for reproducible installs.
|
||||
- Prefer `bun run` for scripts. For TypeScript, Bun runs `.ts` natively.
|
||||
- Keep dependencies up to date; Bun and the ecosystem evolve quickly.
|
||||
@@ -0,0 +1,245 @@
|
||||
---
|
||||
name: click-path-audit
|
||||
description: "Trace every user-facing button/touchpoint through its full state change sequence to find bugs where functions individually work but cancel each other out, produce wrong final state, or leave the UI in an inconsistent state. Use when: systematic debugging found no bugs but users report broken buttons, or after any major refactor touching shared state stores."
|
||||
metadata:
|
||||
origin: community
|
||||
---
|
||||
|
||||
# /click-path-audit — Behavioural Flow Audit
|
||||
|
||||
Find bugs that static code reading misses: state interaction side effects, race conditions between sequential calls, and handlers that silently undo each other.
|
||||
|
||||
## The Problem This Solves
|
||||
|
||||
Traditional debugging checks:
|
||||
- Does the function exist? (missing wiring)
|
||||
- Does it crash? (runtime errors)
|
||||
- Does it return the right type? (data flow)
|
||||
|
||||
But it does NOT check:
|
||||
- **Does the final UI state match what the button label promises?**
|
||||
- **Does function B silently undo what function A just did?**
|
||||
- **Does shared state (Zustand/Redux/context) have side effects that cancel the intended action?**
|
||||
|
||||
Real example: A "New Email" button called `setComposeMode(true)` then `selectThread(null)`. Both worked individually. But `selectThread` had a side effect resetting `composeMode: false`. The button did nothing. 54 bugs were found by systematic debugging — this one was missed.
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
For EVERY interactive touchpoint in the target area:
|
||||
|
||||
```
|
||||
1. IDENTIFY the handler (onClick, onSubmit, onChange, etc.)
|
||||
2. TRACE every function call in the handler, IN ORDER
|
||||
3. For EACH function call:
|
||||
a. What state does it READ?
|
||||
b. What state does it WRITE?
|
||||
c. Does it have SIDE EFFECTS on shared state?
|
||||
d. Does it reset/clear any state as a side effect?
|
||||
4. CHECK: Does any later call UNDO a state change from an earlier call?
|
||||
5. CHECK: Is the FINAL state what the user expects from the button label?
|
||||
6. CHECK: Are there race conditions (async calls that resolve in wrong order)?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1: Map State Stores
|
||||
|
||||
Before auditing any touchpoint, build a side-effect map of every state store action:
|
||||
|
||||
```
|
||||
For each Zustand store / React context in scope:
|
||||
For each action/setter:
|
||||
- What fields does it set?
|
||||
- Does it RESET other fields as a side effect?
|
||||
- Document: actionName → {sets: [...], resets: [...]}
|
||||
```
|
||||
|
||||
This is the critical reference. The "New Email" bug was invisible without knowing that `selectThread` resets `composeMode`.
|
||||
|
||||
**Output format:**
|
||||
```
|
||||
STORE: emailStore
|
||||
setComposeMode(bool) → sets: {composeMode}
|
||||
selectThread(thread|null) → sets: {selectedThread, selectedThreadId, messages, drafts, selectedDraft, summary} RESETS: {composeMode: false, composeData: null, redraftOpen: false}
|
||||
setDraftGenerating(bool) → sets: {draftGenerating}
|
||||
...
|
||||
|
||||
DANGEROUS RESETS (actions that clear state they don't own):
|
||||
selectThread → resets composeMode (owned by setComposeMode)
|
||||
reset → resets everything
|
||||
```
|
||||
|
||||
### Step 2: Audit Each Touchpoint
|
||||
|
||||
For each button/toggle/form submit in the target area:
|
||||
|
||||
```
|
||||
TOUCHPOINT: [Button label] in [Component:line]
|
||||
HANDLER: onClick → {
|
||||
call 1: functionA() → sets {X: true}
|
||||
call 2: functionB() → sets {Y: null} RESETS {X: false} ← CONFLICT
|
||||
}
|
||||
EXPECTED: User sees [description of what button label promises]
|
||||
ACTUAL: X is false because functionB reset it
|
||||
VERDICT: BUG — [description]
|
||||
```
|
||||
|
||||
**Check each of these bug patterns:**
|
||||
|
||||
#### Pattern 1: Sequential Undo
|
||||
```
|
||||
handler() {
|
||||
setState_A(true) // sets X = true
|
||||
setState_B(null) // side effect: resets X = false
|
||||
}
|
||||
// Result: X is false. First call was pointless.
|
||||
```
|
||||
|
||||
#### Pattern 2: Async Race
|
||||
```
|
||||
handler() {
|
||||
fetchA().then(() => setState({ loading: false }))
|
||||
fetchB().then(() => setState({ loading: true }))
|
||||
}
|
||||
// Result: final loading state depends on which resolves first
|
||||
```
|
||||
|
||||
#### Pattern 3: Stale Closure
|
||||
```
|
||||
const [count, setCount] = useState(0)
|
||||
const handler = useCallback(() => {
|
||||
setCount(count + 1) // captures stale count
|
||||
setCount(count + 1) // same stale count — increments by 1, not 2
|
||||
}, [count])
|
||||
```
|
||||
|
||||
#### Pattern 4: Missing State Transition
|
||||
```
|
||||
// Button says "Save" but handler only validates, never actually saves
|
||||
// Button says "Delete" but handler sets a flag without calling the API
|
||||
// Button says "Send" but the API endpoint is removed/broken
|
||||
```
|
||||
|
||||
#### Pattern 5: Conditional Dead Path
|
||||
```
|
||||
handler() {
|
||||
if (someState) { // someState is ALWAYS false at this point
|
||||
doTheActualThing() // never reached
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Pattern 6: useEffect Interference
|
||||
```
|
||||
// Button sets stateX = true
|
||||
// A useEffect watches stateX and resets it to false
|
||||
// User sees nothing happen
|
||||
```
|
||||
|
||||
### Step 3: Report
|
||||
|
||||
For each bug found:
|
||||
|
||||
```
|
||||
CLICK-PATH-NNN: [severity: CRITICAL/HIGH/MEDIUM/LOW]
|
||||
Touchpoint: [Button label] in [file:line]
|
||||
Pattern: [Sequential Undo / Async Race / Stale Closure / Missing Transition / Dead Path / useEffect Interference]
|
||||
Handler: [function name or inline]
|
||||
Trace:
|
||||
1. [call] → sets {field: value}
|
||||
2. [call] → RESETS {field: value} ← CONFLICT
|
||||
Expected: [what user expects]
|
||||
Actual: [what actually happens]
|
||||
Fix: [specific fix]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scope Control
|
||||
|
||||
This audit is expensive. Scope it appropriately:
|
||||
|
||||
- **Full app audit:** Use when launching or after major refactor. Launch parallel agents per page.
|
||||
- **Single page audit:** Use after building a new page or after a user reports a broken button.
|
||||
- **Store-focused audit:** Use after modifying a Zustand store — audit all consumers of the changed actions.
|
||||
|
||||
### Recommended agent split for full app:
|
||||
|
||||
```
|
||||
Agent 1: Map ALL state stores (Step 1) — this is shared context for all other agents
|
||||
Agent 2: Dashboard (Tasks, Notes, Journal, Ideas)
|
||||
Agent 3: Chat (DanteChatColumn, JustChatPage)
|
||||
Agent 4: Emails (ThreadList, DraftArea, EmailsPage)
|
||||
Agent 5: Projects (ProjectsPage, ProjectOverviewTab, NewProjectWizard)
|
||||
Agent 6: CRM (all sub-tabs)
|
||||
Agent 7: Profile, Settings, Vault, Notifications
|
||||
Agent 8: Management Suite (all pages)
|
||||
```
|
||||
|
||||
Agent 1 MUST complete first. Its output is input for all other agents.
|
||||
|
||||
---
|
||||
|
||||
## When to Use
|
||||
|
||||
- After systematic debugging finds "no bugs" but users report broken UI
|
||||
- After modifying any Zustand store action (check all callers)
|
||||
- After any refactor that touches shared state
|
||||
- Before release, on critical user flows
|
||||
- When a button "does nothing" — this is THE tool for that
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
- For API-level bugs (wrong response shape, missing endpoint) — use systematic-debugging
|
||||
- For styling/layout issues — visual inspection
|
||||
- For performance issues — profiling tools
|
||||
|
||||
---
|
||||
|
||||
## Integration with Other Skills
|
||||
|
||||
- Run AFTER `/superpowers:systematic-debugging` (which finds the other 54 bug types)
|
||||
- Run BEFORE `/superpowers:verification-before-completion` (which verifies fixes work)
|
||||
- Feeds into `/superpowers:test-driven-development` — every bug found here should get a test
|
||||
|
||||
---
|
||||
|
||||
## Example: The Bug That Inspired This Skill
|
||||
|
||||
**ThreadList.tsx "New Email" button:**
|
||||
```
|
||||
onClick={() => {
|
||||
useEmailStore.getState().setComposeMode(true) // ✓ sets composeMode = true
|
||||
useEmailStore.getState().selectThread(null) // ✗ RESETS composeMode = false
|
||||
}}
|
||||
```
|
||||
|
||||
Store definition:
|
||||
```
|
||||
selectThread: (thread) => set({
|
||||
selectedThread: thread,
|
||||
selectedThreadId: thread?.id ?? null,
|
||||
messages: [],
|
||||
drafts: [],
|
||||
selectedDraft: null,
|
||||
summary: null,
|
||||
composeMode: false, // ← THIS silent reset killed the button
|
||||
composeData: null,
|
||||
redraftOpen: false,
|
||||
})
|
||||
```
|
||||
|
||||
**Systematic debugging missed it** because:
|
||||
- The button has an onClick handler (not dead)
|
||||
- Both functions exist (no missing wiring)
|
||||
- Neither function crashes (no runtime error)
|
||||
- The data types are correct (no type mismatch)
|
||||
|
||||
**Click-path audit catches it** because:
|
||||
- Step 1 maps `selectThread` resets `composeMode`
|
||||
- Step 2 traces the handler: call 1 sets true, call 2 resets false
|
||||
- Verdict: Sequential Undo — final state contradicts button intent
|
||||
@@ -0,0 +1,445 @@
|
||||
---
|
||||
name: clickhouse-io
|
||||
description: ClickHouse database patterns, query optimization, analytics, and data engineering best practices for high-performance analytical workloads. Use when writing ClickHouse schemas or queries, or when an analytical query is too slow.
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# ClickHouse Analytics Patterns
|
||||
|
||||
ClickHouse-specific patterns for high-performance analytics and data engineering.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Designing ClickHouse table schemas (MergeTree engine selection)
|
||||
- Writing analytical queries (aggregations, window functions, joins)
|
||||
- Optimizing query performance (partition pruning, projections, materialized views)
|
||||
- Ingesting large volumes of data (batch inserts, Kafka integration)
|
||||
- Migrating from PostgreSQL/MySQL to ClickHouse for analytics
|
||||
- Implementing real-time dashboards or time-series analytics
|
||||
|
||||
## Overview
|
||||
|
||||
ClickHouse is a column-oriented database management system (DBMS) for online analytical processing (OLAP). It's optimized for fast analytical queries on large datasets.
|
||||
|
||||
**Key Features:**
|
||||
- Column-oriented storage
|
||||
- Data compression
|
||||
- Parallel query execution
|
||||
- Distributed queries
|
||||
- Real-time analytics
|
||||
|
||||
## Table Design Patterns
|
||||
|
||||
### MergeTree Engine (Most Common)
|
||||
|
||||
```sql
|
||||
CREATE TABLE markets_analytics (
|
||||
date Date,
|
||||
market_id String,
|
||||
market_name String,
|
||||
volume UInt64,
|
||||
trades UInt32,
|
||||
unique_traders UInt32,
|
||||
avg_trade_size Float64,
|
||||
created_at DateTime
|
||||
) ENGINE = MergeTree()
|
||||
PARTITION BY toYYYYMM(date)
|
||||
ORDER BY (date, market_id)
|
||||
SETTINGS index_granularity = 8192;
|
||||
```
|
||||
|
||||
### ReplacingMergeTree (Deduplication)
|
||||
|
||||
```sql
|
||||
-- For data that may have duplicates (e.g., from multiple sources)
|
||||
CREATE TABLE user_events (
|
||||
event_id String,
|
||||
user_id String,
|
||||
event_type String,
|
||||
timestamp DateTime,
|
||||
properties String
|
||||
) ENGINE = ReplacingMergeTree()
|
||||
PARTITION BY toYYYYMM(timestamp)
|
||||
ORDER BY (user_id, event_id, timestamp)
|
||||
PRIMARY KEY (user_id, event_id);
|
||||
```
|
||||
|
||||
### AggregatingMergeTree (Pre-aggregation)
|
||||
|
||||
```sql
|
||||
-- For maintaining aggregated metrics
|
||||
CREATE TABLE market_stats_hourly (
|
||||
hour DateTime,
|
||||
market_id String,
|
||||
total_volume AggregateFunction(sum, UInt64),
|
||||
total_trades AggregateFunction(count, UInt32),
|
||||
unique_users AggregateFunction(uniq, String)
|
||||
) ENGINE = AggregatingMergeTree()
|
||||
PARTITION BY toYYYYMM(hour)
|
||||
ORDER BY (hour, market_id);
|
||||
|
||||
-- Query aggregated data
|
||||
SELECT
|
||||
hour,
|
||||
market_id,
|
||||
sumMerge(total_volume) AS volume,
|
||||
countMerge(total_trades) AS trades,
|
||||
uniqMerge(unique_users) AS users
|
||||
FROM market_stats_hourly
|
||||
WHERE hour >= toStartOfHour(now() - INTERVAL 24 HOUR)
|
||||
GROUP BY hour, market_id
|
||||
ORDER BY hour DESC;
|
||||
```
|
||||
|
||||
## Query Optimization Patterns
|
||||
|
||||
### Efficient Filtering
|
||||
|
||||
```sql
|
||||
-- PASS: GOOD: Use indexed columns first
|
||||
SELECT *
|
||||
FROM markets_analytics
|
||||
WHERE date >= '2025-01-01'
|
||||
AND market_id = 'market-123'
|
||||
AND volume > 1000
|
||||
ORDER BY date DESC
|
||||
LIMIT 100;
|
||||
|
||||
-- FAIL: BAD: Filter on non-indexed columns first
|
||||
SELECT *
|
||||
FROM markets_analytics
|
||||
WHERE volume > 1000
|
||||
AND market_name LIKE '%election%'
|
||||
AND date >= '2025-01-01';
|
||||
```
|
||||
|
||||
### Aggregations
|
||||
|
||||
```sql
|
||||
-- PASS: GOOD: Use ClickHouse-specific aggregation functions
|
||||
SELECT
|
||||
toStartOfDay(created_at) AS day,
|
||||
market_id,
|
||||
sum(volume) AS total_volume,
|
||||
count() AS total_trades,
|
||||
uniq(trader_id) AS unique_traders,
|
||||
avg(trade_size) AS avg_size
|
||||
FROM trades
|
||||
WHERE created_at >= today() - INTERVAL 7 DAY
|
||||
GROUP BY day, market_id
|
||||
ORDER BY day DESC, total_volume DESC;
|
||||
|
||||
-- PASS: Use quantile for percentiles (more efficient than percentile)
|
||||
SELECT
|
||||
quantile(0.50)(trade_size) AS median,
|
||||
quantile(0.95)(trade_size) AS p95,
|
||||
quantile(0.99)(trade_size) AS p99
|
||||
FROM trades
|
||||
WHERE created_at >= now() - INTERVAL 1 HOUR;
|
||||
```
|
||||
|
||||
### Window Functions
|
||||
|
||||
```sql
|
||||
-- Calculate running totals
|
||||
SELECT
|
||||
date,
|
||||
market_id,
|
||||
volume,
|
||||
sum(volume) OVER (
|
||||
PARTITION BY market_id
|
||||
ORDER BY date
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||||
) AS cumulative_volume
|
||||
FROM markets_analytics
|
||||
WHERE date >= today() - INTERVAL 30 DAY
|
||||
ORDER BY market_id, date;
|
||||
```
|
||||
|
||||
## Data Insertion Patterns
|
||||
|
||||
### Bulk Insert (Recommended)
|
||||
|
||||
```typescript
|
||||
import { createClient } from '@clickhouse/client'
|
||||
|
||||
const clickhouse = createClient({
|
||||
url: process.env.CLICKHOUSE_URL ?? 'http://localhost:8123',
|
||||
username: process.env.CLICKHOUSE_USER,
|
||||
password: process.env.CLICKHOUSE_PASSWORD
|
||||
})
|
||||
|
||||
// PASS: Batch insert (efficient)
|
||||
async function bulkInsertTrades(trades: Trade[]) {
|
||||
await clickhouse.insert({
|
||||
table: 'trades',
|
||||
values: trades.map(trade => ({
|
||||
id: trade.id,
|
||||
market_id: trade.market_id,
|
||||
user_id: trade.user_id,
|
||||
amount: trade.amount,
|
||||
timestamp: trade.timestamp.toISOString()
|
||||
})),
|
||||
format: 'JSONEachRow'
|
||||
})
|
||||
}
|
||||
|
||||
// FAIL: Individual inserts (slow)
|
||||
async function insertTrade(trade: Trade) {
|
||||
// Don't do this in a loop!
|
||||
await clickhouse.insert({
|
||||
table: 'trades',
|
||||
values: [{
|
||||
id: trade.id,
|
||||
market_id: trade.market_id,
|
||||
user_id: trade.user_id,
|
||||
amount: trade.amount,
|
||||
timestamp: trade.timestamp.toISOString()
|
||||
}],
|
||||
format: 'JSONEachRow'
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming Insert
|
||||
|
||||
```typescript
|
||||
// For continuous data ingestion
|
||||
import { Readable } from 'node:stream'
|
||||
|
||||
async function streamInserts(dataSource: AsyncIterable<Record<string, unknown>>) {
|
||||
await clickhouse.insert({
|
||||
table: 'trades',
|
||||
values: Readable.from(dataSource, { objectMode: true }),
|
||||
format: 'JSONEachRow'
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Materialized Views
|
||||
|
||||
### Real-time Aggregations
|
||||
|
||||
```sql
|
||||
-- Create materialized view for hourly stats
|
||||
CREATE MATERIALIZED VIEW market_stats_hourly_mv
|
||||
TO market_stats_hourly
|
||||
AS SELECT
|
||||
toStartOfHour(timestamp) AS hour,
|
||||
market_id,
|
||||
sumState(amount) AS total_volume,
|
||||
countState() AS total_trades,
|
||||
uniqState(user_id) AS unique_users
|
||||
FROM trades
|
||||
GROUP BY hour, market_id;
|
||||
|
||||
-- Query the materialized view
|
||||
SELECT
|
||||
hour,
|
||||
market_id,
|
||||
sumMerge(total_volume) AS volume,
|
||||
countMerge(total_trades) AS trades,
|
||||
uniqMerge(unique_users) AS users
|
||||
FROM market_stats_hourly
|
||||
WHERE hour >= now() - INTERVAL 24 HOUR
|
||||
GROUP BY hour, market_id;
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
### Query Performance
|
||||
|
||||
```sql
|
||||
-- Check slow queries
|
||||
SELECT
|
||||
query_id,
|
||||
user,
|
||||
query,
|
||||
query_duration_ms,
|
||||
read_rows,
|
||||
read_bytes,
|
||||
memory_usage
|
||||
FROM system.query_log
|
||||
WHERE type = 'QueryFinish'
|
||||
AND query_duration_ms > 1000
|
||||
AND event_time >= now() - INTERVAL 1 HOUR
|
||||
ORDER BY query_duration_ms DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
### Table Statistics
|
||||
|
||||
```sql
|
||||
-- Check table sizes
|
||||
SELECT
|
||||
database,
|
||||
table,
|
||||
formatReadableSize(sum(bytes)) AS size,
|
||||
sum(rows) AS rows,
|
||||
max(modification_time) AS latest_modification
|
||||
FROM system.parts
|
||||
WHERE active
|
||||
GROUP BY database, table
|
||||
ORDER BY sum(bytes) DESC;
|
||||
```
|
||||
|
||||
## Common Analytics Queries
|
||||
|
||||
### Time Series Analysis
|
||||
|
||||
```sql
|
||||
-- Daily active users
|
||||
SELECT
|
||||
toDate(timestamp) AS date,
|
||||
uniq(user_id) AS daily_active_users
|
||||
FROM events
|
||||
WHERE timestamp >= today() - INTERVAL 30 DAY
|
||||
GROUP BY date
|
||||
ORDER BY date;
|
||||
|
||||
-- Retention analysis
|
||||
SELECT
|
||||
signup_date,
|
||||
countIf(days_since_signup = 0) AS day_0,
|
||||
countIf(days_since_signup = 1) AS day_1,
|
||||
countIf(days_since_signup = 7) AS day_7,
|
||||
countIf(days_since_signup = 30) AS day_30
|
||||
FROM (
|
||||
SELECT
|
||||
user_id,
|
||||
min(toDate(timestamp)) AS signup_date,
|
||||
toDate(timestamp) AS activity_date,
|
||||
dateDiff('day', signup_date, activity_date) AS days_since_signup
|
||||
FROM events
|
||||
GROUP BY user_id, activity_date
|
||||
)
|
||||
GROUP BY signup_date
|
||||
ORDER BY signup_date DESC;
|
||||
```
|
||||
|
||||
### Funnel Analysis
|
||||
|
||||
```sql
|
||||
-- Conversion funnel
|
||||
SELECT
|
||||
countIf(step = 'viewed_market') AS viewed,
|
||||
countIf(step = 'clicked_trade') AS clicked,
|
||||
countIf(step = 'completed_trade') AS completed,
|
||||
round(clicked / viewed * 100, 2) AS view_to_click_rate,
|
||||
round(completed / clicked * 100, 2) AS click_to_completion_rate
|
||||
FROM (
|
||||
SELECT
|
||||
user_id,
|
||||
session_id,
|
||||
event_type AS step
|
||||
FROM events
|
||||
WHERE event_date = today()
|
||||
)
|
||||
GROUP BY session_id;
|
||||
```
|
||||
|
||||
### Cohort Analysis
|
||||
|
||||
```sql
|
||||
-- User cohorts by signup month
|
||||
SELECT
|
||||
toStartOfMonth(signup_date) AS cohort,
|
||||
toStartOfMonth(activity_date) AS month,
|
||||
dateDiff('month', cohort, month) AS months_since_signup,
|
||||
count(DISTINCT user_id) AS active_users
|
||||
FROM (
|
||||
SELECT
|
||||
user_id,
|
||||
min(toDate(timestamp)) OVER (PARTITION BY user_id) AS signup_date,
|
||||
toDate(timestamp) AS activity_date
|
||||
FROM events
|
||||
)
|
||||
GROUP BY cohort, month, months_since_signup
|
||||
ORDER BY cohort, months_since_signup;
|
||||
```
|
||||
|
||||
## Data Pipeline Patterns
|
||||
|
||||
### ETL Pattern
|
||||
|
||||
```typescript
|
||||
// Extract, Transform, Load
|
||||
async function etlPipeline() {
|
||||
// 1. Extract from source
|
||||
const rawData = await extractFromPostgres()
|
||||
|
||||
// 2. Transform
|
||||
const transformed = rawData.map(row => ({
|
||||
date: new Date(row.created_at).toISOString().split('T')[0],
|
||||
market_id: row.market_slug,
|
||||
volume: parseFloat(row.total_volume),
|
||||
trades: parseInt(row.trade_count)
|
||||
}))
|
||||
|
||||
// 3. Load to ClickHouse
|
||||
await bulkInsertToClickHouse(transformed)
|
||||
}
|
||||
|
||||
// Run periodically
|
||||
setInterval(etlPipeline, 60 * 60 * 1000) // Every hour
|
||||
```
|
||||
|
||||
### Change Data Capture (CDC)
|
||||
|
||||
```typescript
|
||||
// Listen to PostgreSQL changes and sync to ClickHouse
|
||||
import { Client } from 'pg'
|
||||
|
||||
const pgClient = new Client({ connectionString: process.env.DATABASE_URL })
|
||||
|
||||
pgClient.query('LISTEN market_updates')
|
||||
|
||||
pgClient.on('notification', async (msg) => {
|
||||
const update = JSON.parse(msg.payload)
|
||||
|
||||
await clickhouse.insert({
|
||||
table: 'market_updates',
|
||||
values: [
|
||||
{
|
||||
market_id: update.id,
|
||||
event_type: update.operation, // INSERT, UPDATE, DELETE
|
||||
timestamp: new Date(),
|
||||
data: JSON.stringify(update.new_data)
|
||||
}
|
||||
],
|
||||
format: 'JSONEachRow'
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Partitioning Strategy
|
||||
- Partition by time (usually month or day)
|
||||
- Avoid too many partitions (performance impact)
|
||||
- Use DATE type for partition key
|
||||
|
||||
### 2. Ordering Key
|
||||
- Put most frequently filtered columns first
|
||||
- Consider cardinality (high cardinality first)
|
||||
- Order impacts compression
|
||||
|
||||
### 3. Data Types
|
||||
- Use smallest appropriate type (UInt32 vs UInt64)
|
||||
- Use LowCardinality for repeated strings
|
||||
- Use Enum for categorical data
|
||||
|
||||
### 4. Avoid
|
||||
- SELECT * (specify columns)
|
||||
- FINAL (merge data before query instead)
|
||||
- Too many JOINs (denormalize for analytics)
|
||||
- Small frequent inserts (batch instead)
|
||||
|
||||
### 5. Monitoring
|
||||
- Track query performance
|
||||
- Monitor disk usage
|
||||
- Check merge operations
|
||||
- Review slow query log
|
||||
|
||||
**Remember**: ClickHouse excels at analytical workloads. Design tables for your query patterns, batch inserts, and leverage materialized views for real-time aggregations.
|
||||
@@ -0,0 +1,254 @@
|
||||
---
|
||||
name: code-tour
|
||||
description: Create CodeTour `.tour` files — persona-targeted, step-by-step walkthroughs with real file and line anchors. Use for onboarding tours, architecture walkthroughs, PR tours, RCA tours, and structured "explain how this works" requests. Use when the user asks for a code tour, onboarding walkthrough, PR tour, or an explanation of how a subsystem works.
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Code Tour
|
||||
|
||||
Create **CodeTour** `.tour` files for codebase walkthroughs that open directly to real files and line ranges. Tours live in `.tours/` and are meant for the CodeTour format, not ad hoc Markdown notes.
|
||||
|
||||
A good tour is a narrative for a specific reader:
|
||||
- what they are looking at
|
||||
- why it matters
|
||||
- what path they should follow next
|
||||
|
||||
Only create `.tour` JSON files. Do not modify source code as part of this skill.
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this skill when:
|
||||
- the user asks for a code tour, onboarding tour, architecture walkthrough, or PR tour
|
||||
- the user says "explain how X works" and wants a reusable guided artifact
|
||||
- the user wants a ramp-up path for a new engineer or reviewer
|
||||
- the task is better served by a guided sequence than a flat summary
|
||||
|
||||
Examples:
|
||||
- onboarding a new maintainer
|
||||
- architecture tour for one service or package
|
||||
- PR-review walk-through anchored to changed files
|
||||
- RCA tour showing the failure path
|
||||
- security review tour of trust boundaries and key checks
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
| Instead of code-tour | Use |
|
||||
| --- | --- |
|
||||
| A one-off explanation in chat is enough | answer directly |
|
||||
| The user wants prose docs, not a `.tour` artifact | `documentation-lookup` or repo docs editing |
|
||||
| The task is implementation or refactoring | do the implementation work |
|
||||
| The task is broad codebase onboarding without a tour artifact | `codebase-onboarding` |
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Discover
|
||||
|
||||
Explore the repo before writing anything:
|
||||
- README and package/app entry points
|
||||
- folder structure
|
||||
- relevant config files
|
||||
- the changed files if the tour is PR-focused
|
||||
|
||||
Do not start writing steps before you understand the shape of the code.
|
||||
|
||||
### 2. Infer the reader
|
||||
|
||||
Decide the persona and depth from the request.
|
||||
|
||||
| Request shape | Persona | Suggested depth |
|
||||
| --- | --- | --- |
|
||||
| "onboarding", "new joiner" | `new-joiner` | 9-13 steps |
|
||||
| "quick tour", "vibe check" | `vibecoder` | 5-8 steps |
|
||||
| "architecture" | `architect` | 14-18 steps |
|
||||
| "tour this PR" | `pr-reviewer` | 7-11 steps |
|
||||
| "why did this break" | `rca-investigator` | 7-11 steps |
|
||||
| "security review" | `security-reviewer` | 7-11 steps |
|
||||
| "explain how this feature works" | `feature-explainer` | 7-11 steps |
|
||||
| "debug this path" | `bug-fixer` | 7-11 steps |
|
||||
|
||||
### 3. Read and verify anchors
|
||||
|
||||
Every file path and line anchor must be real:
|
||||
- confirm the file exists
|
||||
- confirm the line numbers are in range
|
||||
- if using a selection, verify the exact block
|
||||
- if the file is volatile, prefer a pattern-based anchor
|
||||
|
||||
Never guess line numbers.
|
||||
|
||||
### 4. Write the `.tour`
|
||||
|
||||
Write to:
|
||||
|
||||
```text
|
||||
.tours/<persona>-<focus>.tour
|
||||
```
|
||||
|
||||
Keep the path deterministic and readable.
|
||||
|
||||
### 5. Validate
|
||||
|
||||
Before finishing:
|
||||
- every referenced path exists
|
||||
- every line or selection is valid
|
||||
- the first step is anchored to a real file or directory
|
||||
- the `ref` points at a branch or commit that actually has every file the tour references (see below)
|
||||
- the tour tells a coherent story rather than listing files
|
||||
|
||||
## The `ref` Field
|
||||
|
||||
`ref` ties the tour to a git branch or commit. It matters more than it looks: when `ref` is not the branch the reader has checked out, CodeTour opens each step's file from that revision in git, not from the files on disk. If a file is not in that revision, the step will not open — the reader sees *"The editor could not be opened because the file was not found"* even though the file is sitting right there. The tour and its comments still show, so the real cause is easy to miss.
|
||||
|
||||
Pick `ref` by tour type:
|
||||
|
||||
| Tour type | Set `ref` to |
|
||||
| --- | --- |
|
||||
| PR tour | the PR branch — never the base branch |
|
||||
| Onboarding / architecture | the branch the reader will be on (often `main`), or leave it out |
|
||||
| Not sure | leave `ref` out, so CodeTour reads files straight from disk |
|
||||
|
||||
The PR case is the common trap: a PR usually adds new files, and new files do not exist on the base branch yet. Point `ref` at the base (e.g. `develop`) and every step on a new file fails to open.
|
||||
|
||||
Before finishing, confirm each step's file actually exists at the `ref` you chose.
|
||||
|
||||
## Step Types
|
||||
|
||||
### Content
|
||||
|
||||
Use sparingly, usually only for a closing step:
|
||||
|
||||
```json
|
||||
{ "title": "Next Steps", "description": "You can now trace the request path end to end." }
|
||||
```
|
||||
|
||||
Do not make the first step content-only.
|
||||
|
||||
### Directory
|
||||
|
||||
Use to orient the reader to a module:
|
||||
|
||||
```json
|
||||
{ "directory": "src/services", "title": "Service Layer", "description": "The core orchestration logic lives here." }
|
||||
```
|
||||
|
||||
### File + line
|
||||
|
||||
This is the default step type:
|
||||
|
||||
```json
|
||||
{ "file": "src/auth/middleware.ts", "line": 42, "title": "Auth Gate", "description": "Every protected request passes here first." }
|
||||
```
|
||||
|
||||
### Selection
|
||||
|
||||
Use when one code block matters more than the whole file:
|
||||
|
||||
```json
|
||||
{
|
||||
"file": "src/core/pipeline.ts",
|
||||
"selection": {
|
||||
"start": { "line": 15, "character": 0 },
|
||||
"end": { "line": 34, "character": 0 }
|
||||
},
|
||||
"title": "Request Pipeline",
|
||||
"description": "This block wires validation, auth, and downstream execution."
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern
|
||||
|
||||
Use when exact lines may drift:
|
||||
|
||||
```json
|
||||
{ "file": "src/app.ts", "pattern": "export default class App", "title": "Application Entry" }
|
||||
```
|
||||
|
||||
### URI
|
||||
|
||||
Use for PRs, issues, or docs when helpful:
|
||||
|
||||
```json
|
||||
{ "uri": "https://github.com/org/repo/pull/456", "title": "The PR" }
|
||||
```
|
||||
|
||||
## Writing Rule: SMIG
|
||||
|
||||
Each description should answer:
|
||||
- **Situation**: what the reader is looking at
|
||||
- **Mechanism**: how it works
|
||||
- **Implication**: why it matters for this persona
|
||||
- **Gotcha**: what a smart reader might miss
|
||||
|
||||
Keep descriptions compact, specific, and grounded in the actual code.
|
||||
|
||||
## Narrative Shape
|
||||
|
||||
Use this arc unless the task clearly needs something different:
|
||||
1. orientation
|
||||
2. module map
|
||||
3. core execution path
|
||||
4. edge case or gotcha
|
||||
5. closing / next move
|
||||
|
||||
The tour should feel like a path, not an inventory.
|
||||
|
||||
## Example
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://aka.ms/codetour-schema",
|
||||
"title": "API Service Tour",
|
||||
"description": "Walkthrough of the request path for the payments service.",
|
||||
"ref": "main",
|
||||
"steps": [
|
||||
{
|
||||
"directory": "src",
|
||||
"title": "Source Root",
|
||||
"description": "All runtime code for the service starts here."
|
||||
},
|
||||
{
|
||||
"file": "src/server.ts",
|
||||
"line": 12,
|
||||
"title": "Entry Point",
|
||||
"description": "The server boots here and wires middleware before any route is reached."
|
||||
},
|
||||
{
|
||||
"file": "src/routes/payments.ts",
|
||||
"line": 8,
|
||||
"title": "Payment Routes",
|
||||
"description": "Every payments request enters through this router before hitting service logic."
|
||||
},
|
||||
{
|
||||
"title": "Next Steps",
|
||||
"description": "You can now follow any payment request end to end with the main anchors in place."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-pattern | Fix |
|
||||
| --- | --- |
|
||||
| Flat file listing | Tell a story with dependency between steps |
|
||||
| Generic descriptions | Name the concrete code path or pattern |
|
||||
| Guessed anchors | Verify every file and line first |
|
||||
| Too many steps for a quick tour | Cut aggressively |
|
||||
| First step is content-only | Anchor the first step to a real file or directory |
|
||||
| Persona mismatch | Write for the actual reader, not a generic engineer |
|
||||
|
||||
## Best Practices
|
||||
|
||||
- keep step count proportional to repo size and persona depth
|
||||
- use directory steps for orientation, file steps for substance
|
||||
- for PR tours, cover changed files first
|
||||
- for monorepos, scope to the relevant packages instead of touring everything
|
||||
- close with what the reader can now do, not a recap
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `codebase-onboarding`
|
||||
- `coding-standards`
|
||||
- `council`
|
||||
- official upstream format: `microsoft/codetour`
|
||||
@@ -0,0 +1,234 @@
|
||||
---
|
||||
name: codebase-onboarding
|
||||
description: Analyze an unfamiliar codebase and generate a structured onboarding guide with architecture map, key entry points, conventions, and a starter CLAUDE.md. Use when joining a new project or setting up Claude Code for the first time in a repo.
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Codebase Onboarding
|
||||
|
||||
Systematically analyze an unfamiliar codebase and produce a structured onboarding guide. Designed for developers joining a new project or setting up Claude Code in an existing repo for the first time.
|
||||
|
||||
## When to Use
|
||||
|
||||
- First time opening a project with Claude Code
|
||||
- Joining a new team or repository
|
||||
- User asks "help me understand this codebase"
|
||||
- User asks to generate a CLAUDE.md for a project
|
||||
- User says "onboard me" or "walk me through this repo"
|
||||
|
||||
## How It Works
|
||||
|
||||
### Phase 1: Reconnaissance
|
||||
|
||||
Gather raw signals about the project without reading every file. Run these checks in parallel:
|
||||
|
||||
```
|
||||
1. Package manifest detection
|
||||
→ package.json, go.mod, Cargo.toml, pyproject.toml, pom.xml, build.gradle,
|
||||
Gemfile, composer.json, mix.exs, pubspec.yaml
|
||||
|
||||
2. Framework fingerprinting
|
||||
→ next.config.*, nuxt.config.*, angular.json, vite.config.*,
|
||||
django settings, flask app factory, fastapi main, rails config
|
||||
|
||||
3. Entry point identification
|
||||
→ main.*, index.*, app.*, server.*, cmd/, src/main/
|
||||
|
||||
4. Directory structure snapshot
|
||||
→ Top 2 levels of the directory tree, ignoring node_modules, vendor,
|
||||
.git, dist, build, __pycache__, .next
|
||||
|
||||
5. Config and tooling detection
|
||||
→ .eslintrc*, .prettierrc*, tsconfig.json, Makefile, Dockerfile,
|
||||
docker-compose*, .github/workflows/, .env.example, CI configs
|
||||
|
||||
6. Test structure detection
|
||||
→ tests/, test/, __tests__/, *_test.go, *.spec.ts, *.test.js,
|
||||
pytest.ini, jest.config.*, vitest.config.*
|
||||
```
|
||||
|
||||
### Phase 2: Architecture Mapping
|
||||
|
||||
From the reconnaissance data, identify:
|
||||
|
||||
**Tech Stack**
|
||||
- Language(s) and version constraints
|
||||
- Framework(s) and major libraries
|
||||
- Database(s) and ORMs
|
||||
- Build tools and bundlers
|
||||
- CI/CD platform
|
||||
|
||||
**Architecture Pattern**
|
||||
- Monolith, monorepo, microservices, or serverless
|
||||
- Frontend/backend split or full-stack
|
||||
- API style: REST, GraphQL, gRPC, tRPC
|
||||
|
||||
**Key Directories**
|
||||
Map the top-level directories to their purpose:
|
||||
|
||||
<!-- Example for a React project — replace with detected directories -->
|
||||
```
|
||||
src/components/ → React UI components
|
||||
src/api/ → API route handlers
|
||||
src/lib/ → Shared utilities
|
||||
src/db/ → Database models and migrations
|
||||
tests/ → Test suites
|
||||
scripts/ → Build and deployment scripts
|
||||
```
|
||||
|
||||
**Data Flow**
|
||||
Trace one request from entry to response:
|
||||
- Where does a request enter? (router, handler, controller)
|
||||
- How is it validated? (middleware, schemas, guards)
|
||||
- Where is business logic? (services, models, use cases)
|
||||
- How does it reach the database? (ORM, raw queries, repositories)
|
||||
|
||||
### Phase 3: Convention Detection
|
||||
|
||||
Identify patterns the codebase already follows:
|
||||
|
||||
**Naming Conventions**
|
||||
- File naming: kebab-case, camelCase, PascalCase, snake_case
|
||||
- Component/class naming patterns
|
||||
- Test file naming: `*.test.ts`, `*.spec.ts`, `*_test.go`
|
||||
|
||||
**Code Patterns**
|
||||
- Error handling style: try/catch, Result types, error codes
|
||||
- Dependency injection or direct imports
|
||||
- State management approach
|
||||
- Async patterns: callbacks, promises, async/await, channels
|
||||
|
||||
**Git Conventions**
|
||||
- Branch naming from recent branches
|
||||
- Commit message style from recent commits
|
||||
- PR workflow (squash, merge, rebase)
|
||||
- If the repo has no commits yet or only a shallow history (e.g. `git clone --depth 1`), skip this section and note "Git history unavailable or too shallow to detect conventions"
|
||||
|
||||
### Phase 4: Generate Onboarding Artifacts
|
||||
|
||||
Produce two outputs:
|
||||
|
||||
#### Output 1: Onboarding Guide
|
||||
|
||||
```markdown
|
||||
# Onboarding Guide: [Project Name]
|
||||
|
||||
## Overview
|
||||
[2-3 sentences: what this project does and who it serves]
|
||||
|
||||
## Tech Stack
|
||||
<!-- Example for a Next.js project — replace with detected stack -->
|
||||
| Layer | Technology | Version |
|
||||
|-------|-----------|---------|
|
||||
| Language | TypeScript | 5.x |
|
||||
| Framework | Next.js | 14.x |
|
||||
| Database | PostgreSQL | 16 |
|
||||
| ORM | Prisma | 5.x |
|
||||
| Testing | Jest + Playwright | - |
|
||||
|
||||
## Architecture
|
||||
[Diagram or description of how components connect]
|
||||
|
||||
## Key Entry Points
|
||||
<!-- Example for a Next.js project — replace with detected paths -->
|
||||
- **API routes**: `src/app/api/` — Next.js route handlers
|
||||
- **UI pages**: `src/app/(dashboard)/` — authenticated pages
|
||||
- **Database**: `prisma/schema.prisma` — data model source of truth
|
||||
- **Config**: `next.config.ts` — build and runtime config
|
||||
|
||||
## Directory Map
|
||||
[Top-level directory → purpose mapping]
|
||||
|
||||
## Request Lifecycle
|
||||
[Trace one API request from entry to response]
|
||||
|
||||
## Conventions
|
||||
- [File naming pattern]
|
||||
- [Error handling approach]
|
||||
- [Testing patterns]
|
||||
- [Git workflow]
|
||||
|
||||
## Common Tasks
|
||||
<!-- Example for a Node.js project — replace with detected commands -->
|
||||
- **Run dev server**: `npm run dev`
|
||||
- **Run tests**: `npm test`
|
||||
- **Run linter**: `npm run lint`
|
||||
- **Database migrations**: `npx prisma migrate dev`
|
||||
- **Build for production**: `npm run build`
|
||||
|
||||
## Where to Look
|
||||
<!-- Example for a Next.js project — replace with detected paths -->
|
||||
| I want to... | Look at... |
|
||||
|--------------|-----------|
|
||||
| Add an API endpoint | `src/app/api/` |
|
||||
| Add a UI page | `src/app/(dashboard)/` |
|
||||
| Add a database table | `prisma/schema.prisma` |
|
||||
| Add a test | `tests/` matching the source path |
|
||||
| Change build config | `next.config.ts` |
|
||||
```
|
||||
|
||||
#### Output 2: Starter CLAUDE.md
|
||||
|
||||
Generate or update a project-specific CLAUDE.md based on detected conventions. If `CLAUDE.md` already exists, read it first and enhance it — preserve existing project-specific instructions and clearly call out what was added or changed.
|
||||
|
||||
```markdown
|
||||
# Project Instructions
|
||||
|
||||
## Tech Stack
|
||||
[Detected stack summary]
|
||||
|
||||
## Code Style
|
||||
- [Detected naming conventions]
|
||||
- [Detected patterns to follow]
|
||||
|
||||
## Testing
|
||||
- Run tests: `[detected test command]`
|
||||
- Test pattern: [detected test file convention]
|
||||
- Coverage: [if configured, the coverage command]
|
||||
|
||||
## Build & Run
|
||||
- Dev: `[detected dev command]`
|
||||
- Build: `[detected build command]`
|
||||
- Lint: `[detected lint command]`
|
||||
|
||||
## Project Structure
|
||||
[Key directory → purpose map]
|
||||
|
||||
## Conventions
|
||||
- [Commit style if detectable]
|
||||
- [PR workflow if detectable]
|
||||
- [Error handling patterns]
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Don't read everything** — reconnaissance should use Glob and Grep, not Read on every file. Read selectively only for ambiguous signals.
|
||||
2. **Verify, don't guess** — if a framework is detected from config but the actual code uses something different, trust the code.
|
||||
3. **Respect existing CLAUDE.md** — if one already exists, enhance it rather than replacing it. Call out what's new vs existing.
|
||||
4. **Stay concise** — the onboarding guide should be scannable in 2 minutes. Details belong in the code, not the guide.
|
||||
5. **Flag unknowns** — if a convention can't be confidently detected, say so rather than guessing. "Could not determine test runner" is better than a wrong answer.
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- Generating a CLAUDE.md that's longer than 100 lines — keep it focused
|
||||
- Listing every dependency — highlight only the ones that shape how you write code
|
||||
- Describing obvious directory names — `src/` doesn't need an explanation
|
||||
- Copying the README — the onboarding guide adds structural insight the README lacks
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: First time in a new repo
|
||||
**User**: "Onboard me to this codebase"
|
||||
**Action**: Run full 4-phase workflow → produce Onboarding Guide + Starter CLAUDE.md
|
||||
**Output**: Onboarding Guide printed directly to the conversation, plus a `CLAUDE.md` written to the project root
|
||||
|
||||
### Example 2: Generate CLAUDE.md for existing project
|
||||
**User**: "Generate a CLAUDE.md for this project"
|
||||
**Action**: Run Phases 1-3, skip Onboarding Guide, produce only CLAUDE.md
|
||||
**Output**: Project-specific `CLAUDE.md` with detected conventions
|
||||
|
||||
### Example 3: Enhance existing CLAUDE.md
|
||||
**User**: "Update the CLAUDE.md with current project conventions"
|
||||
**Action**: Read existing CLAUDE.md, run Phases 1-3, merge new findings
|
||||
**Output**: Updated `CLAUDE.md` with additions clearly marked
|
||||
@@ -0,0 +1,551 @@
|
||||
---
|
||||
name: coding-standards
|
||||
description: Baseline cross-project coding conventions for naming, readability, immutability, and code-quality review. Use detailed frontend or backend skills for framework-specific patterns. Use when reviewing code quality or naming with no framework-specific skill that applies.
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Coding Standards & Best Practices
|
||||
|
||||
Baseline coding conventions applicable across projects.
|
||||
|
||||
This skill is the shared floor, not the detailed framework playbook.
|
||||
|
||||
- Use `frontend-patterns` for React, state, forms, rendering, and UI architecture.
|
||||
- Use `backend-patterns` or `api-design` for repository/service layers, endpoint design, validation, and server-specific concerns.
|
||||
- Use `rules/common/coding-style.md` when you need the shortest reusable rule layer instead of a full skill walkthrough.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Starting a new project or module
|
||||
- Reviewing code for quality and maintainability
|
||||
- Refactoring existing code to follow conventions
|
||||
- Enforcing naming, formatting, or structural consistency
|
||||
- Setting up linting, formatting, or type-checking rules
|
||||
- Onboarding new contributors to coding conventions
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
Activate this skill for:
|
||||
- descriptive naming
|
||||
- immutability defaults
|
||||
- readability, KISS, DRY, and YAGNI enforcement
|
||||
- error-handling expectations and code-smell review
|
||||
|
||||
Do not use this skill as the primary source for:
|
||||
- React composition, hooks, or rendering patterns
|
||||
- backend architecture, API design, or database layering
|
||||
- domain-specific framework guidance when a narrower ECC skill already exists
|
||||
|
||||
## Code Quality Principles
|
||||
|
||||
### 1. Readability First
|
||||
- Code is read more than written
|
||||
- Clear variable and function names
|
||||
- Self-documenting code preferred over comments
|
||||
- Consistent formatting
|
||||
|
||||
### 2. KISS (Keep It Simple, Stupid)
|
||||
- Simplest solution that works
|
||||
- Avoid over-engineering
|
||||
- No premature optimization
|
||||
- Easy to understand > clever code
|
||||
|
||||
### 3. DRY (Don't Repeat Yourself)
|
||||
- Extract common logic into functions
|
||||
- Create reusable components
|
||||
- Share utilities across modules
|
||||
- Avoid copy-paste programming
|
||||
|
||||
### 4. YAGNI (You Aren't Gonna Need It)
|
||||
- Don't build features before they're needed
|
||||
- Avoid speculative generality
|
||||
- Add complexity only when required
|
||||
- Start simple, refactor when needed
|
||||
|
||||
## TypeScript/JavaScript Standards
|
||||
|
||||
### Variable Naming
|
||||
|
||||
```typescript
|
||||
// PASS: GOOD: Descriptive names
|
||||
const marketSearchQuery = 'election'
|
||||
const isUserAuthenticated = true
|
||||
const totalRevenue = 1000
|
||||
|
||||
// FAIL: BAD: Unclear names
|
||||
const q = 'election'
|
||||
const flag = true
|
||||
const x = 1000
|
||||
```
|
||||
|
||||
### Function Naming
|
||||
|
||||
```typescript
|
||||
// PASS: GOOD: Verb-noun pattern
|
||||
async function fetchMarketData(marketId: string) { }
|
||||
function calculateSimilarity(a: number[], b: number[]) { }
|
||||
function isValidEmail(email: string): boolean { }
|
||||
|
||||
// FAIL: BAD: Unclear or noun-only
|
||||
async function market(id: string) { }
|
||||
function similarity(a, b) { }
|
||||
function email(e) { }
|
||||
```
|
||||
|
||||
### Immutability Pattern (CRITICAL)
|
||||
|
||||
```typescript
|
||||
// PASS: ALWAYS use spread operator
|
||||
const updatedUser = {
|
||||
...user,
|
||||
name: 'New Name'
|
||||
}
|
||||
|
||||
const updatedArray = [...items, newItem]
|
||||
|
||||
// FAIL: NEVER mutate directly
|
||||
user.name = 'New Name' // BAD
|
||||
items.push(newItem) // BAD
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
// PASS: GOOD: Comprehensive error handling
|
||||
async function fetchData(url: string) {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Fetch failed:', error)
|
||||
throw new Error('Failed to fetch data')
|
||||
}
|
||||
}
|
||||
|
||||
// FAIL: BAD: No error handling
|
||||
async function fetchData(url) {
|
||||
const response = await fetch(url)
|
||||
return response.json()
|
||||
}
|
||||
```
|
||||
|
||||
### Async/Await Best Practices
|
||||
|
||||
```typescript
|
||||
// PASS: GOOD: Parallel execution when possible
|
||||
const [users, markets, stats] = await Promise.all([
|
||||
fetchUsers(),
|
||||
fetchMarkets(),
|
||||
fetchStats()
|
||||
])
|
||||
|
||||
// FAIL: BAD: Sequential when unnecessary
|
||||
const users = await fetchUsers()
|
||||
const markets = await fetchMarkets()
|
||||
const stats = await fetchStats()
|
||||
```
|
||||
|
||||
### Type Safety
|
||||
|
||||
```typescript
|
||||
// PASS: GOOD: Proper types
|
||||
interface Market {
|
||||
id: string
|
||||
name: string
|
||||
status: 'active' | 'resolved' | 'closed'
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
function getMarket(id: string): Promise<Market> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// FAIL: BAD: Using 'any'
|
||||
function getMarket(id: any): Promise<any> {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
## React Best Practices
|
||||
|
||||
### Component Structure
|
||||
|
||||
```typescript
|
||||
// PASS: GOOD: Functional component with types
|
||||
interface ButtonProps {
|
||||
children: React.ReactNode
|
||||
onClick: () => void
|
||||
disabled?: boolean
|
||||
variant?: 'primary' | 'secondary'
|
||||
}
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
onClick,
|
||||
disabled = false,
|
||||
variant = 'primary'
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`btn btn-${variant}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// FAIL: BAD: No types, unclear structure
|
||||
export function Button(props) {
|
||||
return <button onClick={props.onClick}>{props.children}</button>
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Hooks
|
||||
|
||||
```typescript
|
||||
// PASS: GOOD: Reusable custom hook
|
||||
export function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, delay)
|
||||
|
||||
return () => clearTimeout(handler)
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
|
||||
// Usage
|
||||
const debouncedQuery = useDebounce(searchQuery, 500)
|
||||
```
|
||||
|
||||
### State Management
|
||||
|
||||
```typescript
|
||||
// PASS: GOOD: Proper state updates
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
// Functional update for state based on previous state
|
||||
setCount(prev => prev + 1)
|
||||
|
||||
// FAIL: BAD: Direct state reference
|
||||
setCount(count + 1) // Can be stale in async scenarios
|
||||
```
|
||||
|
||||
### Conditional Rendering
|
||||
|
||||
```typescript
|
||||
// PASS: GOOD: Clear conditional rendering
|
||||
{isLoading && <Spinner />}
|
||||
{error && <ErrorMessage error={error} />}
|
||||
{data && <DataDisplay data={data} />}
|
||||
|
||||
// FAIL: BAD: Ternary hell
|
||||
{isLoading ? <Spinner /> : error ? <ErrorMessage error={error} /> : data ? <DataDisplay data={data} /> : null}
|
||||
```
|
||||
|
||||
## API Design Standards
|
||||
|
||||
### REST API Conventions
|
||||
|
||||
```
|
||||
GET /api/markets # List all markets
|
||||
GET /api/markets/:id # Get specific market
|
||||
POST /api/markets # Create new market
|
||||
PUT /api/markets/:id # Update market (full)
|
||||
PATCH /api/markets/:id # Update market (partial)
|
||||
DELETE /api/markets/:id # Delete market
|
||||
|
||||
# Query parameters for filtering
|
||||
GET /api/markets?status=active&limit=10&offset=0
|
||||
```
|
||||
|
||||
### Response Format
|
||||
|
||||
```typescript
|
||||
// PASS: GOOD: Consistent response structure
|
||||
interface ApiResponse<T> {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
meta?: {
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
}
|
||||
}
|
||||
|
||||
// Success response
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: markets,
|
||||
meta: { total: 100, page: 1, limit: 10 }
|
||||
})
|
||||
|
||||
// Error response
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Invalid request'
|
||||
}, { status: 400 })
|
||||
```
|
||||
|
||||
### Input Validation
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
|
||||
// PASS: GOOD: Schema validation
|
||||
const CreateMarketSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
description: z.string().min(1).max(2000),
|
||||
endDate: z.string().datetime(),
|
||||
categories: z.array(z.string()).min(1)
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json()
|
||||
|
||||
try {
|
||||
const validated = CreateMarketSchema.parse(body)
|
||||
// Proceed with validated data
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Validation failed',
|
||||
details: error.issues
|
||||
}, { status: 400 })
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## File Organization
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── api/ # API routes
|
||||
│ ├── markets/ # Market pages
|
||||
│ └── (auth)/ # Auth pages (route groups)
|
||||
├── components/ # React components
|
||||
│ ├── ui/ # Generic UI components
|
||||
│ ├── forms/ # Form components
|
||||
│ └── layouts/ # Layout components
|
||||
├── hooks/ # Custom React hooks
|
||||
├── lib/ # Utilities and configs
|
||||
│ ├── api/ # API clients
|
||||
│ ├── utils/ # Helper functions
|
||||
│ └── constants/ # Constants
|
||||
├── types/ # TypeScript types
|
||||
└── styles/ # Global styles
|
||||
```
|
||||
|
||||
### File Naming
|
||||
|
||||
```
|
||||
components/Button.tsx # PascalCase for components
|
||||
hooks/useAuth.ts # camelCase with 'use' prefix
|
||||
lib/formatDate.ts # camelCase for utilities
|
||||
types/market.types.ts # camelCase with .types suffix
|
||||
```
|
||||
|
||||
## Comments & Documentation
|
||||
|
||||
### When to Comment
|
||||
|
||||
```typescript
|
||||
// PASS: GOOD: Explain WHY, not WHAT
|
||||
// Use exponential backoff to avoid overwhelming the API during outages
|
||||
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000)
|
||||
|
||||
// Deliberately using mutation here for performance with large arrays
|
||||
items.push(newItem)
|
||||
|
||||
// FAIL: BAD: Stating the obvious
|
||||
// Increment counter by 1
|
||||
count++
|
||||
|
||||
// Set name to user's name
|
||||
name = user.name
|
||||
```
|
||||
|
||||
### JSDoc for Public APIs
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Searches markets using semantic similarity.
|
||||
*
|
||||
* @param query - Natural language search query
|
||||
* @param limit - Maximum number of results (default: 10)
|
||||
* @returns Array of markets sorted by similarity score
|
||||
* @throws {Error} If OpenAI API fails or Redis unavailable
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const results = await searchMarkets('election', 5)
|
||||
* console.log(results[0].name) // "Trump vs Biden"
|
||||
* ```
|
||||
*/
|
||||
export async function searchMarkets(
|
||||
query: string,
|
||||
limit: number = 10
|
||||
): Promise<Market[]> {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Best Practices
|
||||
|
||||
### Memoization
|
||||
|
||||
```typescript
|
||||
import { useMemo, useCallback } from 'react'
|
||||
|
||||
// PASS: GOOD: Memoize expensive computations
|
||||
// Copy before sorting - Array.prototype.sort mutates in place
|
||||
const sortedMarkets = useMemo(() => {
|
||||
return [...markets].sort((a, b) => b.volume - a.volume)
|
||||
}, [markets])
|
||||
|
||||
// PASS: GOOD: Memoize callbacks
|
||||
const handleSearch = useCallback((query: string) => {
|
||||
setSearchQuery(query)
|
||||
}, [])
|
||||
```
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```typescript
|
||||
import { lazy, Suspense } from 'react'
|
||||
|
||||
// PASS: GOOD: Lazy load heavy components
|
||||
const HeavyChart = lazy(() => import('./HeavyChart'))
|
||||
|
||||
export function Dashboard() {
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<HeavyChart />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Database Queries
|
||||
|
||||
```typescript
|
||||
// PASS: GOOD: Select only needed columns
|
||||
const { data } = await supabase
|
||||
.from('markets')
|
||||
.select('id, name, status')
|
||||
.limit(10)
|
||||
|
||||
// FAIL: BAD: Select everything
|
||||
const { data } = await supabase
|
||||
.from('markets')
|
||||
.select('*')
|
||||
```
|
||||
|
||||
## Testing Standards
|
||||
|
||||
### Test Structure (AAA Pattern)
|
||||
|
||||
```typescript
|
||||
test('calculates similarity correctly', () => {
|
||||
// Arrange
|
||||
const vector1 = [1, 0, 0]
|
||||
const vector2 = [0, 1, 0]
|
||||
|
||||
// Act
|
||||
const similarity = calculateCosineSimilarity(vector1, vector2)
|
||||
|
||||
// Assert
|
||||
expect(similarity).toBe(0)
|
||||
})
|
||||
```
|
||||
|
||||
### Test Naming
|
||||
|
||||
```typescript
|
||||
// PASS: GOOD: Descriptive test names
|
||||
test('returns empty array when no markets match query', () => { })
|
||||
test('throws error when OpenAI API key is missing', () => { })
|
||||
test('falls back to substring search when Redis unavailable', () => { })
|
||||
|
||||
// FAIL: BAD: Vague test names
|
||||
test('works', () => { })
|
||||
test('test search', () => { })
|
||||
```
|
||||
|
||||
## Code Smell Detection
|
||||
|
||||
Watch for these anti-patterns:
|
||||
|
||||
### 1. Long Functions
|
||||
```typescript
|
||||
// FAIL: BAD: Function > 50 lines
|
||||
function processMarketData() {
|
||||
// 100 lines of code
|
||||
}
|
||||
|
||||
// PASS: GOOD: Split into smaller functions
|
||||
function processMarketData() {
|
||||
const validated = validateData()
|
||||
const transformed = transformData(validated)
|
||||
return saveData(transformed)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Deep Nesting
|
||||
```typescript
|
||||
// FAIL: BAD: 5+ levels of nesting
|
||||
if (user) {
|
||||
if (user.isAdmin) {
|
||||
if (market) {
|
||||
if (market.isActive) {
|
||||
if (hasPermission) {
|
||||
// Do something
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PASS: GOOD: Early returns
|
||||
if (!user) return
|
||||
if (!user.isAdmin) return
|
||||
if (!market) return
|
||||
if (!market.isActive) return
|
||||
if (!hasPermission) return
|
||||
|
||||
// Do something
|
||||
```
|
||||
|
||||
### 3. Magic Numbers
|
||||
```typescript
|
||||
// FAIL: BAD: Unexplained numbers
|
||||
if (retryCount > 3) { }
|
||||
setTimeout(callback, 500)
|
||||
|
||||
// PASS: GOOD: Named constants
|
||||
const MAX_RETRIES = 3
|
||||
const DEBOUNCE_DELAY_MS = 500
|
||||
|
||||
if (retryCount > MAX_RETRIES) { }
|
||||
setTimeout(callback, DEBOUNCE_DELAY_MS)
|
||||
```
|
||||
|
||||
**Remember**: Code quality is not negotiable. Clear, maintainable code enables rapid development and confident refactoring.
|
||||
@@ -0,0 +1,300 @@
|
||||
---
|
||||
name: compose-multiplatform-patterns
|
||||
description: Compose Multiplatform and Jetpack Compose patterns for KMP projects — state management, navigation, theming, performance, and platform-specific UI. Use when building Compose or Jetpack Compose UI, state, navigation, or theming in a KMP project.
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Compose Multiplatform Patterns
|
||||
|
||||
Patterns for building shared UI across Android, iOS, Desktop, and Web using Compose Multiplatform and Jetpack Compose. Covers state management, navigation, theming, and performance.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Building Compose UI (Jetpack Compose or Compose Multiplatform)
|
||||
- Managing UI state with ViewModels and Compose state
|
||||
- Implementing navigation in KMP or Android projects
|
||||
- Designing reusable composables and design systems
|
||||
- Optimizing recomposition and rendering performance
|
||||
|
||||
## State Management
|
||||
|
||||
### ViewModel + Single State Object
|
||||
|
||||
Use a single data class for screen state. Expose it as `StateFlow` and collect in Compose:
|
||||
|
||||
```kotlin
|
||||
data class ItemListState(
|
||||
val items: List<Item> = emptyList(),
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val searchQuery: String = ""
|
||||
)
|
||||
|
||||
class ItemListViewModel(
|
||||
private val getItems: GetItemsUseCase
|
||||
) : ViewModel() {
|
||||
private val _state = MutableStateFlow(ItemListState())
|
||||
val state: StateFlow<ItemListState> = _state.asStateFlow()
|
||||
|
||||
fun onSearch(query: String) {
|
||||
_state.update { it.copy(searchQuery = query) }
|
||||
loadItems(query)
|
||||
}
|
||||
|
||||
private fun loadItems(query: String) {
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true) }
|
||||
getItems(query).fold(
|
||||
onSuccess = { items -> _state.update { it.copy(items = items, isLoading = false) } },
|
||||
onFailure = { e -> _state.update { it.copy(error = e.message, isLoading = false) } }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Collecting State in Compose
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun ItemListScreen(viewModel: ItemListViewModel = koinViewModel()) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
ItemListContent(
|
||||
state = state,
|
||||
onSearch = viewModel::onSearch
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ItemListContent(
|
||||
state: ItemListState,
|
||||
onSearch: (String) -> Unit
|
||||
) {
|
||||
// Stateless composable — easy to preview and test
|
||||
}
|
||||
```
|
||||
|
||||
### Event Sink Pattern
|
||||
|
||||
For complex screens, use a sealed interface for events instead of multiple callback lambdas:
|
||||
|
||||
```kotlin
|
||||
sealed interface ItemListEvent {
|
||||
data class Search(val query: String) : ItemListEvent
|
||||
data class Delete(val itemId: String) : ItemListEvent
|
||||
data object Refresh : ItemListEvent
|
||||
}
|
||||
|
||||
// In ViewModel
|
||||
fun onEvent(event: ItemListEvent) {
|
||||
when (event) {
|
||||
is ItemListEvent.Search -> onSearch(event.query)
|
||||
is ItemListEvent.Delete -> deleteItem(event.itemId)
|
||||
is ItemListEvent.Refresh -> loadItems(_state.value.searchQuery)
|
||||
}
|
||||
}
|
||||
|
||||
// In Composable — single lambda instead of many
|
||||
ItemListContent(
|
||||
state = state,
|
||||
onEvent = viewModel::onEvent
|
||||
)
|
||||
```
|
||||
|
||||
## Navigation
|
||||
|
||||
### Type-Safe Navigation (Compose Navigation 2.8+)
|
||||
|
||||
Define routes as `@Serializable` objects:
|
||||
|
||||
```kotlin
|
||||
@Serializable data object HomeRoute
|
||||
@Serializable data class DetailRoute(val id: String)
|
||||
@Serializable data object SettingsRoute
|
||||
|
||||
@Composable
|
||||
fun AppNavHost(navController: NavHostController = rememberNavController()) {
|
||||
NavHost(navController, startDestination = HomeRoute) {
|
||||
composable<HomeRoute> {
|
||||
HomeScreen(onNavigateToDetail = { id -> navController.navigate(DetailRoute(id)) })
|
||||
}
|
||||
composable<DetailRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<DetailRoute>()
|
||||
DetailScreen(id = route.id)
|
||||
}
|
||||
composable<SettingsRoute> { SettingsScreen() }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dialog and Bottom Sheet Navigation
|
||||
|
||||
Use `dialog()` and overlay patterns instead of imperative show/hide:
|
||||
|
||||
```kotlin
|
||||
NavHost(navController, startDestination = HomeRoute) {
|
||||
composable<HomeRoute> { /* ... */ }
|
||||
dialog<ConfirmDeleteRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<ConfirmDeleteRoute>()
|
||||
ConfirmDeleteDialog(
|
||||
itemId = route.itemId,
|
||||
onConfirm = { navController.popBackStack() },
|
||||
onDismiss = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Composable Design
|
||||
|
||||
### Slot-Based APIs
|
||||
|
||||
Design composables with slot parameters for flexibility:
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun AppCard(
|
||||
modifier: Modifier = Modifier,
|
||||
header: @Composable () -> Unit = {},
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
actions: @Composable RowScope.() -> Unit = {}
|
||||
) {
|
||||
Card(modifier = modifier) {
|
||||
Column {
|
||||
header()
|
||||
Column(content = content)
|
||||
Row(horizontalArrangement = Arrangement.End, content = actions)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Modifier Ordering
|
||||
|
||||
Modifier order matters — apply in this sequence:
|
||||
|
||||
```kotlin
|
||||
Text(
|
||||
text = "Hello",
|
||||
modifier = Modifier
|
||||
.padding(16.dp) // 1. Layout (padding, size)
|
||||
.clip(RoundedCornerShape(8.dp)) // 2. Shape
|
||||
.background(Color.White) // 3. Drawing (background, border)
|
||||
.clickable { } // 4. Interaction
|
||||
)
|
||||
```
|
||||
|
||||
## KMP Platform-Specific UI
|
||||
|
||||
### expect/actual for Platform Composables
|
||||
|
||||
```kotlin
|
||||
// commonMain
|
||||
@Composable
|
||||
expect fun PlatformStatusBar(darkIcons: Boolean)
|
||||
|
||||
// androidMain
|
||||
@Composable
|
||||
actual fun PlatformStatusBar(darkIcons: Boolean) {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
SideEffect { systemUiController.setStatusBarColor(Color.Transparent, darkIcons) }
|
||||
}
|
||||
|
||||
// iosMain
|
||||
@Composable
|
||||
actual fun PlatformStatusBar(darkIcons: Boolean) {
|
||||
// iOS handles this via UIKit interop or Info.plist
|
||||
}
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
### Stable Types for Skippable Recomposition
|
||||
|
||||
Mark classes as `@Stable` or `@Immutable` when all properties are stable:
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
data class ItemUiModel(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val progress: Float
|
||||
)
|
||||
```
|
||||
|
||||
### Use `key()` and Lazy Lists Correctly
|
||||
|
||||
```kotlin
|
||||
LazyColumn {
|
||||
items(
|
||||
items = items,
|
||||
key = { it.id } // Stable keys enable item reuse and animations
|
||||
) { item ->
|
||||
ItemRow(item = item)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Defer Reads with `derivedStateOf`
|
||||
|
||||
```kotlin
|
||||
val listState = rememberLazyListState()
|
||||
val showScrollToTop by remember {
|
||||
derivedStateOf { listState.firstVisibleItemIndex > 5 }
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid Allocations in Recomposition
|
||||
|
||||
```kotlin
|
||||
// BAD — new lambda and list every recomposition
|
||||
items.filter { it.isActive }.forEach { ActiveItem(it, onClick = { handle(it) }) }
|
||||
|
||||
// GOOD — key each item so callbacks stay attached to the right row
|
||||
val activeItems = remember(items) { items.filter { it.isActive } }
|
||||
activeItems.forEach { item ->
|
||||
key(item.id) {
|
||||
ActiveItem(item, onClick = { handle(item) })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Theming
|
||||
|
||||
### Material 3 Dynamic Theming
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun AppTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
if (darkTheme) dynamicDarkColorScheme(LocalContext.current)
|
||||
else dynamicLightColorScheme(LocalContext.current)
|
||||
}
|
||||
darkTheme -> darkColorScheme()
|
||||
else -> lightColorScheme()
|
||||
}
|
||||
|
||||
MaterialTheme(colorScheme = colorScheme, content = content)
|
||||
}
|
||||
```
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- Using `mutableStateOf` in ViewModels when `MutableStateFlow` with `collectAsStateWithLifecycle` is safer for lifecycle
|
||||
- Passing `NavController` deep into composables — pass lambda callbacks instead
|
||||
- Heavy computation inside `@Composable` functions — move to ViewModel or `remember {}`
|
||||
- Using `LaunchedEffect(Unit)` as a substitute for ViewModel init — it re-runs on configuration change in some setups
|
||||
- Creating new object instances in composable parameters — causes unnecessary recomposition
|
||||
|
||||
## References
|
||||
|
||||
See skill: `android-clean-architecture` for module structure and layering.
|
||||
See skill: `kotlin-coroutines-flows` for coroutine and Flow patterns.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user