feat: release 3 new skills: configuring-code-analyzer, managing-cdc-enablement, using-salesforce-archive @W-23038011@

This commit is contained in:
GitHub Action 2026-06-19 11:16:46 +00:00
parent d289e9ef79
commit 496599720c
29 changed files with 4954 additions and 277 deletions

View File

@ -1,6 +1,6 @@
---
name: building-sf-integrations
description: "Salesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data)."
description: "Salesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), data import/export (use handling-sf-data), or CDC channel-membership metadata such as PlatformEventChannel, PlatformEventChannelMember, or EnrichedField (use managing-cdc-enablement)."
metadata:
version: "1.1"
---

View File

@ -0,0 +1,482 @@
---
name: configuring-code-analyzer
description: "Set up, configure, and troubleshoot Salesforce Code Analyzer for any project. Handles installation, prerequisite checks, diagnosing broken setups, creating and editing code-analyzer.yml overrides, engine-specific settings, ignore patterns, severity overrides, and CI/CD pipeline setup. TRIGGER when: user says 'set up code analyzer', 'configure code analyzer', 'install code analyzer', 'code analyzer not working', 'fix my setup', 'scan is failing', 'check my setup', 'is code analyzer installed', 'enable/disable engine', 'exclude files', 'change severity', 'set up GitHub Actions', 'set up CI/CD', 'add code analyzer to pipeline', 'make pipeline fail', 'update my workflow', 'quality gate', 'fail on violations', 'scan changed files only', 'add SARIF', 'code-analyzer.yml', 'ESLint config', 'increase SFGE memory', or reports errors running Code Analyzer. DO NOT TRIGGER when: user wants to run a scan (use running-code-analyzer), fix violations, explain rules, create custom rules, or suppress violations."
metadata:
version: "1.0"
argument-hint: "[--check-prerequisites] [--generate-config] [--engine pmd|eslint|cpd|retire-js|regex|flow|sfge|apexguru] [--ci github-actions|jenkins]"
---
# Configuring Code Analyzer Skill
## Overview
This skill manages the `code-analyzer.yml` configuration file — the single source of truth for how Code Analyzer behaves in a project. All customization (engines, rules, ignores, suppressions) is done by creating or editing this file. If the file doesn't exist, this skill creates it in the current working directory.
---
## Scope
**In scope:**
- Checking prerequisites (sf CLI, Java, Node.js, Python, org auth)
- Installing/updating the Code Analyzer plugin
- Creating `code-analyzer.yml` if it doesn't exist
- Editing `code-analyzer.yml` for all configuration changes
- Engine settings, rule overrides, ignore patterns, suppressions
- CI/CD pipeline setup (GitHub Actions, Jenkins, etc.)
- Environment validation and troubleshooting
**Out of scope:**
- Running scans (use `running-code-analyzer` skill)
- Fixing violations, explaining rules, creating custom rules, suppression management
---
## Tool Usage Rules
**Allowed:** Bash (sf, java, node, python3, git, npm), Read, Write, Edit
**Forbidden:** MCP tools, Agent tool, Web tools, other skills, `which`, `find`, `locate`, searching for binaries
---
## Core Principle: YAML Only When Customizing
Code Analyzer works out of the box with NO config file — all defaults are built into the tool. The `code-analyzer.yml` file is ONLY created when the user explicitly requests a customization.
**Rules:**
- **Do NOT create `code-analyzer.yml` proactively** — only when user asks to change something
- **Do NOT duplicate built-in defaults** — only write entries that intentionally override behavior
- **Always place at project root** — where `sfdx-project.json` or `sf-project.json` lives
- **The CLI auto-discovers it**`sf code-analyzer run` from project root automatically picks up `code-analyzer.yml` in that directory. No `--config-file` flag needed.
- User says "configure code analyzer" with no specifics? → **Ask what they want to customize**. Don't create an empty or boilerplate file.
**Workflow:**
1. User requests a customization (e.g., "disable PMD", "ignore test files", "increase SFGE memory")
2. Check if `code-analyzer.yml` exists at project root
3. If NO → create it at project root with ONLY the requested override
4. If YES → read it, then edit in the requested change
5. Validate with `sf code-analyzer config`
---
## Step 1: Understand Intent and Map to Config Sections
The user can request ANY combination of configuration changes in natural language. Your job is to:
1. **Parse what they want** — may be one thing or many things combined
2. **Map each request to the correct section(s) of `code-analyzer.yml`**
3. **Create the file if it doesn't exist, then apply all changes**
### The `code-analyzer.yml` Structure (what you can write/edit)
```yaml
config_root: . # Root for relative path resolution
log_folder: <path> # Where logs are written
log_level: <1-5> # 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Fine
ignores: # Files/folders excluded from scanning
files: [<glob patterns>]
engines: # Per-engine settings
<engine_name>:
disable_engine: <bool>
<engine_specific_keys>: ...
rules: # Per-rule overrides
<engine_name>:
<rule_name>:
severity: <1-5>
tags: [<strings>]
disabled: <bool>
suppressions: # Bulk suppression configuration
disable_suppressions: <bool>
"<file_or_folder_path>":
- rule_selector: "<selector>"
max_suppressed_violations: <number|null>
reason: "<why>"
```
### Mapping Principle
Any user request maps to one or more sections above. Parse the intent and edit the right section(s):
| Intent Category | Maps To | Examples of What User Might Say |
|----------------|---------|-------------------------------|
| Setup / Install | Step 2 (prerequisites + install) | "set up", "install", "get started", "new laptop", "from scratch" |
| **Diagnose / Fix** | **Step 2A (systematic debug)** | **"not working", "broken", "fix my setup", "scan fails", "getting errors"** |
| Engine control | `engines.<name>.disable_engine` | "disable X", "turn off Y", "only use Z", "enable all" |
| Engine tuning | `engines.<name>.<property>` | "increase memory", "change heap", "use my eslint config", "set tokens to 50" |
| File exclusions | `ignores.files` | "exclude", "ignore", "skip", "don't scan X" |
| Rule severity | `rules.<engine>.<rule>.severity` | "make X critical", "promote", "demote", "change severity" |
| Rule disable | `rules.<engine>.<rule>.disabled` | "disable rule X", "turn off Y rule", "remove Z" |
| Rule tags | `rules.<engine>.<rule>.tags` | "tag X as security", "add recommended tag" |
| Suppressions | `suppressions` section | "suppress X in folder Y", "allow N violations" |
| CI/CD | Generate pipeline file (separate from config) | "github actions", "CI", "quality gate" |
| View/inspect | Read file + `sf code-analyzer config` | "show config", "what's configured", "current settings" |
### File Existence Decision
**BEFORE editing anything**, check if `code-analyzer.yml` exists at project root:
```bash
ls code-analyzer.yml code-analyzer.yaml 2>/dev/null
```
- **File does NOT exist** → Create it at project root with ONLY the user's requested override(s)
- **File exists** → Read it, then Edit to add/modify the requested section(s)
The CLI auto-discovers `code-analyzer.yml` in the current directory. Since scans run from project root, the file must live there.
### ⚠️ Rule Name Resolution — ALWAYS Before Writing YAML
When a user references rules by partial, descriptive, or approximate names (e.g., "the doc rule", "CRUD violation", "console rule", "hardcoded values"), you MUST resolve to exact rule names using the lookup in **Step 6.1** BEFORE writing any YAML. The `code-analyzer.yml` file silently ignores rule names that don't exactly match — there is no error, the override just won't apply.
**Examples of fuzzy → exact resolution needed:**
- "Disable the ApexDoc rule" → lookup confirms `ApexDoc` (engine: `pmd`)
- "Demote no-console to low" → lookup confirms `no-console` (engine: `eslint`)
- "Make CRUD violations critical" → lookup confirms `ApexCRUDViolation` (engine: `pmd`)
- "Turn off the hardcoded values check" → lookup finds `@salesforce-ux/slds/no-hardcoded-values-slds2` (engine: `eslint`)
- "Disable the injection rule" → multiple matches possible → ask user which one
**Only skip the lookup** when the user provides an unambiguous, exact, well-known name (e.g., "ApexDoc", "no-console", "no-unused-vars").
### Handling Combined/Complex Requests
Users will often combine multiple changes in one request. Handle ALL of them in a single edit:
- "Disable PMD's ApexDoc rule and make CRUD violations critical" → edit two entries under `rules.pmd`
- "Exclude test files and vendor code, and increase SFGE memory" → edit `ignores.files` + `engines.sfge.java_max_heap_size`
- "Set up code analyzer with only ESLint and PMD, ignore node_modules" → create file with `engines` (disable others) + `ignores`
- "Make all security rules severity 1" → look up rules via `sf code-analyzer rules --rule-selector Security`, then override each
- "Configure code analyzer" (no specifics) → ask user what they want to customize before creating any file
### Quick Reference: Common Requests → Config Output
| User Says | Resulting YAML |
|-----------|---------------|
| "configure code analyzer" | Ask user what to customize — don't create file until there's an actual override |
| "disable the ApexDoc rule" | `rules: pmd: ApexDoc: disabled: true` |
| "only scan Apex, no JavaScript" | `engines: eslint: disable_engine: true` + `engines: retire-js: disable_engine: true` |
| "ignore all test files" | `ignores: files: ["**/test/**", "**/__tests__/**", "**/*.test.js"]` |
| "make security rules critical" | Look up rules, then `rules: <engine>: <rule>: severity: 1` for each |
| "increase SFGE memory to 8g" | `engines: sfge: java_max_heap_size: "8g"` |
| "use my project's ESLint config" | `engines: eslint: auto_discover_eslint_config: true` |
| "suppress CRUD violations in legacy folder" | `suppressions: "force-app/legacy/": [{rule_selector: "pmd:ApexCRUDViolation", reason: "..."}]` |
**The AI must understand the YAML schema and write valid config for ANY request, not just the examples above.**
---
## Step 2: Check Prerequisites and Install
Run `bash "<skill_dir>/scripts/check-prerequisites.sh"` or check manually:
```bash
sf --version 2>&1 # sf CLI
sf plugins --core 2>&1 | grep -i "code-analyzer" # Plugin
java -version 2>&1 # Java 11+ (PMD, CPD, SFGE)
node --version 2>&1 # Node 18+ (ESLint, RetireJS)
python3 --version 2>&1 # Python 3 (Flow engine)
```
If anything is missing, install it (**always ask user first**):
```bash
npm install -g @salesforce/cli # sf CLI
sf plugins install @salesforce/plugin-code-analyzer # Code Analyzer plugin
```
For Java/Node/Python installs, read `<skill_dir>/references/engine-prerequisites.md`.
If install fails, read `<skill_dir>/references/troubleshooting.md`.
---
## Step 2A: Diagnose and Fix a Broken Setup
**TRIGGER:** User says "not working", "broken", "getting errors", "scan fails", "help me fix", etc.
**Read `<skill_dir>/references/diagnostic-flow.md`** for the complete layered diagnostic procedure, fix table, and anti-patterns.
**Key principles (always apply):**
- Never search for binaries (`which`, `find`, `ls /opt/homebrew/bin/`)
- Never use `sfdx` as a workaround — only `sf`
- Fix layer by layer: CLI → Plugin → Engine deps → verify scan
- Give user ONE command at a time, wait for confirmation before continuing
- After fix succeeds, proceed to run the full scan automatically
---
## Step 3: Create or Edit `code-analyzer.yml`
**Only triggered when user requests a customization.** Never create proactively.
### Creating (file doesn't exist)
Choose **one** of the two approaches below — do not run both:
**Option A — Auto-generate from project type (recommended for first-time setup):**
Run `bash "<skill_dir>/scripts/generate-config.sh"`. This detects Apex, LWC, and Flow markers and produces a minimal `code-analyzer.yml` suited to the project. Skip to the "After any create/edit, validate" section.
> Note: The script exits with an error if `code-analyzer.yml` already exists. Delete the existing file first if you need to regenerate.
**Option B — Write manually (when the user has specific customizations in mind):**
Read the appropriate example config as a reference for structure:
- For Apex-only projects, read `<skill_dir>/examples/apex-project-config.yml`
- For LWC-only projects, read `<skill_dir>/examples/lwc-project-config.yml`
- For full-stack (Apex + LWC + Flows), read `<skill_dir>/examples/fullstack-project-config.yml`
Write the file at project root using the Write tool. Include ONLY the user's requested changes:
```bash
# Example: user said "ignore test files and increase SFGE memory"
# → Write to project root (where sfdx-project.json lives):
```
```yaml
ignores:
files:
- "**/test/**"
- "**/__tests__/**"
engines:
sfge:
java_max_heap_size: "4g"
```
Do NOT add `config_root`, `log_folder`, or any other field the user didn't ask for.
### Editing (file already exists)
Read the file, then use the Edit tool to add/modify only the relevant section. Preserve everything else.
### After any create/edit, validate:
Run `bash "<skill_dir>/scripts/validate-config.sh"` to validate YAML syntax and schema correctness, or use the CLI directly:
```bash
sf code-analyzer config
```
(No `--config-file` needed — the CLI auto-discovers `code-analyzer.yml` in CWD.)
### If user says "configure code analyzer" with no specifics
Ask: "What would you like to customize? For example: ignore certain files, change rule severities, tune engine settings, or disable engines you don't need."
---
## Step 4: Enable/Disable Engines
Edit the `engines` section in `code-analyzer.yml`:
```yaml
engines:
pmd:
disable_engine: true # Disable PMD
eslint:
disable_engine: false # Enable ESLint (default)
```
Valid engine names: `pmd`, `cpd`, `eslint`, `regex`, `retire-js`, `flow`, `sfge`, `apexguru`
**Always validate after editing:**
```bash
sf code-analyzer config --config-file code-analyzer.yml
```
---
## Step 5: Ignore Patterns
Edit the `ignores` section in `code-analyzer.yml`:
```yaml
ignores:
files:
- "**/node_modules/**"
- "**/.sfdx/**"
- "**/.sf/**"
- "**/vendor/**"
- "**/*.min.js"
```
Common patterns:
| Pattern | Excludes |
|---------|----------|
| `**/node_modules/**` | npm dependencies |
| `**/.sfdx/**`, `**/.sf/**` | SF CLI internals |
| `**/test/**`, `**/__tests__/**` | Test directories |
| `**/*.test.js`, `**/*.spec.js` | Test files |
| `**/jest-mocks/**` | Jest mocks |
| `**/vendor/**`, `**/*.min.js` | Third-party/minified |
| `**/staticresources/**` | Static resources |
---
## Step 6: Rule Overrides
Edit the `rules` section in `code-analyzer.yml`. Each rule can have `severity`, `tags`, and `disabled` overrides:
```yaml
rules:
pmd:
ApexCRUDViolation:
severity: 1 # Promote to Critical
AvoidGlobalModifier:
disabled: true # Turn off entirely
ApexDoc:
severity: 5 # Demote to Info
tags: ["Documentation"]
eslint:
no-console:
severity: 4 # Demote to Low
no-unused-vars:
severity: 2 # Promote to High
```
**Severity values:** `1`/Critical, `2`/High, `3`/Moderate, `4`/Low, `5`/Info
### 6.1 Rule Name Resolution (Fuzzy Matching)
**⚠️ CRITICAL:** A misspelled or partial rule name in `code-analyzer.yml` is SILENTLY IGNORED — no error, the override just won't apply.
**When users reference rules by approximate names** (e.g., "the doc rule", "CRUD violation", "hardcoded values"), resolve to exact names BEFORE writing YAML:
```bash
sf code-analyzer rules --rule-selector all 2>&1 | grep -i "<USER_KEYWORD>"
```
- **1 match** → use that exact name + its engine for the YAML path
- **Multiple matches** → ask user which one they meant
- **0 matches** → try broader keywords or inform user
**Skip the lookup only** when the name is unambiguous and exact (e.g., "ApexDoc", "no-console", "no-unused-vars").
**For detailed matching strategies, common fuzzy→exact mappings, and engine identification:** Read `<skill_dir>/references/rule-name-resolution.md`.
---
## Step 7: Engine-Specific Settings
Edit the `engines` section. Most common overrides:
```yaml
engines:
sfge:
java_max_heap_size: "4g" # <200 classes"2g", 200-500"4g", 500+"6g"/"8g"
java_thread_count: 4
java_thread_timeout: 900000
eslint:
auto_discover_eslint_config: true # Use project's own ESLint config
eslint_config_file: "./eslint.config.mjs"
pmd:
custom_rulesets: ["./config/custom-pmd-rules.xml"]
java_classpath_entries: ["./lib/custom-rules.jar"]
cpd:
minimum_tokens: { apex: 100, javascript: 100 }
apexguru:
target_org: "my-org-alias"
flow:
python_command: "python3"
regex:
custom_rules:
NoHardcodedIds:
regex: "/[a-zA-Z0-9]{15,18}/"
file_extensions: [".cls", ".trigger"]
description: "Detects hardcoded Salesforce record IDs"
severity: 2
tags: ["Security"]
```
For full property list per engine, read `<skill_dir>/references/config-schema.md`.
---
## Step 8: CI/CD Pipeline Setup
Detect CI system from workspace (`.github/workflows/` → GitHub Actions, `Jenkinsfile` → Jenkins, etc.). Read `<skill_dir>/references/ci-cd-templates.md` for templates. Use `<skill_dir>/examples/ci-github-actions.yml` as GitHub Actions base. Key flags: `--severity-threshold 2` (gate), `--output-file results.sarif` (GitHub scanning), `--config-file code-analyzer.yml`.
---
## Step 9: View Current Configuration
```bash
sf code-analyzer config # Show effective config
sf code-analyzer config --rule-selector pmd:Security # Specific rules
sf code-analyzer config --include-unmodified-rules # All defaults
```
---
## Cross-Skill Integration
This skill works together with `running-code-analyzer`. The AI agent should seamlessly hand off between them:
### When `running-code-analyzer` delegates HERE:
If a user says "scan my code" / "run code analyzer" but it fails (CLI missing, plugin not installed, or scan errors out), `running-code-analyzer` delegates to this skill. In that case:
1. Run the **diagnose and fix** flow (Step 2A) — find what's broken, fix it
2. After everything works, **automatically proceed to run the scan** — do not stop and ask. The user's original intent was to scan.
3. Hand execution back to `running-code-analyzer` behavior (build command, execute, parse results).
### When THIS skill hands off to `running-code-analyzer`:
After any successful configuration action, offer to run a scan (e.g., "Setup complete! Want me to run a scan?", "Config updated — want to scan and verify?"). If user says yes, proceed with `running-code-analyzer` behavior.
### When user intent spans BOTH skills:
Handle end-to-end: "not working" → Diagnose → Fix → Scan. "Set up and scan" → Install → Scan. "Disable ESLint and scan Apex" → Edit config → Run with `--rule-selector pmd`. Always follow through to the user's final intent.
---
## Rules / Constraints
| Constraint | Rationale |
|-----------|-----------|
| Only create YAML when user requests a customization | Defaults work without any file — don't create boilerplate |
| Place YAML at project root only | CLI auto-discovers `code-analyzer.yml` from CWD |
| Write only overrides, never duplicate defaults | Keep file minimal and intentional |
| Use Write tool to create, Edit tool to modify | Preserves existing settings |
| Validate after every change | `sf code-analyzer config` catches YAML errors |
| Ask before installing prerequisites | Never auto-install without consent |
| Never delete existing config without asking | User may have custom settings |
| After setup, offer to scan | Close the loop — config without scan is incomplete |
---
## Gotchas
| Issue | Solution |
|-------|----------|
| Config not picked up | Must be `code-analyzer.yml` in CWD or use `--config-file` |
| YAML validation fails | Spaces only (no tabs), check colon spacing |
| SFGE out of memory | Increase `java_max_heap_size` in engines section |
| ESLint rules missing | Set `auto_discover_eslint_config: true` |
For full troubleshooting, read `<skill_dir>/references/troubleshooting.md`.
---
## Reference File Index
`<skill_dir>` is the absolute path to the directory containing this SKILL.md file.
| File | Purpose |
|------|---------|
| `<skill_dir>/scripts/check-prerequisites.sh` | Environment check |
| `<skill_dir>/scripts/generate-config.sh` | Auto-detect project type and generate config |
| `<skill_dir>/scripts/validate-config.sh` | Validate YAML after changes |
| `<skill_dir>/references/config-schema.md` | Full YAML schema documentation |
| `<skill_dir>/references/diagnostic-flow.md` | Step 2A: layered diagnostic procedure and fix table |
| `<skill_dir>/references/rule-name-resolution.md` | Step 6.1: fuzzy rule name lookup strategies and mappings |
| `<skill_dir>/references/engine-prerequisites.md` | Install instructions per engine |
| `<skill_dir>/references/ci-cd-templates.md` | CI/CD pipeline templates |
| `<skill_dir>/references/troubleshooting.md` | Common setup issues and fixes |
| `<skill_dir>/examples/apex-project-config.yml` | Config for Apex-only project |
| `<skill_dir>/examples/lwc-project-config.yml` | Config for LWC-only project |
| `<skill_dir>/examples/fullstack-project-config.yml` | Config for Apex + LWC + Flows |
| `<skill_dir>/examples/ci-github-actions.yml` | GitHub Actions workflow |

View File

@ -0,0 +1,41 @@
# Code Analyzer Configuration — Apex-focused Project
#
# This file contains ONLY overrides. Code Analyzer's built-in defaults
# handle everything else. Only add entries here that intentionally
# change behavior for your project.
#
# Usage:
# Place at project root as code-analyzer.yml
# Validate: sf code-analyzer config --config-file code-analyzer.yml
# Exclude non-project files (these would otherwise be scanned)
ignores:
files:
- "**/node_modules/**"
- "**/.sfdx/**"
- "**/.sf/**"
- "**/staticresources/**"
# Engine tuning — only override what differs from defaults
engines:
# SFGE: increase heap for projects with 200+ Apex classes
# (built-in default is dynamic/1g, too small for large projects)
sfge:
java_max_heap_size: "4g"
# Rule overrides — promote security, demote noise
rules:
pmd:
# Promote security rules to Critical (default is High)
ApexCRUDViolation:
severity: 1
ApexSOQLInjection:
severity: 1
ApexSharingViolations:
severity: 1
# Demote noisy rules (default is Moderate)
ApexDoc:
severity: 5
# Disable rules that conflict with project conventions
AvoidGlobalModifier:
disabled: true

View File

@ -0,0 +1,96 @@
# GitHub Actions Workflow — Code Analyzer Quality Gate
#
# Scans pull requests for code quality and security violations.
# Uploads SARIF results to GitHub Code Scanning for inline annotations.
#
# Usage:
# Copy to .github/workflows/code-analyzer.yml in your repository
# Customize severity-threshold and rule-selector as needed
#
# Prerequisites:
# - Repository must have GitHub Advanced Security enabled for SARIF upload
# - Or remove the "Upload SARIF" step for basic artifact-only workflow
name: Code Analyzer
on:
pull_request:
branches: [main, develop]
paths:
- 'force-app/**'
- 'src/**'
- '**/*.cls'
- '**/*.trigger'
- '**/*.js'
- '**/*.ts'
- '**/*.html'
- '**/*.flow-meta.xml'
- 'code-analyzer.yml'
push:
branches: [main, develop]
permissions:
contents: read
security-events: write # Required for SARIF upload
jobs:
code-analysis:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for diff-based scans
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '11'
- name: Install Salesforce CLI
run: npm install -g @salesforce/cli
- name: Install Code Analyzer
run: sf plugins install @salesforce/plugin-code-analyzer
- name: Verify installation
run: |
sf --version
sf plugins --core | grep code-analyzer
java -version
node --version
- name: Run Code Analyzer
run: |
sf code-analyzer run \
--rule-selector Recommended \
--severity-threshold 2 \
--output-file results.sarif \
--output-file results.json \
--output-file results.html \
--config-file code-analyzer.yml
- name: Upload SARIF to GitHub Security
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
category: code-analyzer
- name: Upload Results as Artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: code-analyzer-results
path: |
results.json
results.html
retention-days: 30

View File

@ -0,0 +1,46 @@
# Code Analyzer Configuration — Full-Stack Salesforce Project
#
# This file contains ONLY overrides. Code Analyzer's built-in defaults
# handle everything else. Only add entries here that intentionally
# change behavior for your project.
#
# Usage:
# Place at project root as code-analyzer.yml
# Validate: sf code-analyzer config --config-file code-analyzer.yml
# Exclude non-project files
ignores:
files:
- "**/node_modules/**"
- "**/.sfdx/**"
- "**/.sf/**"
- "**/jest-mocks/**"
- "**/__tests__/**"
- "**/*.min.js"
- "**/staticresources/jquery*"
- "**/staticresources/bootstrap*"
# Engine tuning — only what differs from defaults
engines:
# SFGE: increase heap for large Apex codebases
sfge:
java_max_heap_size: "4g"
# ESLint: use project's existing config for LWC-specific rules
eslint:
auto_discover_eslint_config: true
# Rule overrides — only rules that need project-specific treatment
rules:
pmd:
# Promote security rules (default is High → make Critical)
ApexCRUDViolation:
severity: 1
ApexSOQLInjection:
severity: 1
# Demote noisy documentation rule
ApexDoc:
severity: 5
eslint:
# Demote console warnings in development (default is Moderate)
no-console:
severity: 4

View File

@ -0,0 +1,26 @@
# Code Analyzer Configuration — LWC-focused Project
#
# This file contains ONLY overrides. Code Analyzer's built-in defaults
# handle everything else. Only add entries here that intentionally
# change behavior for your project.
#
# Usage:
# Place at project root as code-analyzer.yml
# Validate: sf code-analyzer config --config-file code-analyzer.yml
# Exclude non-project files and test infrastructure
ignores:
files:
- "**/node_modules/**"
- "**/.sfdx/**"
- "**/.sf/**"
- "**/jest-mocks/**"
- "**/__tests__/**"
- "**/*.min.js"
# Engine tuning
engines:
# ESLint: opt-in to use project's existing ESLint config
# (built-in default is false — Code Analyzer uses its own base configs)
eslint:
auto_discover_eslint_config: true

View File

@ -0,0 +1,648 @@
# CI/CD Pipeline Templates
Ready-to-use templates for integrating Code Analyzer into CI/CD pipelines.
## GitHub Actions (using `forcedotcom/run-code-analyzer@v2`)
The official GitHub Action for Code Analyzer. It orchestrates the run, uploads artifacts, and optionally creates PR reviews with violation counts.
**Action inputs:**
| Input | Required | Default | Description |
|-------|----------|---------|-------------|
| `run-arguments` | No | `--view detail --output-file sfca_results.json` | Flags passed to `sf code-analyzer run` |
| `results-artifact-name` | No | `salesforce-code-analyzer-results` | Name of the uploaded ZIP artifact |
| `github-token` | No | *(none)* | Enables PR reviews + `*-in-changed-files` outputs |
**Action outputs:** `exit-code`, `num-violations`, `num-sev1-violations` through `num-sev5-violations`, `num-violations-in-changed-files`, `num-sev1-violations-in-changed-files` through `num-sev5-violations-in-changed-files`, `review-id`.
**Key `run-arguments` flags:**
| Flag | Purpose | Example |
|------|---------|---------|
| `--workspace` | Root directory to scan (scans all eligible files recursively) | `--workspace .` |
| `--target` | Specific files/directories to scan (comma-separated or repeated) | `--target force-app/main/default/classes --target force-app/main/default/triggers` |
| `--rule-selector` | Which rules to run | `Recommended`, `all`, `all:Security`, `pmd`, `"Severity:1,2"` |
| `--config-file` | Path to `code-analyzer.yml`**omit if no config file exists** | `--config-file code-analyzer.yml` |
| `--output-file` | Output file (repeat for multiple formats; format inferred from extension) | `--output-file results.html --output-file results.sarif` |
| `--view` | Output verbosity | `detail` or `summary` |
**`--workspace` vs `--target`:**
- Use `--workspace .` for full-repo scans (most common in CI)
- Use `--target <path>` to scan specific directories (monorepos, scoped scans, faster feedback)
- If both are omitted, defaults to current working directory
- You can combine: `--workspace . --target force-app/` scans only `force-app/` within the workspace
### Basic Quality Gate (Pull Requests)
```yaml
# .github/workflows/code-analyzer.yml
name: Salesforce Code Analyzer
on:
pull_request:
branches: [main, develop]
jobs:
code-analysis:
permissions:
pull-requests: write
contents: read
actions: read
runs-on: ubuntu-latest
steps:
- name: Check out files
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '>=20.9.0'
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '>=11'
- name: Install Salesforce CLI
run: npm install -g @salesforce/cli@latest
- name: Install Code Analyzer Plugin
run: sf plugins install code-analyzer@latest
- name: Run Salesforce Code Analyzer
id: run-code-analyzer
uses: forcedotcom/run-code-analyzer@v2
with:
run-arguments: --workspace . --rule-selector Recommended --output-file sfca_results.html --output-file sfca_results.json
results-artifact-name: code-analyzer-results
github-token: ${{ github.token }}
- name: Check Quality Gate (Changed Files Only)
if: |
steps.run-code-analyzer.outputs.num-sev1-violations-in-changed-files > 0 ||
steps.run-code-analyzer.outputs.num-sev2-violations-in-changed-files > 0
run: |
echo "Critical/High violations found in changed files!"
echo "Sev1: ${{ steps.run-code-analyzer.outputs.num-sev1-violations-in-changed-files }}"
echo "Sev2: ${{ steps.run-code-analyzer.outputs.num-sev2-violations-in-changed-files }}"
exit 1
```
### Strict Quality Gate (All Files + SARIF)
```yaml
# .github/workflows/code-analyzer-strict.yml
name: Salesforce Code Analyzer (Strict)
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
code-analysis:
permissions:
pull-requests: write
contents: read
actions: read
security-events: write
runs-on: ubuntu-latest
steps:
- name: Check out files
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '>=20.9.0'
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '>=11'
- name: Install Salesforce CLI
run: npm install -g @salesforce/cli@latest
- name: Install Code Analyzer Plugin
run: sf plugins install code-analyzer@latest
- name: Run Salesforce Code Analyzer
id: run-code-analyzer
uses: forcedotcom/run-code-analyzer@v2
with:
run-arguments: --workspace . --rule-selector Recommended --output-file sfca_results.html --output-file sfca_results.json --output-file sfca_results.sarif --config-file code-analyzer.yml
results-artifact-name: code-analyzer-results
github-token: ${{ github.token }}
- name: Upload SARIF to GitHub Security
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: sfca_results.sarif
- name: Check Quality Gate (All Files)
if: |
steps.run-code-analyzer.outputs.exit-code > 0 ||
steps.run-code-analyzer.outputs.num-sev1-violations > 0 ||
steps.run-code-analyzer.outputs.num-sev2-violations > 0
run: |
echo "Quality gate failed!"
echo "Total violations: ${{ steps.run-code-analyzer.outputs.num-violations }}"
echo "Sev1: ${{ steps.run-code-analyzer.outputs.num-sev1-violations }}"
echo "Sev2: ${{ steps.run-code-analyzer.outputs.num-sev2-violations }}"
exit 1
```
### Security-Focused (AppExchange Prep)
```yaml
# .github/workflows/code-analyzer-security.yml
name: Security Analysis
on:
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1' # Weekly Monday 6am
jobs:
security-scan:
permissions:
pull-requests: write
contents: read
actions: read
security-events: write
runs-on: ubuntu-latest
steps:
- name: Check out files
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '>=20.9.0'
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '>=11'
- name: Install Salesforce CLI
run: npm install -g @salesforce/cli@latest
- name: Install Code Analyzer Plugin
run: sf plugins install code-analyzer@latest
- name: Run Security Scan
id: run-code-analyzer
uses: forcedotcom/run-code-analyzer@v2
with:
run-arguments: --workspace . --rule-selector "all:Security" --output-file security-results.html --output-file security-results.json --output-file security-results.sarif --config-file code-analyzer.yml
results-artifact-name: security-scan-results
github-token: ${{ github.token }}
- name: Upload SARIF to GitHub Security
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: security-results.sarif
- name: Fail on Any Security Violations
if: steps.run-code-analyzer.outputs.num-sev1-violations > 0
run: |
echo "Critical security violations found!"
echo "Sev1: ${{ steps.run-code-analyzer.outputs.num-sev1-violations }}"
exit 1
```
### With Path Filters (Faster Feedback)
Use `paths:` to only trigger the workflow when relevant source files change. This avoids running scans on README edits, CI config changes, etc.
```yaml
# .github/workflows/code-analyzer-filtered.yml
name: Salesforce Code Analyzer
on:
pull_request:
branches: [main, develop]
paths:
- 'force-app/**'
- '**/*.cls'
- '**/*.trigger'
- '**/*.js'
- '**/*.ts'
- '**/*.html'
- '**/*.flow-meta.xml'
- 'code-analyzer.yml'
jobs:
code-analysis:
permissions:
pull-requests: write
contents: read
actions: read
runs-on: ubuntu-latest
steps:
- name: Check out files
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '>=20.9.0'
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '>=11'
- name: Install Salesforce CLI
run: npm install -g @salesforce/cli@latest
- name: Install Code Analyzer Plugin
run: sf plugins install code-analyzer@latest
- name: Run Salesforce Code Analyzer
id: run-code-analyzer
uses: forcedotcom/run-code-analyzer@v2
with:
run-arguments: --workspace . --rule-selector Recommended --output-file sfca_results.html --output-file sfca_results.json
results-artifact-name: code-analyzer-results
github-token: ${{ github.token }}
- name: Check Quality Gate
if: |
steps.run-code-analyzer.outputs.num-sev1-violations-in-changed-files > 0 ||
steps.run-code-analyzer.outputs.num-sev2-violations-in-changed-files > 0
run: exit 1
```
**When to use path filters:**
- Use when the repo contains non-Salesforce code (docs, scripts, infra) that shouldn't trigger scans
- Always include `code-analyzer.yml` in paths so config changes trigger a validation run
- Don't use if you want every PR to get a scan regardless of what changed
### With Flow Engine (Python Required)
If the project contains `.flow-meta.xml` files and uses the Flow engine, Python 3.10+ must be installed:
```yaml
- name: Setup Python (required for Flow engine)
uses: actions/setup-python@v5
with:
python-version: '>=3.10'
```
Insert this step after the Java setup step. If you're unsure whether the project uses Flows, include it — it adds ~5s and avoids a partial scan failure.
### Monorepo / Scoped Scan
For monorepos where Salesforce code lives in a subdirectory, use `--target` to scope the scan:
```yaml
- name: Run Salesforce Code Analyzer
id: run-code-analyzer
uses: forcedotcom/run-code-analyzer@v2
with:
run-arguments: --workspace . --target packages/salesforce-app/force-app --rule-selector Recommended --output-file sfca_results.html --output-file sfca_results.json
results-artifact-name: code-analyzer-results
github-token: ${{ github.token }}
```
For multiple packages:
```
--target packages/app-a/force-app --target packages/app-b/force-app
```
### Without a Config File
If the project has no `code-analyzer.yml`, simply omit `--config-file`. Code Analyzer uses built-in defaults:
```yaml
- name: Run Salesforce Code Analyzer
id: run-code-analyzer
uses: forcedotcom/run-code-analyzer@v2
with:
run-arguments: --workspace . --rule-selector Recommended --output-file sfca_results.html --output-file sfca_results.json
results-artifact-name: code-analyzer-results
github-token: ${{ github.token }}
```
To conditionally use a config file if it exists:
```yaml
- name: Check for config file
id: config-check
run: |
if [ -f "code-analyzer.yml" ]; then
echo "config-flag=--config-file code-analyzer.yml" >> $GITHUB_OUTPUT
else
echo "config-flag=" >> $GITHUB_OUTPUT
fi
- name: Run Salesforce Code Analyzer
id: run-code-analyzer
uses: forcedotcom/run-code-analyzer@v2
with:
run-arguments: --workspace . --rule-selector Recommended --output-file sfca_results.html --output-file sfca_results.json ${{ steps.config-check.outputs.config-flag }}
results-artifact-name: code-analyzer-results
github-token: ${{ github.token }}
```
## Jenkins
### Jenkinsfile (Declarative Pipeline)
```groovy
// Jenkinsfile
pipeline {
agent {
docker {
image 'node:20'
args '-v /usr/local/share/java:/usr/local/share/java'
}
}
environment {
JAVA_HOME = '/usr/lib/jvm/java-11-openjdk-amd64'
}
stages {
stage('Setup') {
steps {
sh 'npm install -g @salesforce/cli@latest'
sh 'sf plugins install code-analyzer@latest'
}
}
stage('Code Analysis') {
steps {
sh '''
sf code-analyzer run \
--workspace . \
--rule-selector Recommended \
--output-file results.json \
--output-file results.html \
--config-file code-analyzer.yml 2>&1 | tee sfca_output.txt
'''
script {
def output = readFile('results.json')
def json = new groovy.json.JsonSlurper().parseText(output)
def sev1Count = json.violations?.count { it.severity == 1 } ?: 0
def sev2Count = json.violations?.count { it.severity == 2 } ?: 0
if (sev1Count > 0 || sev2Count > 0) {
error("Quality gate failed! Sev1: ${sev1Count}, Sev2: ${sev2Count}")
}
}
}
post {
always {
archiveArtifacts artifacts: 'results.*', allowEmptyArchive: true
publishHTML(target: [
reportName: 'Code Analyzer Report',
reportDir: '.',
reportFiles: 'results.html'
])
}
}
}
}
post {
failure {
echo 'Code analysis found violations above severity threshold!'
}
}
}
```
## GitLab CI
```yaml
# .gitlab-ci.yml
code-analysis:
image: node:20
stage: test
before_script:
- apt-get update && apt-get install -y openjdk-11-jdk python3
- export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
- npm install -g @salesforce/cli@latest
- sf plugins install code-analyzer@latest
script:
- |
sf code-analyzer run \
--workspace . \
--rule-selector Recommended \
--output-file results.json \
--output-file results.html \
--config-file code-analyzer.yml
- |
# Quality gate: fail on sev1 or sev2 violations
SEV1=$(cat results.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(sum(1 for v in d.get('violations',[]) if v.get('severity')==1))")
SEV2=$(cat results.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(sum(1 for v in d.get('violations',[]) if v.get('severity')==2))")
echo "Sev1: $SEV1, Sev2: $SEV2"
if [ "$SEV1" -gt 0 ] || [ "$SEV2" -gt 0 ]; then
echo "Quality gate failed!"
exit 1
fi
artifacts:
paths:
- results.json
- results.html
reports:
codequality: results.json
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
```
## Bitbucket Pipelines
```yaml
# bitbucket-pipelines.yml
pipelines:
pull-requests:
'**':
- step:
name: Code Analysis
image: node:20
script:
- apt-get update && apt-get install -y openjdk-11-jdk
- export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
- npm install -g @salesforce/cli@latest
- sf plugins install code-analyzer@latest
- |
sf code-analyzer run \
--workspace . \
--rule-selector Recommended \
--output-file results.json \
--output-file results.html \
--config-file code-analyzer.yml
- |
# Quality gate: fail on sev1 or sev2 violations
SEV1=$(cat results.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(sum(1 for v in d.get('violations',[]) if v.get('severity')==1))")
SEV2=$(cat results.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(sum(1 for v in d.get('violations',[]) if v.get('severity')==2))")
echo "Sev1: $SEV1, Sev2: $SEV2"
if [ "$SEV1" -gt 0 ] || [ "$SEV2" -gt 0 ]; then
echo "Quality gate failed!"
exit 1
fi
artifacts:
- results.json
- results.html
```
## Configuration Tips for CI/CD
### Quality Gating Strategy
With `forcedotcom/run-code-analyzer@v2`, you control the quality gate via output checks:
| Strategy | Condition | Use Case |
|----------|-----------|----------|
| Block on critical only | `num-sev1-violations > 0` | Permissive — only block security vulnerabilities |
| Block on critical + high | `num-sev1-violations > 0 \|\| num-sev2-violations > 0` | Recommended for most teams |
| Block on changed files only | `num-sev1-violations-in-changed-files > 0` | Great for legacy codebases — don't block on pre-existing issues |
| Block on total count | `num-violations > 10` | Budget approach — allow some violations but cap total |
| Zero tolerance | `num-violations > 0` | For new greenfield projects only |
For non-GitHub platforms (Jenkins, GitLab, Bitbucket), parse `results.json` to count violations by severity and fail the pipeline accordingly.
### Output Formats
Specify multiple `--output-file` flags (format inferred from extension):
| Format | Extension | Best For |
|--------|-----------|----------|
| SARIF | `.sarif` | GitHub Code Scanning integration (requires `security-events: write` permission) |
| JSON | `.json` | Programmatic processing, quality gating scripts, custom dashboards |
| HTML | `.html` | Human-readable reports in artifacts |
| CSV | `.csv` | Spreadsheet analysis |
| XML | `.xml` | Legacy tool integration |
### Caching for Faster CI
```yaml
# GitHub Actions: Cache sf CLI plugins
- uses: actions/cache@v4
with:
path: ~/.local/share/sf/
key: sf-plugins-${{ hashFiles('**/code-analyzer.yml') }}
restore-keys: sf-plugins-
```
### Recommended PR Workflow
1. **On PR open/update:** Use `forcedotcom/run-code-analyzer@v2` with `github-token` — gate on changed-files outputs only
2. **On merge to main:** Full scan with SARIF upload to GitHub Security tab
3. **Weekly schedule:** Full security scan with `--rule-selector "all:Security"`
### When to Use Path Filters
| Situation | Use Path Filters? | Reasoning |
|-----------|-------------------|-----------|
| Mixed repo (docs, infra, salesforce code) | Yes | Avoid wasting CI minutes on non-code PRs |
| Pure Salesforce project | Optional | Every PR likely touches scannable files anyway |
| You want config changes to trigger a scan | Yes, include `code-analyzer.yml` | Validates config changes don't break the scan |
| Monorepo with multiple apps | Yes, scope to your package path | Avoid scanning unrelated packages |
---
## Composition & Adaptation Guide
When a user's request doesn't match an existing template exactly, compose from these building blocks:
### Prerequisites Block (always required)
```yaml
# ALWAYS include these — the action does NOT install them
- uses: actions/setup-node@v4 # Node >= 20.9.0
- uses: actions/setup-java@v4 # Java >= 11 (for PMD/CPD/SFGE)
- uses: actions/setup-python@v5 # Python >= 3.10 (ONLY if project has .flow-meta.xml files)
- run: npm install -g @salesforce/cli@latest
- run: sf plugins install code-analyzer@latest
```
### Composing `run-arguments`
Build the `run-arguments` string by combining these independent flags as needed:
| Need | Flag to Add |
|------|-------------|
| Scan the whole repo | `--workspace .` |
| Scan specific directory | `--target force-app/main/default/classes` |
| Scan multiple directories | `--target dir1 --target dir2` |
| Use recommended rules | `--rule-selector Recommended` |
| Use all rules | `--rule-selector all` |
| Security rules only | `--rule-selector "all:Security"` |
| Specific engine only | `--rule-selector pmd` or `--rule-selector eslint` |
| Specific severity filter | `--rule-selector "Severity:1,2"` |
| Combined selector | `--rule-selector "pmd:Security:(1,2)"` |
| Apply custom config | `--config-file code-analyzer.yml` |
| No custom config | *(omit --config-file entirely)* |
| HTML output | `--output-file results.html` |
| JSON output | `--output-file results.json` |
| SARIF for GitHub Security | `--output-file results.sarif` |
| Multiple formats | `--output-file a.html --output-file b.json --output-file c.sarif` |
| Verbose output | `--view detail` |
### Permissions Required
| Feature | Permission Needed |
|---------|------------------|
| PR review comments | `pull-requests: write` |
| Upload SARIF to Security tab | `security-events: write` |
| Private repo checkout | `contents: read` |
| Download artifacts | `actions: read` |
### Decision Tree for Template Selection
```
User wants CI/CD for Code Analyzer
├── Platform?
│ ├── GitHub Actions → Use forcedotcom/run-code-analyzer@v2
│ ├── Jenkins → Use Jenkinsfile template + JSON parsing for quality gate
│ ├── GitLab → Use .gitlab-ci.yml template + script-based quality gate
│ └── Bitbucket → Use bitbucket-pipelines.yml template + script-based quality gate
├── Scope?
│ ├── Full repo → --workspace .
│ ├── Specific folder → --target <path>
│ └── Changed files only → use github-token + *-in-changed-files outputs (GitHub only)
├── Strictness?
│ ├── Block on sev1 only → check num-sev1-violations
│ ├── Block on sev1+sev2 → check both (RECOMMENDED DEFAULT)
│ ├── Block on any violation → check num-violations
│ └── Legacy codebase → use *-in-changed-files to avoid blocking on old debt
├── Has config file?
│ ├── Yes → add --config-file code-analyzer.yml
│ ├── No → omit --config-file
│ └── Maybe → use conditional check pattern
├── Has Flow files?
│ ├── Yes → add actions/setup-python step
│ └── No → skip Python setup
└── Wants GitHub Security integration?
├── Yes → add --output-file *.sarif + upload-sarif step + security-events: write
└── No → skip SARIF
```
### Common User Requests → Modifications
| User Says | What to Change |
|-----------|---------------|
| "Only scan on PRs to main" | Set `on: pull_request: branches: [main]` |
| "Scan only Apex classes" | Use `--target force-app/main/default/classes` |
| "Don't fail on existing violations" | Use `*-in-changed-files` outputs for quality gate |
| "I want to see results in GitHub Security tab" | Add `--output-file *.sarif` + `upload-sarif` step + `security-events: write` |
| "Run a nightly full scan" | Add `schedule: - cron: '0 0 * * *'` trigger |
| "We don't have a config file" | Remove `--config-file` from run-arguments |
| "Only run security rules" | Change `--rule-selector` to `"all:Security"` |
| "Block the PR if there are more than 5 violations" | Change gate to `num-violations > 5` |
| "We have Flows in our project" | Add `actions/setup-python@v5` with `python-version: '>=3.10'` |
| "Scan only what changed in the PR" | Use `github-token` + gate on `*-in-changed-files` outputs |
| "We're a monorepo" | Use `--target packages/my-sf-app/force-app` instead of `--workspace .` |
| "Cache the plugin install" | Add `actions/cache@v4` step for `~/.local/share/sf/` |
| "I want an HTML report I can download" | Add `--output-file results.html` (auto-uploaded via results-artifact-name) |

View File

@ -0,0 +1,257 @@
# Code Analyzer Configuration Schema
Full reference for the `code-analyzer.yml` configuration file.
## Top-Level Fields
```yaml
# code-analyzer.yml
# Root directory for resolving relative paths in the config
config_root: .
# Directory where Code Analyzer writes log files
log_folder: /tmp
# Log verbosity: 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Fine
log_level: 3
# File patterns to exclude from analysis
ignores:
files: []
# Rule severity, tag, and disable overrides
rules: {}
# Engine-specific configuration
engines: {}
# Bulk suppression rules
suppressions:
disable_suppressions: false
```
## Ignores Section
```yaml
ignores:
files:
- "**/node_modules/**" # npm dependencies
- "**/.sfdx/**" # Salesforce DX internal
- "**/.sf/**" # Salesforce CLI internal
- "**/test/**" # Test directories
- "**/*.test.js" # Test files
- "**/*.min.js" # Minified files
- "**/staticresources/**" # Static resources (often vendor)
```
**Pattern syntax:** Glob patterns using `*` (any filename chars), `**` (any path segment), `?` (single char).
## Rules Section
Override severity, tags, or disable rules per engine:
```yaml
rules:
<engine_name>:
<rule_name>:
severity: <1-5 or "Critical"|"High"|"Moderate"|"Low"|"Info">
tags: ["Tag1", "Tag2"] # Override rule tags
disabled: true|false # Disable/enable rule
```
### Severity Values
| Number | Name | Meaning |
|--------|------|---------|
| 1 | Critical | Security vulnerabilities, must fix before release |
| 2 | High | Significant issues, should fix |
| 3 | Moderate | Recommended improvements |
| 4 | Low | Minor suggestions |
| 5 | Info | Informational, no action required |
### Example Rule Overrides
```yaml
rules:
pmd:
ApexCRUDViolation:
severity: 1 # Promote to Critical
AvoidGlobalModifier:
disabled: true # Disable entirely
ApexDoc:
severity: 5 # Demote to Info
tags: ["Documentation"]
eslint:
no-console:
severity: 4 # Demote to Low
no-unused-vars:
severity: 2 # Promote to High
```
## Engines Section
### PMD Engine
```yaml
engines:
pmd:
disable_engine: false
java_command: "java" # Path to Java executable
custom_rulesets: # Additional ruleset XML files
- "./config/custom-pmd-rules.xml"
java_classpath_entries: # JARs for custom Java rules
- "./lib/my-custom-rules.jar"
file_extensions: # Override scanned file types
apex: [".cls", ".trigger"]
visualforce: [".page", ".component"]
```
### ESLint Engine
```yaml
engines:
eslint:
disable_engine: false
auto_discover_eslint_config: true # Use project's eslint config files
eslint_config_file: "./eslint.config.mjs" # Explicit config file path
disable_javascript_base_config: false # Disable built-in JS rules
disable_typescript_base_config: false # Disable built-in TS rules
disable_lwc_base_config: false # Disable built-in LWC rules
disable_flow_base_config: false # Disable built-in Flow rules
```
**Note:** `auto_discover_eslint_config` requires a `--workspace` flag on the run command.
### CPD Engine (Copy-Paste Detector)
```yaml
engines:
cpd:
disable_engine: false
minimum_tokens: # Min tokens for duplicate detection
apex: 100 # Lower = more sensitive
html: 100
javascript: 100
visualforce: 100
xml: 100
skip_duplicate_files: false # Skip files with identical content
```
### SFGE Engine (Salesforce Graph Engine)
```yaml
engines:
sfge:
disable_engine: false
java_max_heap_size: "4g" # JVM heap (increase for large projects)
java_thread_count: 4 # Parallel threads
java_thread_timeout: 900000 # Per-thread timeout in ms
```
**Warning:** SFGE is resource-intensive. For projects with 500+ Apex classes, use 4g+ heap. Analysis can take 10-30 minutes.
### ApexGuru Engine
```yaml
engines:
apexguru:
disable_engine: false
target_org: "my-org-alias" # Authenticated org alias or username
api_timeout_ms: 300000 # API timeout in ms (default 5min)
```
**Requires:** Authenticated Salesforce org (`sf org login web`).
### Flow Engine
```yaml
engines:
flow:
disable_engine: false
python_command: "python3" # Path to Python 3 executable
```
**Requires:** Python 3 installed.
### Regex Engine
```yaml
engines:
regex:
disable_engine: false
custom_rules:
<RuleName>:
regex: "/<pattern>/<flags>" # JavaScript regex syntax
regex_ignore: "/<pattern>/<flags>" # Optional: false positive filter
file_extensions: [".cls", ".trigger"]
description: "What this rule checks"
violation_message: "Message shown to developer"
severity: 3
tags: ["Recommended", "Security"]
```
### RetireJS Engine
```yaml
engines:
retire-js:
disable_engine: false
```
**Note:** RetireJS scans JavaScript dependencies for known CVEs. No additional configuration needed beyond enable/disable.
## Suppressions Section
```yaml
suppressions:
disable_suppressions: false # Set true to ignore ALL suppressions
# Bulk suppressions by file/folder path
"src/legacy/":
- rule_selector: "pmd:ApexDoc"
max_suppressed_violations: 50 # Quota (null = unlimited)
reason: "Legacy code, documentation not required"
"src/utils/Logger.cls":
- rule_selector: "eslint:no-console"
max_suppressed_violations: 10
reason: "Logger intentionally uses console"
```
### Inline Suppression Markers
In addition to bulk config suppressions, violations can be suppressed inline:
```java
// Apex: PMD suppression
// NOPMD - reason here
@SuppressWarnings('PMD.ApexCRUDViolation')
// Any engine: universal marker
// code-analyzer-suppress(pmd:ApexCRUDViolation) - reason
// code-analyzer-suppress(eslint:no-console) - reason
```
## Config File Discovery
Code Analyzer automatically looks for configuration in this order:
1. File specified via `--config-file` flag
2. `code-analyzer.yml` in current working directory
3. `code-analyzer.yaml` in current working directory
4. No config (use defaults)
## Validating Configuration
Always validate after making changes:
```bash
# Validate config and show effective settings
sf code-analyzer config --config-file code-analyzer.yml
# Show config for specific rules
sf code-analyzer config --rule-selector pmd:Security
# Show all rule defaults (verbose)
sf code-analyzer config --include-unmodified-rules --rule-selector all
```

View File

@ -0,0 +1,70 @@
# Diagnostic Flow: Fix a Broken Setup
**TRIGGER:** User says "not working", "broken", "getting errors", "scan fails", "help me fix", etc.
## NEVER DO THESE (anti-patterns that waste time)
- ❌ NEVER run `which sfdx`, `which sf`, `find`, `ls /opt/homebrew/bin/` or search for binaries
- ❌ NEVER use an old `sfdx` binary as a workaround — it is NOT a substitute for `sf`
- ❌ NEVER create symlinks (`ln -s`) to work around missing commands
- ❌ NEVER check PATH, inspect Cellar directories, or search for alternative installations
- ❌ NEVER proceed to Layer 2 if Layer 1 failed — fix Layer 1 first
- ❌ NEVER give the user a list of manual steps — fix it yourself or give ONE command
## Diagnostic Flow (follow this EXACTLY, no deviation)
Run **ONLY** this one command first:
```bash
sf --version 2>&1
```
**If output contains "command not found":**
→ STOP. Do not run any other commands. Tell user: "sf CLI is not installed. I'll install it now."
→ Ask user for permission, then run: `npm install -g @salesforce/cli`
→ After install, re-run `sf --version 2>&1` to verify. If it works, continue to next layer.
**If sf works**, run ONLY:
```bash
sf plugins --core 2>&1 | grep -i "code-analyzer"
```
**If output is empty or shows "JIT" but not a real version:**
→ STOP. Run: `sf plugins install @salesforce/plugin-code-analyzer`
→ After install, re-check. If it works, continue to next layer.
**If plugin is installed**, check engine deps:
```bash
java -version 2>&1
node --version 2>&1
```
**If all pass**, verify with a scan:
```bash
sf code-analyzer run --rule-selector Recommended 2>&1 | tail -20
```
## Fix Table
| Error Pattern | The ONE Fix |
|--------------|-------------|
| `sf: command not found` | `npm install -g @salesforce/cli` |
| Plugin missing / JIT error | `sf plugins install @salesforce/plugin-code-analyzer` |
| `Cannot find module` | `sf plugins uninstall @salesforce/plugin-code-analyzer && sf plugins install @salesforce/plugin-code-analyzer` |
| `java: command not found` | Install Java 11+ (see `<skill_dir>/references/engine-prerequisites.md`) |
| `OutOfMemoryError` (SFGE) | Add `engines.sfge.java_max_heap_size: "4g"` to `code-analyzer.yml` |
| `YAMLException` | Read the config file, fix YAML syntax |
| `EPERM` / npm permission error | Tell user to run: `sudo chown -R $(whoami) ~/.npm` — then wait for them to confirm, then retry the SAME install command that failed. Do NOT dump next steps. |
## After Fix: Verify and Hand Off
Re-run the check for the fixed layer. Once a scan succeeds, tell the user what was fixed and **proceed to run the full scan**.
## When a fix requires user action (sudo, manual step)
Tell the user ONLY the ONE command they need to run and WHY. Then STOP and WAIT for them to confirm it's done. Do NOT:
- ❌ List the remaining steps ("after that, do X, then Y, then Z")
- ❌ Tell them what to run next after the manual step
- ❌ Provide a multi-step recovery plan
- ❌ Ask "would you like me to attempt with sudo or do it yourself"
Just say: "Run this command: `<command>`. It fixes [reason]. Let me know when it's done and I'll continue."

View File

@ -0,0 +1,276 @@
# Engine Prerequisites
Detailed installation instructions for each Code Analyzer engine's dependencies.
## Summary Table
| Engine | Required Dependencies | Optional |
|--------|----------------------|----------|
| PMD | Java 11+ | Custom ruleset JARs |
| CPD | Java 11+ | — |
| ESLint | Node.js 18+ | Project ESLint config |
| RetireJS | Node.js 18+ | — |
| Regex | None (built-in) | — |
| Flow | Python 3 | — |
| SFGE | Java 11+ (4g+ heap recommended) | — |
| ApexGuru | Authenticated Salesforce org | — |
## Core: Salesforce CLI
**Required for ALL engines.**
### macOS
```bash
# Via Homebrew (recommended)
brew install sf
# Or via npm
npm install -g @salesforce/cli
```
### Windows
```bash
# Via npm
npm install -g @salesforce/cli
# Or download installer from:
# https://developer.salesforce.com/tools/salesforcecli
```
### Linux
```bash
# Via npm
npm install -g @salesforce/cli
# Or via tarball:
# https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_install_cli.htm
```
### Verify
```bash
sf --version
# Expected: @salesforce/cli/2.x.x ...
```
## Code Analyzer Plugin
**Required: sf CLI must be installed first.**
```bash
# Install
sf plugins install @salesforce/plugin-code-analyzer
# Verify
sf code-analyzer --help
# Update to latest
sf plugins install @salesforce/plugin-code-analyzer@latest
# Check version
sf plugins --core | grep code-analyzer
```
## Java 11+ (for PMD, CPD, SFGE)
### macOS
```bash
# Via Homebrew
brew install openjdk@11
# Add to PATH (add to ~/.zshrc or ~/.bash_profile)
export PATH="/opt/homebrew/opt/openjdk@11/bin:$PATH"
export JAVA_HOME="/opt/homebrew/opt/openjdk@11"
# Or via SDKMAN (manages multiple Java versions)
curl -s "https://get.sdkman.io" | bash
sdk install java 11.0.21-tem
```
### Windows
```bash
# Via winget
winget install EclipseAdoptium.Temurin.11.JDK
# Or via Chocolatey
choco install temurin11
```
### Linux
```bash
# Ubuntu/Debian
sudo apt install openjdk-11-jdk
# RHEL/CentOS/Fedora
sudo dnf install java-11-openjdk-devel
# Via SDKMAN (any Linux)
curl -s "https://get.sdkman.io" | bash
sdk install java 11.0.21-tem
```
### Verify
```bash
java -version
# Expected: openjdk version "11.x.x" or higher
echo $JAVA_HOME
# Should point to JDK installation
```
### Troubleshooting Java
| Issue | Solution |
|-------|----------|
| `java: command not found` | Add Java bin dir to PATH |
| Wrong Java version | Set JAVA_HOME explicitly |
| Multiple Java versions | Use `sdk use java 11.x.x` or update PATH order |
| SFGE heap errors | Increase `java_max_heap_size` in config |
## Node.js 18+ (for ESLint, RetireJS)
### macOS
```bash
# Via Homebrew
brew install node@20
# Or via nvm (recommended for version management)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
nvm install 20
nvm use 20
```
### Windows
```bash
# Via winget
winget install OpenJS.NodeJS.LTS
# Or via nvm-windows
# Download from: https://github.com/coreybutler/nvm-windows/releases
nvm install 20
nvm use 20
```
### Linux
```bash
# Via nvm (recommended)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
nvm install 20
nvm use 20
# Or via package manager (may be outdated)
# Ubuntu/Debian (use NodeSource for latest):
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
```
### Verify
```bash
node --version
# Expected: v20.x.x or v18.x.x (minimum v18)
npm --version
# Expected: 9.x.x or 10.x.x
```
## Python 3 (for Flow Engine)
**Only needed if you scan Flow files (*.flow-meta.xml).**
### macOS
```bash
# Via Homebrew
brew install python3
# macOS may already have python3 via Xcode Command Line Tools
xcode-select --install
```
### Windows
```bash
# Via winget
winget install Python.Python.3.12
# Or from python.org
# https://www.python.org/downloads/windows/
```
### Linux
```bash
# Usually pre-installed. If not:
# Ubuntu/Debian
sudo apt install python3
# RHEL/CentOS/Fedora
sudo dnf install python3
```
### Verify
```bash
python3 --version
# Expected: Python 3.x.x
```
## Authenticated Org (for ApexGuru)
**Only needed for ApexGuru performance analysis.**
```bash
# Login to a Salesforce org
sf org login web --alias my-org
# Or login with JWT (CI/CD)
sf org login jwt --client-id <id> --jwt-key-file <key> --username <user> --alias my-org
# Verify
sf org display --target-org my-org
```
### Troubleshooting ApexGuru Auth
| Issue | Solution |
|-------|----------|
| `No default org` | Set default: `sf config set target-org my-org` |
| `Session expired` | Re-login: `sf org login web --alias my-org` |
| `Insufficient permissions` | Org needs API access enabled |
## Quick Setup Script
For a complete setup on macOS with Homebrew:
```bash
# Install all prerequisites
brew install node@20 openjdk@11 python3
# Set Java environment
export JAVA_HOME="/opt/homebrew/opt/openjdk@11"
export PATH="/opt/homebrew/opt/openjdk@11/bin:$PATH"
# Install Salesforce CLI
npm install -g @salesforce/cli
# Install Code Analyzer
sf plugins install @salesforce/plugin-code-analyzer
# Verify everything
sf --version
sf plugins --core | grep code-analyzer
java -version
node --version
python3 --version
```

View File

@ -0,0 +1,67 @@
# Rule Name Resolution (Fuzzy Matching)
**⚠️ CRITICAL:** The `rules` section in `code-analyzer.yml` requires the EXACT full rule name as it appears in Code Analyzer's rule registry. A misspelled or partial name will be silently ignored — the override won't apply, and no error is shown.
## Why This Matters
Unlike `--rule-selector` (which returns 0 results on mismatch), a wrong name in `code-analyzer.yml` is SILENTLY ignored. The config validates fine, but the override simply doesn't apply. This makes typos and partial names dangerous.
## Common Fuzzy → Exact Mappings
Users will often refer to rules by approximate, partial, or descriptive names:
| User Says | Exact Rule Name | Engine |
|-----------|----------------|--------|
| "the ApexDoc rule" | `ApexDoc` | `pmd` |
| "no-console" | `no-console` | `eslint` |
| "CRUD violation" | `ApexCRUDViolation` | `pmd` |
| "hardcoded values" | `@salesforce-ux/slds/no-hardcoded-values-slds2` | `eslint` |
| "unused variables" | `no-unused-vars` | `eslint` |
| "soql injection" | `ApexSOQLInjection` | `pmd` |
| "global modifier" | `AvoidGlobalModifier` | `pmd` |
| "empty catch" | `EmptyCatchBlock` | `pmd` |
## Lookup Procedure
When you are NOT 100% certain of the exact full rule name:
1. **Do NOT guess** — a wrong name silently fails (the override is ignored with no error)
2. **Look up the rule first** using the `sf code-analyzer rules` command with grep:
```bash
sf code-analyzer rules --rule-selector all 2>&1 | grep -i "<USER_KEYWORD>"
```
3. **If grep returns exactly one match** → use that exact rule name in the YAML
4. **If grep returns multiple matches** → present them to the user and ask which one they meant
5. **If grep returns 0 matches** → try broader keywords or tell the user no rule matched
## When You CAN Skip the Lookup
Skip only when confident in the exact name:
- User provides the full exact name (e.g., "ApexCRUDViolation", "no-unused-vars")
- The rule is extremely common AND unambiguous (e.g., "ApexDoc", "no-console")
## Matching Strategies for Ambiguous Input
| User Says | Grep Command | Notes |
|-----------|-------------|-------|
| "the doc rule" | `grep -i "doc"` | May match ApexDoc, JSDoc, etc. — ask user if multiple |
| "CRUD" | `grep -i "crud"` | Likely matches ApexCRUDViolation |
| "hardcoded" | `grep -i "hardcoded"` | May match multiple SLDS/custom rules |
| "console" | `grep -i "console"` | Likely matches no-console |
| "security rules" | Use `--rule-selector all:Security` | Category-based, not name-based |
| "the injection rule" | `grep -i "injection"` | May match ApexSOQLInjection, ApexXSSFromURLParam, etc. |
| "unused" | `grep -i "unused"` | May match no-unused-vars, UnusedLocalVariable, etc. |
## Identifying the Engine
The YAML structure requires nesting under the correct engine. Always extract BOTH the engine and rule name from the `sf code-analyzer rules` output:
```yaml
rules:
<engine>: # ← must match the engine that owns the rule
<rule_name>:
severity: ...
disabled: ...
```
The output of `sf code-analyzer rules` shows engine name alongside each rule. Use that to determine the correct YAML path.

View File

@ -0,0 +1,298 @@
# Troubleshooting Code Analyzer Setup
Common issues and solutions during installation and configuration.
## Installation Issues
### sf CLI Not Found
**Symptom:** `sf: command not found` or `'sf' is not recognized`
**Solutions:**
1. Check if installed: `which sf` or `where sf`
2. If not installed: `npm install -g @salesforce/cli`
3. If installed but not in PATH:
- macOS/Linux: Add `export PATH="$(npm prefix -g)/bin:$PATH"` to `~/.zshrc` or `~/.bashrc`
- Windows: Add npm global bin to System PATH
4. Restart terminal after PATH changes
### Plugin Install Fails
**Symptom:** `Error: EACCES permission denied` or timeout errors
**Solutions:**
| Error | Fix |
|-------|-----|
| Permission denied | `sudo sf plugins install @salesforce/plugin-code-analyzer` or fix npm permissions |
| Network timeout | Check proxy: `npm config set proxy http://proxy:port` |
| Node version error | Upgrade Node.js to 18+: `nvm install 20 && nvm use 20` |
| Corrupt install | `sf plugins uninstall @salesforce/plugin-code-analyzer && sf plugins install @salesforce/plugin-code-analyzer` |
### Plugin Version Mismatch
**Symptom:** Command flags don't work, unexpected behavior
**Check version:**
```bash
sf plugins --core | grep code-analyzer
```
**Expected:** `@salesforce/plugin-code-analyzer` v5.x+
**If on v3/v4 (legacy):**
```bash
sf plugins uninstall @salesforce/sfdx-scanner # Remove legacy v3
sf plugins install @salesforce/plugin-code-analyzer # Install v5+
```
## Java Issues
### Java Not Found
**Symptom:** PMD/CPD/SFGE fails with `java: command not found` or `JAVA_HOME not set`
**Fix:**
```bash
# macOS
brew install openjdk@11
export JAVA_HOME="/opt/homebrew/opt/openjdk@11"
export PATH="$JAVA_HOME/bin:$PATH"
# Add to ~/.zshrc for persistence
echo 'export JAVA_HOME="/opt/homebrew/opt/openjdk@11"' >> ~/.zshrc
echo 'export PATH="$JAVA_HOME/bin:$PATH"' >> ~/.zshrc
```
### Wrong Java Version
**Symptom:** `UnsupportedClassVersionError` or `class file version X.Y`
**Fix:** Code Analyzer needs Java 11+. Check and switch:
```bash
java -version # Check current
# If using SDKMAN:
sdk install java 11.0.21-tem
sdk use java 11.0.21-tem
# If using Homebrew:
brew install openjdk@11
export JAVA_HOME="/opt/homebrew/opt/openjdk@11"
```
### SFGE Out of Memory
**Symptom:** `java.lang.OutOfMemoryError: Java heap space`
**Fix:** Increase heap in `code-analyzer.yml`:
```yaml
engines:
sfge:
java_max_heap_size: "4g" # Default is 1g, increase for large projects
```
**Guidelines:**
| Project Size | Heap Recommendation |
|-------------|-------------------|
| < 200 Apex classes | 2g |
| 200-500 Apex classes | 4g |
| 500-1000 Apex classes | 6g |
| 1000+ Apex classes | 8g |
## Node.js Issues
### Node.js Version Too Old
**Symptom:** `Error: Node.js v16 is not supported` or ESLint failures
**Fix:**
```bash
# Check version
node --version
# Upgrade via nvm
nvm install 20
nvm use 20
nvm alias default 20
# Or via Homebrew
brew upgrade node
```
### ESLint Config Conflicts
**Symptom:** ESLint rules not loading, or unexpected rules appearing
**Possible causes:**
1. Project has its own `.eslintrc.*` conflicting with Code Analyzer's built-in config
2. `auto_discover_eslint_config` is enabled but project config is incompatible
**Fix options:**
```yaml
# Option A: Disable auto-discovery (use only Code Analyzer's built-in rules)
engines:
eslint:
auto_discover_eslint_config: false
# Option B: Use project's config exclusively
engines:
eslint:
auto_discover_eslint_config: true
disable_javascript_base_config: true
disable_typescript_base_config: true
disable_lwc_base_config: true
```
## Configuration Issues
### Config File Not Picked Up
**Symptom:** Custom settings not applied, default behavior persists
**Checklist:**
1. File must be named exactly `code-analyzer.yml` or `code-analyzer.yaml`
2. File must be in the current working directory when running commands
3. Or specify explicitly: `--config-file ./path/to/code-analyzer.yml`
4. Check for YAML syntax errors: `sf code-analyzer config --config-file code-analyzer.yml`
### YAML Syntax Errors
**Symptom:** `YAMLException: bad indentation` or `unexpected token`
**Common YAML mistakes:**
```yaml
# WRONG - tabs instead of spaces
engines:
pmd: # TAB character - YAML requires spaces!
# CORRECT - spaces only
engines:
pmd: # 2 spaces
# WRONG - missing quotes around special values
rules:
pmd:
MyRule:
severity: High # String values need quotes or use numbers
# CORRECT
rules:
pmd:
MyRule:
severity: 2 # Use numbers 1-5
# OR
severity: "High" # Or quoted strings
```
### Unknown Engine or Rule Name
**Symptom:** Rule selector returns 0 results
**Fix:** Verify the engine/rule name:
```bash
# List all available engines
sf code-analyzer rules --rule-selector all 2>&1 | head -50
# Search for a specific rule
sf code-analyzer rules --rule-selector all 2>&1 | grep -i "CRUD"
# List rules for specific engine
sf code-analyzer rules --rule-selector pmd 2>&1 | head -50
```
## Engine-Specific Issues
### PMD: Custom Rules Not Loading
**Symptom:** Custom PMD rules don't appear in `sf code-analyzer rules`
**Checklist:**
1. Ruleset XML must be valid PMD format
2. Path in `custom_rulesets` must be relative to `config_root`
3. For Java rules: JAR must be in `java_classpath_entries`
4. Validate: `sf code-analyzer rules --rule-selector pmd:<YourRuleName>`
### RetireJS: False Positives on Test Files
**Symptom:** RetireJS flags test fixtures or mock data
**Fix:** Add test paths to ignores:
```yaml
ignores:
files:
- "**/test/**"
- "**/__tests__/**"
- "**/jest-mocks/**"
- "**/*.test.js"
- "**/*.spec.js"
```
### Flow Engine: Python Not Found
**Symptom:** `python3: command not found` when scanning Flows
**Fix:**
```bash
# Install Python 3
brew install python3 # macOS
# OR
sudo apt install python3 # Linux
# If python3 is at non-standard path:
# code-analyzer.yml
engines:
flow:
python_command: "/usr/local/bin/python3"
```
### ApexGuru: Authentication Error
**Symptom:** `No authenticated org found` or `Session expired`
**Fix:**
```bash
# Login to org
sf org login web --alias my-org
# Set as default
sf config set target-org my-org
# Configure in code-analyzer.yml
engines:
apexguru:
target_org: "my-org"
```
## Performance Issues
### Scan Takes Too Long
**Possible causes and solutions:**
| Cause | Solution |
|-------|----------|
| SFGE on large project | Increase heap, reduce thread timeout, or disable SFGE for routine scans |
| Scanning node_modules | Add `**/node_modules/**` to ignores |
| Too many engines enabled | Use `--rule-selector Recommended` instead of `all` for routine scans |
| Large static resources | Add `**/staticresources/**` to ignores |
| Many Flow files | Flow engine can be slow; scan Flows separately |
### Reduce Scan Time in CI
```yaml
# Fast CI scan: recommended rules only, severity gate at High
sf code-analyzer run \
--rule-selector Recommended \
--severity-threshold 2 \
--target force-app/main/default \
--output-file results.json
```
## Getting Help
If none of the above solves your issue:
1. **Check logs:** Look in the log folder (default: `/tmp` or configured `log_folder`)
2. **Increase log level:** Set `log_level: 5` in config for maximum detail
3. **Run with debug:** `SF_LOG_LEVEL=debug sf code-analyzer run ...`
4. **File an issue:** https://github.com/forcedotcom/code-analyzer-core/issues

View File

@ -0,0 +1,189 @@
#!/bin/bash
# check-prerequisites.sh
# Checks all Code Analyzer prerequisites and reports status.
# Usage: bash <skill_dir>/scripts/check-prerequisites.sh
#
# Exit codes:
# 0 = All prerequisites met
# 1 = Some prerequisites missing (details in output)
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
PASS="${GREEN}PASS${NC}"
FAIL="${RED}FAIL${NC}"
WARN="${YELLOW}WARN${NC}"
MISSING_COUNT=0
WARNINGS_COUNT=0
echo "========================================="
echo " Code Analyzer Prerequisites Check"
echo "========================================="
echo ""
# --- Check sf CLI ---
echo -n "Salesforce CLI (sf): "
if command -v sf &> /dev/null; then
SF_VERSION=$(sf --version 2>&1 | head -1)
echo -e "${PASS} - ${SF_VERSION}"
else
echo -e "${FAIL} - Not installed"
echo " Install: npm install -g @salesforce/cli"
MISSING_COUNT=$((MISSING_COUNT + 1))
fi
# --- Check Code Analyzer Plugin ---
echo -n "Code Analyzer plugin: "
if command -v sf &> /dev/null; then
CA_VERSION=$(sf plugins --core 2>&1 | grep -i "code-analyzer" | head -1)
if [ -n "$CA_VERSION" ]; then
echo -e "${PASS} - ${CA_VERSION}"
else
echo -e "${FAIL} - Not installed"
echo " Install: sf plugins install @salesforce/plugin-code-analyzer"
MISSING_COUNT=$((MISSING_COUNT + 1))
fi
else
echo -e "${FAIL} - Cannot check (sf CLI missing)"
MISSING_COUNT=$((MISSING_COUNT + 1))
fi
# --- Check Java ---
echo -n "Java 11+ (PMD, CPD, SFGE): "
if command -v java &> /dev/null; then
JAVA_VERSION=$(java -version 2>&1 | head -1)
# Extract major version number
JAVA_MAJOR=$(java -version 2>&1 | head -1 | sed -E 's/.*"([0-9]+)\..*/\1/')
if [ "$JAVA_MAJOR" -ge 11 ] 2>/dev/null; then
echo -e "${PASS} - ${JAVA_VERSION}"
else
echo -e "${WARN} - ${JAVA_VERSION} (need 11+)"
echo " Upgrade: brew install openjdk@11"
WARNINGS_COUNT=$((WARNINGS_COUNT + 1))
fi
else
echo -e "${FAIL} - Not installed"
echo " Install: brew install openjdk@11 (macOS) / sdk install java 11.0.x-tem"
echo " Needed for: PMD, CPD, SFGE engines"
MISSING_COUNT=$((MISSING_COUNT + 1))
fi
# --- Check JAVA_HOME ---
echo -n "JAVA_HOME: "
if [ -n "${JAVA_HOME:-}" ]; then
echo -e "${PASS} - ${JAVA_HOME}"
else
echo -e "${WARN} - Not set (may cause issues with some Java installations)"
WARNINGS_COUNT=$((WARNINGS_COUNT + 1))
fi
# --- Check Node.js ---
echo -n "Node.js 18+ (ESLint, RetireJS): "
if command -v node &> /dev/null; then
NODE_VERSION=$(node --version 2>&1)
NODE_MAJOR=$(echo "$NODE_VERSION" | sed -E 's/v([0-9]+)\..*/\1/')
if [ "$NODE_MAJOR" -ge 18 ] 2>/dev/null; then
echo -e "${PASS} - ${NODE_VERSION}"
else
echo -e "${WARN} - ${NODE_VERSION} (need 18+)"
echo " Upgrade: nvm install 20 && nvm use 20"
WARNINGS_COUNT=$((WARNINGS_COUNT + 1))
fi
else
echo -e "${FAIL} - Not installed"
echo " Install: brew install node@20 (macOS) / nvm install 20"
echo " Needed for: ESLint, RetireJS engines"
MISSING_COUNT=$((MISSING_COUNT + 1))
fi
# --- Check Python 3 ---
echo -n "Python 3 (Flow engine): "
if command -v python3 &> /dev/null; then
PY_VERSION=$(python3 --version 2>&1)
echo -e "${PASS} - ${PY_VERSION}"
else
echo -e "${WARN} - Not installed (only needed for Flow scanning)"
echo " Install: brew install python3 (macOS) / apt install python3 (Linux)"
echo " Needed for: Flow engine only"
WARNINGS_COUNT=$((WARNINGS_COUNT + 1))
fi
# --- Check authenticated org ---
echo -n "Authenticated Org (ApexGuru): "
if command -v sf &> /dev/null; then
ORG_INFO=$(sf org display 2>&1)
if echo "$ORG_INFO" | grep -qi "username\|access token"; then
ORG_USER=$(echo "$ORG_INFO" | grep -i "username" | head -1 | awk '{print $NF}')
echo -e "${PASS} - ${ORG_USER}"
else
echo -e "${WARN} - No default org (only needed for ApexGuru)"
echo " Login: sf org login web --alias my-org"
WARNINGS_COUNT=$((WARNINGS_COUNT + 1))
fi
else
echo -e "${WARN} - Cannot check (sf CLI missing)"
WARNINGS_COUNT=$((WARNINGS_COUNT + 1))
fi
# --- Check for existing config ---
echo ""
echo "-----------------------------------------"
echo -n "Config file (code-analyzer.yml): "
if [ -f "code-analyzer.yml" ] || [ -f "code-analyzer.yaml" ]; then
CONFIG_FILE=$(ls code-analyzer.yml code-analyzer.yaml 2>/dev/null | head -1)
echo -e "${PASS} - Found: ${CONFIG_FILE}"
else
echo -e "${WARN} - Not found (will use defaults)"
echo " Generate: sf code-analyzer config --output-file code-analyzer.yml"
fi
# --- Check project type ---
echo -n "Project type: "
HAS_APEX=false
HAS_LWC=false
HAS_FLOWS=false
HAS_SFDX=false
[ -d "force-app" ] && HAS_APEX=true
find . -maxdepth 4 -path "*/lwc/*" -name "*.js" 2>/dev/null | head -1 | grep -q . && HAS_LWC=true
find . -maxdepth 4 -name "*.flow-meta.xml" 2>/dev/null | head -1 | grep -q . && HAS_FLOWS=true
[ -f "sfdx-project.json" ] || [ -f "sf-project.json" ] && HAS_SFDX=true
PROJECT_TYPES=""
[ "$HAS_APEX" = true ] && PROJECT_TYPES="${PROJECT_TYPES}Apex, "
[ "$HAS_LWC" = true ] && PROJECT_TYPES="${PROJECT_TYPES}LWC, "
[ "$HAS_FLOWS" = true ] && PROJECT_TYPES="${PROJECT_TYPES}Flows, "
[ "$HAS_SFDX" = true ] && PROJECT_TYPES="${PROJECT_TYPES}SFDX Project, "
if [ -n "$PROJECT_TYPES" ]; then
echo "${PROJECT_TYPES%, }"
else
echo "Unknown (no Salesforce project markers found)"
fi
# --- Summary ---
echo ""
echo "========================================="
echo " Summary"
echo "========================================="
if [ $MISSING_COUNT -eq 0 ] && [ $WARNINGS_COUNT -eq 0 ]; then
echo -e "${GREEN}All prerequisites met! Ready to scan.${NC}"
exit 0
elif [ $MISSING_COUNT -eq 0 ]; then
echo -e "${YELLOW}${WARNINGS_COUNT} warning(s) - some engines may not work.${NC}"
echo "Core functionality (PMD, ESLint) should work if Java and Node.js are available."
exit 0
else
echo -e "${RED}${MISSING_COUNT} required prerequisite(s) missing.${NC}"
[ $WARNINGS_COUNT -gt 0 ] && echo -e "${YELLOW}${WARNINGS_COUNT} additional warning(s).${NC}"
echo ""
echo "Install missing prerequisites before running Code Analyzer."
exit 1
fi

View File

@ -0,0 +1,143 @@
#!/bin/bash
# generate-config.sh
# Generates a code-analyzer.yml with ONLY project-specific overrides.
# Does NOT duplicate built-in defaults — only writes what intentionally differs.
#
# Usage: bash <skill_dir>/scripts/generate-config.sh [--type apex|lwc|fullstack]
#
# If --type is not specified, auto-detects from workspace contents.
set -euo pipefail
CONFIG_FILE="code-analyzer.yml"
PROJECT_TYPE="${1:-auto}"
# Remove --type prefix if present
PROJECT_TYPE="${PROJECT_TYPE#--type=}"
PROJECT_TYPE="${PROJECT_TYPE#--type }"
# Auto-detect project type
if [ "$PROJECT_TYPE" = "auto" ] || [ "$PROJECT_TYPE" = "" ]; then
HAS_APEX=false
HAS_LWC=false
HAS_NODE_MODULES=false
[ -d "force-app" ] && HAS_APEX=true
find . -maxdepth 4 -path "*/lwc/*" -name "*.js" 2>/dev/null | head -1 | grep -q . && HAS_LWC=true
[ -d "node_modules" ] && HAS_NODE_MODULES=true
if [ "$HAS_APEX" = true ] && [ "$HAS_LWC" = true ]; then
PROJECT_TYPE="fullstack"
elif [ "$HAS_LWC" = true ]; then
PROJECT_TYPE="lwc"
elif [ "$HAS_APEX" = true ]; then
PROJECT_TYPE="apex"
else
PROJECT_TYPE="minimal"
fi
echo "Auto-detected project type: ${PROJECT_TYPE}"
# Warn if no Salesforce project markers found
if [ "$PROJECT_TYPE" = "minimal" ] && [ ! -f "sfdx-project.json" ] && [ ! -f "sf-project.json" ]; then
echo "WARNING: No Salesforce project markers found (no sfdx-project.json, sf-project.json, or force-app/)."
echo " Are you running this from your project root?"
echo " Generating minimal config — re-run from project root for better detection."
fi
fi
# Check if config already exists
if [ -f "$CONFIG_FILE" ] || [ -f "code-analyzer.yaml" ]; then
echo "WARNING: Config file already exists."
echo "Edit the existing file instead of regenerating."
exit 1
fi
echo "Generating ${CONFIG_FILE} (overrides only)..."
case "$PROJECT_TYPE" in
apex)
cat > "$CONFIG_FILE" << 'YAML'
# Code Analyzer overrides for Apex project
# Only entries that differ from built-in defaults
ignores:
files:
- "**/node_modules/**"
- "**/.sfdx/**"
- "**/.sf/**"
engines:
sfge:
java_max_heap_size: "4g"
YAML
;;
lwc)
cat > "$CONFIG_FILE" << 'YAML'
# Code Analyzer overrides for LWC project
# Only entries that differ from built-in defaults
ignores:
files:
- "**/node_modules/**"
- "**/.sfdx/**"
- "**/.sf/**"
- "**/jest-mocks/**"
- "**/__tests__/**"
engines:
eslint:
auto_discover_eslint_config: true
YAML
;;
fullstack)
cat > "$CONFIG_FILE" << 'YAML'
# Code Analyzer overrides for full-stack Salesforce project
# Only entries that differ from built-in defaults
ignores:
files:
- "**/node_modules/**"
- "**/.sfdx/**"
- "**/.sf/**"
- "**/jest-mocks/**"
- "**/__tests__/**"
engines:
sfge:
java_max_heap_size: "4g"
eslint:
auto_discover_eslint_config: true
YAML
;;
minimal)
cat > "$CONFIG_FILE" << 'YAML'
# Code Analyzer overrides
# Only entries that differ from built-in defaults
ignores:
files:
- "**/node_modules/**"
YAML
;;
*)
echo "ERROR: Unknown project type: ${PROJECT_TYPE}"
echo "Valid types: apex, lwc, fullstack, minimal"
exit 1
;;
esac
echo ""
echo "Created: ${CONFIG_FILE}"
echo ""
echo "This file contains ONLY overrides — built-in defaults still apply for"
echo "everything not listed here. Add more overrides as needed."
echo ""
echo "Next steps:"
echo " 1. Review: cat ${CONFIG_FILE}"
echo " 2. Validate: sf code-analyzer config --config-file ${CONFIG_FILE}"
echo " 3. Run scan: sf code-analyzer run --output-file results.json --include-fixes"

View File

@ -0,0 +1,153 @@
#!/bin/bash
# validate-config.sh
# Validates a code-analyzer.yml configuration file.
# Usage: bash <skill_dir>/scripts/validate-config.sh [config-file]
#
# If no config file is specified, looks for code-analyzer.yml in current directory.
#
# Exit codes:
# 0 = Config is valid
# 1 = Config has errors
set -euo pipefail
CONFIG_FILE="${1:-code-analyzer.yml}"
echo "Validating: ${CONFIG_FILE}"
echo ""
# Check file exists
if [ ! -f "$CONFIG_FILE" ]; then
echo "ERROR: Config file not found: ${CONFIG_FILE}"
echo ""
echo "Expected locations:"
echo " - ./code-analyzer.yml"
echo " - ./code-analyzer.yaml"
echo ""
echo "Generate one with: sf code-analyzer config --output-file code-analyzer.yml"
exit 1
fi
# Check file is not empty
if [ ! -s "$CONFIG_FILE" ]; then
echo "ERROR: Config file is empty: ${CONFIG_FILE}"
exit 1
fi
# Basic YAML syntax check (if python3 available)
if command -v python3 &> /dev/null; then
echo "Checking YAML syntax..."
if python3 -c "
import yaml, sys
try:
with open('${CONFIG_FILE}', 'r') as f:
config = yaml.safe_load(f)
if config is None:
print('WARNING: Config file is empty or contains only comments')
sys.exit(0)
if not isinstance(config, dict):
print('ERROR: Config must be a YAML mapping (dictionary)')
sys.exit(1)
print('YAML syntax: OK')
except yaml.YAMLError as e:
print(f'ERROR: Invalid YAML syntax')
print(f' {e}')
sys.exit(1)
" 2>&1; then
echo ""
else
echo ""
echo "Fix YAML syntax errors before proceeding."
exit 1
fi
else
echo "WARNING: python3 not found — skipping YAML syntax validation (install python3 for full checks)"
fi
# Validate known field names
echo "Checking known fields..."
VALID_TOP_LEVEL="config_root log_folder log_level rules engines ignores suppressions"
if command -v python3 &> /dev/null; then
python3 -c "
import yaml, sys
with open('${CONFIG_FILE}', 'r') as f:
config = yaml.safe_load(f)
if config is None:
sys.exit(0)
valid_fields = set('${VALID_TOP_LEVEL}'.split())
unknown = set(config.keys()) - valid_fields
if unknown:
print(f'WARNING: Unknown top-level fields: {sorted(unknown)}')
print(' Valid fields: config_root, log_folder, log_level, rules, engines, ignores, suppressions')
else:
print('Known fields: OK')
# Check engines section
if 'engines' in config and config['engines']:
valid_engines = {'pmd', 'cpd', 'eslint', 'regex', 'retire-js', 'flow', 'sfge', 'apexguru'}
configured_engines = set(config['engines'].keys())
unknown_engines = configured_engines - valid_engines
if unknown_engines:
print(f'WARNING: Unknown engine names: {sorted(unknown_engines)}')
print(f' Valid engines: {sorted(valid_engines)}')
else:
print(f'Engines configured: {sorted(configured_engines)}')
# Check ignores section
if 'ignores' in config and config['ignores']:
if 'files' not in config['ignores']:
print('WARNING: ignores section should contain a \"files\" list')
elif not isinstance(config['ignores']['files'], list):
print('ERROR: ignores.files must be a list of glob patterns')
sys.exit(1)
else:
print(f'Ignore patterns: {len(config[\"ignores\"][\"files\"])} patterns configured')
# Check rules section
if 'rules' in config and config['rules']:
for engine, rules in config['rules'].items():
if rules and isinstance(rules, dict):
for rule_name, overrides in rules.items():
if overrides and isinstance(overrides, dict):
if 'severity' in overrides:
sev = overrides['severity']
valid_sevs = [1, 2, 3, 4, 5, 'Critical', 'High', 'Moderate', 'Low', 'Info']
if sev not in valid_sevs:
print(f'WARNING: rules.{engine}.{rule_name}.severity = {sev} (expected 1-5 or Critical/High/Moderate/Low/Info)')
print('')
" 2>&1
fi
# Run sf code-analyzer config validation (if sf CLI available)
echo ""
echo "Running Code Analyzer validation..."
if command -v sf &> /dev/null; then
if sf plugins --core 2>&1 | grep -qi "code-analyzer"; then
if sf code-analyzer config --config-file "$CONFIG_FILE" > /dev/null 2>&1; then
echo "Code Analyzer validation: PASSED"
echo ""
echo "Configuration is valid and ready to use."
exit 0
else
echo "Code Analyzer validation: FAILED"
echo ""
echo "Running with verbose output:"
sf code-analyzer config --config-file "$CONFIG_FILE" 2>&1 || true
exit 1
fi
else
echo "SKIP: Code Analyzer plugin not installed (cannot run full validation)"
echo "Install: sf plugins install @salesforce/plugin-code-analyzer"
fi
else
echo "SKIP: sf CLI not available (cannot run full validation)"
fi
echo ""
echo "Basic validation passed. Install Code Analyzer for full validation."
exit 0

View File

@ -0,0 +1,164 @@
---
name: managing-cdc-enablement
description: "Use to enable Salesforce Change Data Capture (CDC) on a standard or custom object, configure a custom event channel, set a filter expression, or add enrichment fields. TRIGGER broadly on any of: 'enable CDC', 'enable Change Data Capture', 'turn on CDC', 'subscribe X to change events', 'only emit events for', 'filter change events', 'enrich change events', 'create a custom event channel'; or any mention of CDC, change events, PlatformEventChannel, PlatformEventChannelMember, EnrichedField, ChangeEvents channel, enrichment fields, change event filter; or when the user wants a downstream system to receive Salesforce data changes; or when the user touches .platformEventChannelMember-meta.xml / .platformEventChannel-meta.xml files. SKIP when publishing platform events, Pub/Sub API or REST/SOAP (use building-sf-integrations), or ManagedEventSubscription (out of scope for CDC). Always use this skill for CDC channel-membership metadata."
metadata:
version: "1.0"
---
# Managing Change Data Capture Enablement
Generate the metadata that subscribes Salesforce objects to Change Data Capture: `PlatformEventChannelMember` files for the default `ChangeEvents` channel or a custom channel, and `PlatformEventChannel` files for new custom channels. Covers enrichment fields, filter expressions, and the canonical naming and value formats that the Metadata API actually accepts (which differ from values that appear in many internal test fixtures and code-search hits).
## Scope
- **In scope**: Generating `PlatformEventChannelMember` and `PlatformEventChannel` metadata for CDC. Subscribing standard objects, custom objects, or both. Configuring enrichment fields. Configuring filter expressions. Defining custom data channels.
- **Out of scope**: Publishing custom platform events (PE) — that's a different metadata type (`PlatformEvent`). Pub/Sub API or external Kafka/Bayeux configuration. Pricing/limits guidance — refer the user to the [CDC Developer Guide](https://developer.salesforce.com/docs/atlas.en-us.change_data_capture.meta/change_data_capture/). Programmatic event-bus subscribers in Apex.
---
## Clarifying Questions
Before generating, confirm with the user if not already clear:
- Which entity (or entities) need CDC enablement? Standard, custom, or both?
- Default channel (`ChangeEvents`) or a custom channel? If custom, what's the channel label?
- Any enrichment fields needed? (Lookup IDs that the consumer needs even when they didn't change.)
- Any filter expression needed? (A SOQL-WHERE-clause body that gates which change events emit.)
---
## Required Inputs
Gather or infer before proceeding:
- **Source entity API name(s)** — e.g. `Account`, `Lead`, `Order__c`. The skill internally translates this to the **ChangeEvent entity name** (see Workflow step 2).
- **Channel** — either `ChangeEvents` (default) or the developer name of a custom channel ending in `__chn`.
- **Enrichment fields (optional)** — list of field API names on the source object whose values should be included in every change event.
- **Filter expression (optional)** — a predicate over fields on the change event payload (e.g. `Status__c != null`).
Defaults unless specified:
- Channel: `ChangeEvents` (the default CDC channel — no path prefix).
- Enrichment fields: none.
- Filter expression: none.
If the user provides a clear, complete request, generate immediately without unnecessary back-and-forth.
---
## Workflow
All steps are sequential. Do not skip or reorder.
**Before generating anything, know the only valid CDC metadata types:** CDC is expressed entirely through `PlatformEventChannelMember` (one per subscribed entity) and `PlatformEventChannel` (only for custom channels). Do NOT use `<ChangeDataCapture>`, `.changeDataCapture-meta.xml`, `changeDataCapture/` directories, `EnableChangeDataCapture`, or `ManagedEventSubscription` — these are not in scope for CDC. If you find yourself writing any of them, stop and use a `PlatformEventChannelMember` file instead.
1. **Identify the channel** — if the user names a custom channel, you'll generate a `PlatformEventChannel` file (see step 4). Otherwise use the literal value `ChangeEvents` for the default channel.
2. **Translate source entity to ChangeEvent entity name**`<selectedEntity>` is the **ChangeEvent** type, NOT the source object:
| Source object | `<selectedEntity>` value |
|---|---|
| `Account` | `AccountChangeEvent` |
| `Lead` | `LeadChangeEvent` |
| `Contact` | `ContactChangeEvent` |
| `Order__c` (custom) | `Order__ChangeEvent` |
| `MyThing__c` (custom) | `MyThing__ChangeEvent` |
For standard objects: append `ChangeEvent`. For custom objects: replace the trailing `__c` with `__ChangeEvent` (the double-underscore is preserved).
3. **Generate the channel-member file** — one file per `(entity, channel)` pair. **The filename and fullName always use a SINGLE underscore between the entity stem and `ChangeEvent`** — this is independent of how `selectedEntity` is formatted in the XML body. For custom objects, drop the `__c` from the source name when forming the filename:
| Source object | Filename (and fullName) | `<selectedEntity>` (in XML) |
|---|---|---|
| `Account` | `Account_ChangeEvent.platformEventChannelMember-meta.xml` | `AccountChangeEvent` |
| `Lead` | `Lead_ChangeEvent.platformEventChannelMember-meta.xml` | `LeadChangeEvent` |
| `Order__c` | `Order_ChangeEvent.platformEventChannelMember-meta.xml` (NOT `Order__ChangeEvent`) | `Order__ChangeEvent` |
| `MyThing__c` | `MyThing_ChangeEvent.platformEventChannelMember-meta.xml` (NOT `MyThing__ChangeEvent`) | `MyThing__ChangeEvent` |
The custom-object case is the easiest place to slip — the filename uses single underscore, the `selectedEntity` keeps its double underscore. Read `assets/PlatformEventChannelMember-template.xml` as the structural template.
4. **For a custom channel**, generate a `PlatformEventChannel` file — required if any member references a non-default channel. Derive a DeveloperName from the user's label: strip spaces and non-alphanumeric characters, convert to CamelCase, then **always** append the literal suffix `__chn`. The filename and the channel's `<eventChannel>` reference must use this exact form, otherwise the deploy fails with `Invalid channel name`:
| User says | DeveloperName | Filename |
|---|---|---|
| `Partner Sync` | `PartnerSync__chn` | `PartnerSync__chn.platformEventChannel-meta.xml` (NOT `Partner_Sync...` or `PartnerSync...`) |
| `Order Updates` | `OrderUpdates__chn` | `OrderUpdates__chn.platformEventChannel-meta.xml` |
| `data sync` | `DataSync__chn` | `DataSync__chn.platformEventChannel-meta.xml` |
Members on this channel reference it by the same DeveloperName: `<eventChannel>PartnerSync__chn</eventChannel>`. Read `assets/PlatformEventChannel-template.xml`.
5. **Add enrichment fields** if requested — repeat the `<enrichedFields><name>FIELD_API_NAME</name></enrichedFields>` block for each field. The name must be a **single-hop API name on the source entity** — verified working with: standard lookup IDs (`OwnerId`, `ParentId`), custom lookup fields (`MyLookup__c`), and custom non-relationship fields (`Region__c`, `Status__c`). Relationship traversals like `Owner.Name` or `Parent.Account.Industry` are rejected by deploy with "The selected field, X.Y, isn't valid".
6. **Add a filter expression** if requested — wrap the predicate in `<filterExpression>...</filterExpression>`. The body is a WHERE-clause body without the `WHERE` keyword (e.g. `Status__c != null`, not `WHERE Status__c != null`). For supported operators, field types, and pitfalls, read `references/filter-expressions.md`.
---
## Rules / Constraints
| Constraint | Rationale |
|---|---|
| `<selectedEntity>` is the ChangeEvent type name, not the source object name | The Metadata API binds the member to a ChangeEvent entity — passing `Account` directly fails with "invalid event in selectedEntity". |
| Member fullName uses **single** underscore: `Account_ChangeEvent` | The double-underscore form (`Account__ChangeEvent`) is parsed as `<namespace>__<name>` and rejected: "Cannot create a new component with the namespace: Account". |
| Default channel value is exactly `ChangeEvents` — no path prefix | Older fixtures and some docs show `data/ChangeEvents`; the deploy returns "Unable to find the specified channel" for that value. |
| Enrichment field names are single-hop API names on the source entity | Standard (`OwnerId`), custom lookup (`MyLookup__c`), and custom non-relationship (`Region__c`) all validate. Traversals like `Owner.Name` are rejected: "The selected field, X.Y, isn't valid". |
| `<filterExpression>` body has no `WHERE` keyword | Deploy returns "filter expression has syntax errors: unexpected token: 'WHERE'". |
| Filter cannot reference `IsDeleted` or do relationship traversal (`Owner.Username`) | Deploy rejects with "field is invalid". |
| DateTime fields support **only equality** in filters (`=`, `!=`) — not `<` / `>` | Deploy returns "Only equality operators are supported for this field type or value". Use a named date literal: `LastModifiedDate = TODAY`. |
| Filter RHS must be a literal — no field-to-field comparison | `BillingCity = ShippingCity` returns "unexpected token: 'ShippingCity'". |
| Compound fields (e.g. `BillingAddress`) require dotted component access in filter | `BillingAddress.City = 'X'` deploys; flat `BillingCity` is rejected as "field is invalid"; raw `BillingAddress` is rejected as "has to be used with a component field". Note this is the OPPOSITE of `<enrichedFields>`, which uses flat names. |
| Custom channel filename ends with `__chn` before the meta-xml suffix | Salesforce's MDAPI naming convention; mismatch causes deploy ambiguity. |
| Custom channel XML must include `<channelType>data</channelType>` | Without `data`, the channel is rejected for CDC (other types exist for streaming/event channels). |
| Source custom objects must already exist (or be deployed in the same transaction) | The ChangeEvent entity for `Foo__c` doesn't exist until `Foo__c` does; member deploy fails otherwise. |
| Never generate a `PlatformEventChannel` file for the default `ChangeEvents` channel | The default channel is system-provided. Reference it via `<eventChannel>ChangeEvents</eventChannel>` on members, but only custom (`__chn`) channels need a channel-meta file. |
| `PlatformEventChannelMember` accepts ONLY four elements: `<enrichedFields>`, `<eventChannel>`, `<filterExpression>`, `<selectedEntity>` | Adding `<description>`, `<isActive>`, `<masterLabel>`, or any other element fails XML schema validation: "Element {...} invalid at this location". Stick to the four documented elements. |
| `PlatformEventChannel` accepts ONLY two elements: `<channelType>` and `<label>` | Adding `<masterLabel>`, `<description>`, etc. produces "Element {...}masterLabel invalid at this location in type PlatformEventChannel". Use `<label>`, not `<masterLabel>`. |
| Generated metadata files only — never run `sf project deploy start` from this skill | This skill produces artifacts; deployment is a separate lifecycle concern. |
---
## Gotchas
| Issue | Resolution |
|---|---|
| `Unable to find the specified channel` | Set `<eventChannel>ChangeEvents</eventChannel>` (no `data/` prefix). |
| `The PlatformEventChannelMember can't be created because it references an invalid event in the "selectedEntity" field` | Use the ChangeEvent name, not the source object: `AccountChangeEvent`, not `Account`. |
| `Cannot create a new component with the namespace: <Object>` | Rename the file to use a single underscore: `Account_ChangeEvent...`, not `Account__ChangeEvent...`. |
| `The selected field, X.Y, isn't valid` (in `<enrichedFields>`) | Replace `Owner.Name` with `OwnerId`. CDC enriches the lookup automatically; only single-hop field API names validate. |
| `filter expression has syntax errors: unexpected token: 'WHERE'` | Remove the `WHERE` keyword. The body is the predicate only. |
| `The BillingCity field in the filter expression is invalid` (or any flat Address component) | Use the compound dotted form: `BillingAddress.City`, not `BillingCity`. See `references/filter-expressions.md` for the full compound-field matrix. |
| Custom-object member fails with "ChangeEvent doesn't exist" | The source object isn't deployed yet. Ensure the `Foo__c` object metadata is in the same deploy or already in the org. |
| `DUPLICATE_VALUE` on second deploy | The member is already subscribed. Either delete first or skip — CDC doesn't support upsert on members directly. |
| `sf infra error (TypeInferenceError, DeployMetadata): Could not infer a metadata type` for a `.changeDataCapture-meta.xml` file | That file extension and metadata type don't exist. Replace the `changeDataCapture/<Entity>.changeDataCapture-meta.xml` file with a `platformEventChannelMembers/<Entity>_ChangeEvent.platformEventChannelMember-meta.xml` file. |
| User says "subscribe Order__c" but means standard `Order` | Confirm — `OrderChangeEvent` (standard) and `Order__ChangeEvent` (custom) are different entities. |
---
## Output Expectations
Deliverables:
- One `force-app/.../platformEventChannelMembers/<Entity>_ChangeEvent.platformEventChannelMember-meta.xml` per subscribed entity.
- One `force-app/.../platformEventChannels/<DevName>__chn.platformEventChannel-meta.xml` per custom channel (if any).
File structure follows the templates in `assets/`.
After receiving the generated files, the user can verify them with `sf project deploy start --dry-run -d <path> --target-org <alias>` before deploying. If a dry-run surfaces an unfamiliar error, `references/deploy-troubleshooting.md` maps the common deploy errors to their metadata-side fixes.
---
## Cross-Skill Integration
| Need | Delegate to |
|---|---|
| Generate the source custom object | `generating-custom-object` skill |
| Generate custom fields referenced by enrichment or filter | `generating-custom-field` skill |
| Build a permission set for users who consume change events | `generating-permission-set` skill |
---
## Reference File Index
| File | When to read |
|---|---|
| `assets/PlatformEventChannelMember-template.xml` | Step 3 — starting structure for a channel member |
| `assets/PlatformEventChannel-template.xml` | Step 4 — starting structure for a custom channel |
| `references/filter-expressions.md` | Step 6 — for the supported operators and field-type matrix when writing a filter expression |
| `references/deploy-troubleshooting.md` | When a user reports a dry-run deploy error and asks for help diagnosing it |

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<PlatformEventChannel xmlns="http://soap.sforce.com/2006/04/metadata">
<channelType>data</channelType>
<label>My Custom Channel</label>
</PlatformEventChannel>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<PlatformEventChannelMember xmlns="http://soap.sforce.com/2006/04/metadata">
<!-- Optional: add one <enrichedFields><name>FIELD_API_NAME</name></enrichedFields> per field. Single-hop API names only (e.g. OwnerId, ParentId, MyLookup__c, Region__c). -->
<!-- Default CDC channel: ChangeEvents (no path prefix). For a custom channel, use its DeveloperName, e.g. PartnerSync__chn. -->
<eventChannel>ChangeEvents</eventChannel>
<!-- Optional: add <filterExpression>YOUR_PREDICATE</filterExpression> if needed (predicate body only, no WHERE keyword). -->
<!-- ChangeEvent entity name. Standard: <Object>ChangeEvent. Custom: replace __c with __ChangeEvent. -->
<selectedEntity>AccountChangeEvent</selectedEntity>
</PlatformEventChannelMember>

View File

@ -0,0 +1,73 @@
# CDC Deploy Troubleshooting
Errors observed during real org dry-runs while authoring this skill, with the metadata-side fix.
## "Unable to find the specified channel"
The `<eventChannel>` value doesn't match a channel known to the org.
- For the default channel, the value must be exactly `ChangeEvents`. NOT `data/ChangeEvents`, NOT `/data/ChangeEvents`, NOT `data/ChangeEvent`.
- For a custom channel, the value must match the channel's DeveloperName **including the `__chn` suffix** — e.g. `PartnerSync__chn`.
- If the custom channel is in the same deploy, ensure it's in the same package directory so MDAPI orders the deploy correctly.
## "...invalid event in the 'selectedEntity' field"
`<selectedEntity>` must be the ChangeEvent entity, not the source object.
| Source | Wrong | Right |
|---|---|---|
| `Account` | `Account` | `AccountChangeEvent` |
| `Lead` | `Lead` | `LeadChangeEvent` |
| `Order__c` | `Order__c` | `Order__ChangeEvent` |
| `Custom__c` | `Custom__c` | `Custom__ChangeEvent` |
For custom objects, swap the trailing `__c` for `__ChangeEvent` — the double underscore separator is preserved.
## "Cannot create a new component with the namespace: <Object>"
The member's fullName has two underscores between the entity and `ChangeEvent`. Salesforce parses `Foo__Bar` as `<namespace>__<name>` and rejects creation outside the org's own namespace.
- Member fullName format: `<Entity>_ChangeEvent` (single underscore).
- File name: `<Entity>_ChangeEvent.platformEventChannelMember-meta.xml`.
This applies even when the source object has `__c` — the member fullName drops the `__c` and uses single underscore: file `Order_ChangeEvent.platformEventChannelMember-meta.xml`, fullName `Order_ChangeEvent`.
## "The selected field, X.Y, isn't valid" (enrichedFields)
Enrichment fields must be **single-hop API names on the source entity**. Field type doesn't matter — standard lookup IDs, custom lookups, and custom non-relationship fields all work. The rejection is specifically for relationship-traversal syntax (`X.Y`, `__r.Y`).
Verified working in dry-run:
| Field | Type | Result |
|---|---|---|
| `OwnerId` | standard lookup ID | ✓ deploys |
| `ParentId` | standard lookup ID | ✓ deploys |
| `MyAccountManager__c` | custom Lookup → User | ✓ deploys |
| `Region__c` | custom Text | ✓ deploys |
| `Status__c` | custom Picklist | ✓ deploys |
Verified rejected:
| Field | Why |
|---|---|
| `Owner.Name` | relationship traversal |
| `Parent.Account.Industry` | multi-hop traversal |
| `MyLookup__r.Name` | relationship traversal (custom) |
When the user says "include the owner's name in every event," the metadata stores `OwnerId` — CDC enriches the lookup automatically and the consumer resolves the related record from the ID.
## "filter expression has syntax errors: unexpected token: 'WHERE'"
The body of `<filterExpression>` is a SOQL WHERE-clause body — without the `WHERE` keyword.
| Wrong | Right |
|---|---|
| `WHERE Status__c != null` | `Status__c != null` |
| `WHERE Industry IN ('Tech', 'Finance')` | `Industry IN ('Tech', 'Finance')` |
## ChangeEvent entity doesn't exist (for custom objects)
`Foo__ChangeEvent` only exists if `Foo__c` exists. If both are being deployed in the same dry-run, the dry-run validator may flag the member because it validates entity references against the *current* org state, not the post-deploy state. Mitigations:
- Deploy the source object first, then the channel member in a second deploy.
- Or accept that dry-run will flag this case but a real (non-dry-run) deploy succeeds because MDAPI orders the components correctly.

View File

@ -0,0 +1,93 @@
# Filter Expression Reference
The `<filterExpression>` body is a SOQL-WHERE-clause-body — predicate only, no `WHERE` keyword. The platform supports a subset of SOQL grammar for CDC filters; this reference documents what the dry-run deploy verifies.
## Operators by field type
| Field type | Supported operators | Notes |
|---|---|---|
| Text | `=`, `!=`, `IN`, `NOT IN`, `LIKE` | `LIKE` accepts `%` wildcard. Quote string literals with single quotes. |
| Number / Currency / Percent | `=`, `!=`, `<`, `<=`, `>`, `>=` | Numeric literals unquoted. |
| Boolean (Checkbox) | `=`, `!=` | Use `true` / `false` literal — no quotes. |
| Date | `=`, `!=` | Named literals (`TODAY`, `THIS_WEEK`, `LAST_N_DAYS:7`) and ISO date strings work. |
| DateTime | `=`, `!=` only | **Range operators (`>`, `<`) are rejected** — "Only equality operators are supported for this field type or value". |
| Reference (lookup ID) | `=`, `!=`, `IN`, `NOT IN` | Use the 18-character ID in single quotes: `OwnerId = '005000000000000AAA'`. |
| Picklist | `=`, `!=`, `IN`, `NOT IN` | Quote the value as a string. |
## Compound expressions
`AND`, `OR`, and parentheses all work. There's no documented limit on nesting depth.
```text
(Industry = 'Technology' OR Industry = 'Finance') AND AnnualRevenue > 0 AND Phone != null
```
## Null checks
```text
Phone = null
Phone != null
```
`null` is a literal — no quotes.
`ISBLANK()` and `ISNULL()` are **not** valid filter operators — they parse-fail with "unexpected token". They work in formulas but not in CDC filter expressions. Use `Field = null` / `Field != null` instead.
## Functions
`LOWER(Name) = 'acme'` deploys successfully. Other SOQL functions probably work, but only `LOWER` has been verified in this skill's dataset. Test before relying on `UPPER`, `CONVERTCURRENCY`, etc.
## What the filter cannot do
| Pattern | Deploy error |
|---|---|
| `WHERE Industry = 'Tech'` | "filter expression has syntax errors: unexpected token: 'WHERE'" — drop the keyword. |
| `IsDeleted = false` | "The IsDeleted field in the filter expression is invalid." Soft-deleted records still emit ChangeEvent; you cannot filter them out via this mechanism. |
| `Owner.Username = 'foo@bar.com'` | "The Owner.Username field in the filter expression is invalid." Relationship traversal works in `<enrichedFields>` rejection messages but NOT in filter expressions either — single-hop only. |
| `LastModifiedDate > LAST_N_DAYS:30` | "Only equality operators are supported for this field type or value." DateTime is equality-only; for "recent changes" semantics, use `LastModifiedDate = LAST_N_DAYS:N` (which compares to the *day*, not the timestamp). |
## Field-to-field comparison: not supported
The right-hand side of a comparison must be a **literal**. Field references are rejected.
| Wrong | Deploy error |
|---|---|
| `BillingCity = ShippingCity` | "syntax errors: unexpected token: 'ShippingCity'" |
| `NumberOfEmployees > AnnualRevenue` | "unexpected token: 'AnnualRevenue'" |
If the user wants "records where two fields differ," that logic must live downstream of the change event consumer, not in the filter.
## Composite (compound) fields like Address: dotted-component is the only valid form
Counterintuitively, the opposite rule applies for composite fields like `BillingAddress` vs. relationship traversals like `Owner.Name`. Compound fields **require** dot-notation; flat component names are rejected.
| Pattern | Result |
|---|---|
| `BillingAddress.City = 'San Francisco'` | ✓ deploys — required form for compound fields |
| `BillingAddress = null` | ✗ "Compound field BillingAddress has to be used with a component field in a filter expression" |
| `BillingCity = 'San Francisco'` (flat component) | ✗ "The BillingCity field in the filter expression is invalid" |
| `BillingState`, `BillingPostalCode`, `BillingCountry`, `BillingLatitude` (flat) | ✗ all rejected as invalid |
So, for the same `BillingAddress` field, the accepted form differs by location:
| Location | `BillingAddress` (compound) | `BillingAddress.City` (dotted) | `BillingCity` (flat component) |
|---|---|---|---|
| `<filterExpression>` | ✗ "has to be used with a component field" | ✓ deploys (required form) | ✗ "field is invalid" |
| `<enrichedFields>` | ✓ deploys | ✗ "isn't valid" (dots never allowed) | ✗ "isn't valid" (components not exposed at this layer) |
Rules per location:
- **Filter expression**: address components are reachable only via dotted form on the compound (`BillingAddress.City`). Both the compound itself (no component) and the flat component name are rejected.
- **Enrichment field**: takes a top-level field API name only. The compound `BillingAddress` is a top-level field and works; dotted forms are never valid here regardless of whether the dot would be a compound-component select or a relationship traversal; flat components like `BillingCity` are not top-level enrichable fields and are rejected.
- **Relationship traversal** (`Owner.Name`, `Parent.Industry`) is rejected in both locations.
The mental model: compound fields are a single physical column with sub-components; relationships are joins. The filter parser supports dotted access into compound sub-components but never into joins. The enrichment list takes top-level field names only and never accepts dots; the platform handles compound-vs-component decomposition itself when emitting the change event payload.
## What's NOT yet verified
- Aggregate functions or subqueries — almost certainly unsupported.
- Operators on Long Text, Rich Text, Encrypted, Geolocation field types.
- Filter on a custom field that doesn't exist (deploy-time error vs runtime).
- Custom-object-specific picklist with locale-sensitive values.
If the user requests one of these and the deploy succeeds — update this file. If it fails — capture the error here.

View File

@ -1,6 +1,6 @@
---
name: running-code-analyzer
description: "Run Salesforce Code Analyzer to scan code for security, performance, best practice, and code style violations. Supports all engines (PMD, ESLint, CPD, RetireJS, Flow, SFGE, ApexGuru), targets (files, folders, git diff), categories, and severities. TRIGGER when: user says 'scan my code', 'check for security issues', 'run PMD/ESLint', 'find duplicates', 'analyze Flows', 'check vulnerable libraries', 'AppExchange review', 'lint my LWC', 'static analysis', 'code quality', or mentions engines/file types (.cls, .trigger, .js, .flow-meta.xml). DO NOT TRIGGER when: user wants to fix code without scanning, or asks about installation/configuration."
description: "Run Salesforce Code Analyzer to scan code for security, performance, best practice, and code style violations. Supports all engines (PMD, ESLint, CPD, RetireJS, Flow, SFGE, ApexGuru), targets (files, folders, git diff), categories, and severities. Also handles post-scan exploration: filtering results by engine/severity/category/file, and explaining what specific rules mean. TRIGGER when: user says 'scan my code', 'check for security issues', 'run PMD/ESLint', 'find duplicates', 'analyze Flows', 'check vulnerable libraries', 'AppExchange review', 'lint my LWC', 'static analysis', 'code quality', 'show only security violations', 'what is this rule', 'explain ApexCRUDViolation', 'filter results', or mentions engines/file types (.cls, .trigger, .js, .flow-meta.xml). Use this skill for scanning, exploring results, understanding rules, and listing available rules. DO NOT TRIGGER when: user wants to fix code without scanning, or asks ONLY about installation/configuration."
allowed-tools: Read, Bash(sf code-analyzer), Bash(node), Bash(git diff), Bash(date), Write, Edit
metadata:
version: "1.0"
@ -9,284 +9,233 @@ metadata:
# Running Code Analyzer Skill
## ⚠️ CRITICAL: Tool Selection
## ⚠️ CRITICAL: Mandatory Script Usage
**BEFORE DOING ANYTHING ELSE:**
Every interaction with Code Analyzer results MUST go through the bundled scripts in `<skill_dir>/scripts/`. No exceptions.
This skill MUST use the **Bash tool** to execute `sf code-analyzer run` and Node.js scripts.
### ❌ WRONG — never do this:
**DO NOT use these tools under any circumstances:**
- ❌ `run_code_analyzer` (MCP tool)
- ❌ `mcp__*` (any MCP tool)
- ❌ Any tool containing `mcp` in its name
```bash
# WRONG: inline Python to parse results
python3 -c "import json; data = json.load(open('results.json'))..."
If you see a `run_code_analyzer` tool available, **ignore it completely**. Use only the Bash tool with `sf code-analyzer run`.
# WRONG: inline Node.js to parse results
node -e "const data = require('./results.json')..."
# WRONG: jq to filter results
cat results.json | jq '.violations[] | select(.engine=="pmd")'
# WRONG: reading the results file directly (it can be 10MB+)
Read tool → code-analyzer-results-*.json
```
Also forbidden: `run_code_analyzer` and any `mcp__*` tool — Bash only.
### ✅ RIGHT — always do this:
```bash
# Summarize scan results
node "<skill_dir>/scripts/parse-results.js" "./code-analyzer-results-TIMESTAMP.json"
# Filter/rank/query results (by engine, severity, file, rule, category)
node "<skill_dir>/scripts/query-results.js" "./code-analyzer-results-TIMESTAMP.json" --engine pmd --summary
# List/browse available rules (by engine, category, language, severity)
node "<skill_dir>/scripts/list-rules.js" "Security" --top 10
# Look up what a rule means
node "<skill_dir>/scripts/describe-rule.js" "ApexCRUDViolation" --engine pmd
# Discover fixable violations
node "<skill_dir>/scripts/discover-fixes.js" "./code-analyzer-results-TIMESTAMP.json"
# Apply fixes (after user confirms)
node "<skill_dir>/scripts/apply-fixes.js" "./code-analyzer-results-TIMESTAMP.json"
# Summarize applied fixes
node "<skill_dir>/scripts/summarize-fixes.js" "./code-analyzer-results-TIMESTAMP.json"
# Filter vendor files (jQuery, Bootstrap, *.min.js) before applying fixes
node "<skill_dir>/scripts/filter-violations.js" "./code-analyzer-results-TIMESTAMP.json" "./code-analyzer-results-TIMESTAMP-filtered.json" --report
```
`<skill_dir>` is the absolute path to the directory containing this SKILL.md. **Never** use `./scripts/` — that resolves against the user's CWD, not the skill dir.
Any aggregation, filter, or rank question ("which file has the most violations?", "how many PMD issues?", "top rules by count", "break down by severity") is answered by `query-results.js` — its output already includes `topRules`, `topFiles`, and `severityCounts`.
---
## Overview
This skill translates natural language requests ("scan for security issues", "check my changes") into the correct `sf code-analyzer run` command, executes scans with any combination of engines/targets/severities, and presents actionable results. When engine-provided fixes are available, it discovers them, asks for user confirmation, applies them safely, and offers verification. Use this skill for static analysis, security reviews, AppExchange certification, code quality checks, or finding duplicates/vulnerabilities in Salesforce projects.
This skill translates natural-language requests ("scan for security issues", "check my changes") into the correct `sf code-analyzer run` command, executes scans across any combination of engines/targets/severities, and presents actionable results. When engine-provided fixes are available, it discovers them, asks for user confirmation, applies them safely, and offers verification. Use it for static analysis, security reviews, AppExchange certification, code-quality checks, and finding duplicates/vulnerabilities in Salesforce projects.
**In scope:** running scans, parsing/filtering/ranking results, applying engine auto-fixes, diff-based scans, all output formats (JSON/HTML/SARIF/CSV/XML), describing/listing rules, scan-failure troubleshooting.
**Out of scope:** installing/configuring `sf` or the plugin (→ `configuring-code-analyzer`), writing custom rules/engines, AI-generated fixes beyond engine-provided ones, deep refactoring, CI/CD setup (→ `configuring-code-analyzer`).
**Allowed tools:** Bash (`sf code-analyzer`, `node`, `git diff`, `date`), Read, Write, Edit. **Forbidden:** any MCP tool, Agent tool, web tools, other skills, Python, `jq`, inline scripts/heredocs. This skill owns the complete scan-fix-verify-query-explain workflow end-to-end.
---
## Scope
## Command Syntax Rules (READ FIRST — ABSOLUTE)
**In scope:**
- Running `sf code-analyzer run` with any combination of engines, targets, categories, severities
- Parsing and presenting scan results in actionable format
- Applying engine-provided auto-fixes when available
- Handling diff-based scans (scan only changed files)
- Supporting all output formats (JSON, HTML, SARIF, CSV, XML)
- Troubleshooting scan failures and prerequisite issues
1. The command is **`sf code-analyzer run`** — NOT `sf scanner run` (deprecated v3).
2. **No `--format` flag.** Use `--output-file <path>.<ext>`; the extension determines the format.
3. **Always** pass `--output-file` with a timestamped name (e.g., `./code-analyzer-results-20260512-143022.json`) — do not rely on stdout.
4. **Foreground only** (no `run_in_background`); timeout 1200000ms for large scans.
5. **Invalid v3 flags** that cause errors: `--format`, `--engine`, `--category`, `--json`. Use `--rule-selector` + `--output-file` instead.
6. **Tool restriction:** Bash, Read, Write, Edit only. No MCP tools, no Agent tool, no web tools, no other skills.
**Out of scope:**
- Installing or configuring Salesforce CLI or Code Analyzer plugin (use setup documentation)
- Writing custom Code Analyzer rules or engines (separate skill needed)
- AI-generated code fixes beyond engine-provided deterministic fixes
- Deep code refactoring or architectural changes based on violations
- Setting up CI/CD integration for automated scanning (separate workflow skill)
Why: the v4+ CLI redesigned the flag interface; v3 flags now error.
---
## Command Syntax Rules (READ THIS FIRST)
**The following rules are ABSOLUTE and override any prior knowledge:**
1. **The command is `sf code-analyzer run`** — NOT `sf scanner run` (deprecated v3 command)
2. **There is NO `--format` flag** — use `--output-file <path>.<ext>` instead (extension determines format)
3. **ALWAYS use `--output-file`** to write results to a file — do NOT rely on terminal stdout
4. **ALWAYS include `--output-file`** with a timestamped filename (e.g., `./code-analyzer-results-20260512-143022.json`)
5. **Do NOT run in background** — use foreground with timeout of 1200000ms for large scans
6. **INVALID v3 flags:** `--format`, `--engine`, `--category`, `--json` — these cause errors, use `--rule-selector` and `--output-file` instead
7. **NEVER use MCP tools** — ONLY use the Bash tool to execute `sf code-analyzer run`
8. **Tool restriction:** This skill MUST use ONLY: Read, Bash, Write, Edit tools
9. **Forbidden tools:** Do NOT use any MCP tools (mcp__*), Agent tool, or web tools
10. **Script execution:** ALL scripts MUST be executed via `node <skill_dir>/scripts/*.js` using the Bash tool
**Why:** The v4+ CLI redesigned the flag interface. Old v3 flags cause "unknown flag" errors.
**For complete flag reference and rule selector syntax**, see `<skill_dir>/references/flag-reference.md`.
Full flag/selector docs: `<skill_dir>/references/flag-reference.md`.
---
## Prerequisites
User must have: **Salesforce CLI** (`sf`), **@salesforce/plugin-code-analyzer** (v5.x+), **Java 11+** (PMD/CPD/SFGE), **Node.js 18+** (ESLint/RetireJS), **Python 3** (Flow), **authenticated org** (ApexGuru).
User needs: **Salesforce CLI** (`sf`), **@salesforce/plugin-code-analyzer** (v5.x+), **Java 11+** (PMD/CPD/SFGE), **Node.js 18+** (ESLint/RetireJS), **Python 3** (Flow), **authenticated org** (ApexGuru).
If a scan fails, read `<skill_dir>/references/error-handling.md`. For quick command examples, see `<skill_dir>/references/quick-start.md`.
Pre-flight: run `sf code-analyzer --help 2>&1 | head -1`. If that fails, or if a scan reports an engine startup error (e.g., "PMD failed to start", "java: command not found", "SFGE failed"):
---
1. **Stop** — do not attempt to install/diagnose prerequisites yourself.
2. **Delegate to `configuring-code-analyzer`** — it handles all setup.
3. After it finishes, return here and re-run the scan.
## Tool Usage Rules
**Allowed:** Bash (sf code-analyzer, node, git, date), Read, Write, Edit
**Forbidden:** MCP tools, Agent tool, Web tools, other skills
This skill owns the complete scan-fix-verify workflow. Using MCP tools bypasses the validated script workflow.
If a scan fails for other reasons, see `<skill_dir>/references/error-handling.md`.
---
## Quick Start: Common Patterns
Use this decision tree for fast pattern matching before going to Step 1 detailed parsing:
Match the request below; if it matches, jump to Step 3 (Build Command). Otherwise, walk Step 1.
| User Says | Action | Rule Selector | Notes |
|-----------|--------|---------------|-------|
| "scan my code" / "run code analyzer" | Default scan | `Recommended` | Curated rule set, all file types |
| "check for security issues" / "security review" | Security scan | `all:Security:(1,2)` | All engines, Critical+High only |
| "scan my changes" / "check the diff" | Diff-based scan | Get changed files via `git diff`, filter to scannable types, use `--target` | See Step 1.5 for filtering logic |
| "run PMD" / "check my Apex" | PMD only | `pmd` | Apex classes and triggers |
| "lint my LWC" / "check my JavaScript" | ESLint only | `eslint` | JavaScript/TypeScript/LWC |
| "find duplicates" / "check for copy-paste" | CPD (Copy-Paste Detector) | `cpd` | Detects code clones |
| "check for vulnerabilities" / "scan libraries" | RetireJS | `retire-js` | JavaScript library CVEs |
| "deep analysis" / "data flow analysis" | SFGE (Graph Engine) | `sfge` | Requires Java 11+, 10-20min, use `--workspace "force-app"` |
| "performance analysis" / "governor limits" | ApexGuru | `apexguru` | Requires authenticated org |
| "analyze my Flows" | Flow engine | `flow` | Target: `**/*.flow-meta.xml`, requires Python 3 |
| "AppExchange security review" | AppExchange scan | `all:Security:(1,2)` | Read `<skill_dir>/references/special-behaviors.md` → AppExchange section |
**If the pattern matches above**, proceed directly to Step 3 (Build Command). Otherwise, continue to Step 1 for detailed parsing.
| User Says | Rule Selector | Notes |
|-----------|---------------|-------|
| "scan my code" / "run code analyzer" | `Recommended` | Curated set, all file types |
| "check for security issues" / "security review" | `all:Security:(1,2)` | All engines, Critical+High |
| "scan my changes" / "check the diff" | (see Step 1.5) | Get files via `git diff`, filter to scannable types, pass via `--target` |
| "run PMD" / "check my Apex" | `pmd` | Apex classes and triggers |
| "lint my LWC" / "check my JavaScript" | `eslint` | JavaScript/TypeScript/LWC |
| "find duplicates" / "check for copy-paste" | `cpd` | Code clones |
| "check for vulnerabilities" / "scan libraries" | `retire-js` | JavaScript library CVEs |
| "deep analysis" / "data flow analysis" | `sfge` | Java 11+, 1020 min, use `--workspace "force-app"` |
| "performance analysis" / "governor limits" | `apexguru` | Authenticated org required |
| "analyze my Flows" | `flow` | `--target **/*.flow-meta.xml`, Python 3 |
| "AppExchange security review" | `all:Security:(1,2)` | See `<skill_dir>/references/special-behaviors.md` → AppExchange |
---
## Step 1: Parse the User's Intent
Analyze the user's request along these 7 dimensions. Any can be combined freely:
Analyze the request along these 7 dimensions; any can combine.
### 1.1 ENGINE — Which analysis engine(s)?
### 1.1 ENGINE
PMD/Apex → `pmd` · ESLint/JS/TS/lint → `eslint` · Flows → `flow` · duplicates/CPD → `cpd` · vulnerabilities/CVE/RetireJS → `retire-js` · SFGE/data flow → `sfge` · performance/ApexGuru → `apexguru` · regex → `regex` · everything → `all` · unspecified → `Recommended`.
Map user keywords to `--rule-selector` values:
- PMD / Apex rules → `pmd`
- ESLint / JS/TS rules / lint → `eslint`
- Flows / Flow analysis → `flow`
- duplicates / copy-paste / CPD → `cpd`
- vulnerabilities / CVE / libraries / RetireJS → `retire-js`
- SFGE / data flow / deep analysis → `sfge`
- performance / ApexGuru → `apexguru`
- regex / pattern rules → `regex`
- all engines / everything → `all`
- Not specified / general "scan" → `Recommended` (default)
### 1.2 CATEGORY
security/OWASP → `Security` · performance → `Performance` · best practices → `BestPractices` · style/format → `CodeStyle` · design/complexity → `Design` · bugs → `ErrorProne` · docs → `Documentation`.
### 1.2 CATEGORY — What kind of issues?
### 1.3 SEVERITY
1=Critical · 2=High · 3=Moderate · 4=Low · 5=Info. "critical only" → `1` · "critical+high" → `(1,2)` · "moderate and above" → `(1,2,3)`.
Map user keywords to category tags:
- security / vulnerabilities / OWASP → `Security`
- performance / speed / optimization → `Performance`
- best practices / quality → `BestPractices`
- code style / formatting → `CodeStyle`
- design / complexity → `Design`
- error prone / bugs → `ErrorProne`
- documentation / comments → `Documentation`
### 1.4 SPECIFIC RULE
If the user names a rule (e.g., "ApexCRUDViolation", "no-unused-vars"): `--rule-selector <engine>:<ruleName>`, or just `<ruleName>` if engine is ambiguous.
### 1.3 SEVERITY — How critical?
⚠️ **Partial names:** `--rule-selector` requires the **exact full** rule name (e.g., `@salesforce-ux/slds/no-hardcoded-values-slds2`, not `no-hardcoded-values`). No wildcards. If you are not 100% certain, look it up first — **do not guess**:
```bash
sf code-analyzer rules --rule-selector all 2>&1 | grep -i "USER_KEYWORD"
```
Multiple matches → ask the user which. Zero matches → tell the user nothing matched.
**Severity levels:** 1=Critical (must fix), 2=High (should fix), 3=Moderate (recommended), 4=Low (nice to fix), 5=Info (FYI)
### 1.5 TARGET
specific path → `--target <path>` · glob ("all Apex") → `--target **/*.cls,**/*.trigger` · "my changes"/"diff" → `git diff --name-only [base]...HEAD`, filter to scannable types, pass as `--target` · "LWC" → `--target **/lwc/**` · "Flows" → `--target **/*.flow-meta.xml` · unspecified → omit (entire workspace).
Map user keywords:
- "critical only" / "sev 1" → `1`
- "critical and high" / "sev 1-2" → `(1,2)`
- "moderate and above" / "sev 1-3" → `(1,2,3)`
Diff-filtering details: `<skill_dir>/references/special-behaviors.md`.
### 1.4 SPECIFIC RULE — Named rule?
### 1.6 OUTPUT
**Default JSON.** Only change if the user explicitly asks. Name: `./code-analyzer-results-<YYYYMMDD-HHmmss>.<ext>` via `TIMESTAMP=$(date +%Y%m%d-%H%M%S)`. Formats: `.json` (default), `.html`, `.sarif`, `.csv`, `.xml`.
If the user mentions a specific rule by name (e.g., "ApexCRUDViolation", "no-unused-vars"):
- Map to: `--rule-selector <engine>:<ruleName>`
- If engine is ambiguous, use just the rule name: `--rule-selector <ruleName>`
**⚠️ IMPORTANT — Partial Rule Names:** The `--rule-selector` flag requires the EXACT full rule name (e.g., `@salesforce-ux/slds/no-hardcoded-values-slds2`, not `no-hardcoded-values`). It does NOT support wildcards or partial matches.
**When you are NOT 100% certain of the full rule name:**
- **Do NOT guess** — a wrong name returns 0 results and wastes a scan cycle
- Instead, **look up the rule first** using the `sf code-analyzer rules` command with grep:
```bash
sf code-analyzer rules --rule-selector all 2>&1 | grep -i "USER_KEYWORD"
```
- Extract the full rule name from the output, then use it in your scan command
- If grep returns multiple matches, present them to the user and ask which one they meant
- If grep returns 0 matches, tell the user no rule matched their keyword
### 1.5 TARGET — What files to scan?
Map user keywords:
- Specific file/folder → `--target <path>`
- Glob pattern / "all Apex classes" → `--target **/*.cls,**/*.trigger`
- "my changes" / "diff" → Run `git diff --name-only [base]...HEAD`, filter to scannable types, pass as `--target`
- "LWC" → `--target **/lwc/**`
- "Flows" → `--target **/*.flow-meta.xml`
- Not specified → Entire workspace (omit `--target`)
**For diff filtering details:** See `<skill_dir>/references/special-behaviors.md`.
### 1.6 OUTPUT — What format?
**DEFAULT:** Always JSON. Only change if user EXPLICITLY requests another format.
**Naming:** `./code-analyzer-results-<YYYYMMDD-HHmmss>.<ext>` (timestamp via `TIMESTAMP=$(date +%Y%m%d-%H%M%S)`)
Formats: `.json` (default), `.html` (report), `.sarif` (GitHub/IDE), `.csv` (spreadsheet), `.xml`
### 1.7 COMPARISON — Delta/trend analysis?
Map user keywords:
- "new since main" → `git diff --name-only main...HEAD` → scan those files
- "new since last commit" → `git diff --name-only HEAD~1`
- "compared to develop" → `git diff --name-only develop...HEAD`
### 1.7 COMPARISON / DELTA
"new since main" → `git diff --name-only main...HEAD` → scan those · "since last commit" → `HEAD~1` · "vs develop" → `develop...HEAD`.
---
## Step 2: Build the Rule Selector
**Syntax:** `:` = AND, `,` = OR, `()` = grouping
Syntax: `:` = AND, `,` = OR, `()` = grouping.
**Examples:**
- Engine only: `pmd`
- Engine + category: `pmd:Security`
- Engine + severity: `pmd:2`
- Complex: `(pmd,eslint):Security:(1,2)` = (PMD or ESLint) AND Security AND (sev 1 or 2)
- Complex: `(pmd,eslint):Security:(1,2)` = (PMD or ESLint) AND Security AND sev (1 or 2)
- Specific rule: `pmd:ApexCRUDViolation`
- All rules: `all`
- All: `all`
**More examples:** `<skill_dir>/references/command-examples.md`
More: `<skill_dir>/references/command-examples.md`.
---
## Step 3: Build the Full Command
Generate timestamp: `TIMESTAMP=$(date +%Y%m%d-%H%M%S)`
Build command:
```bash
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
sf code-analyzer run \
--rule-selector <selector> \
--target <targets> \ # optional
--output-file "./code-analyzer-results-${TIMESTAMP}.json" \ # DEFAULT: JSON
--include-fixes \ # always
--workspace <path> # optional
--target <targets> \ # optional
--output-file "./code-analyzer-results-${TIMESTAMP}.json" \ # default JSON
--include-fixes \ # always
--workspace <path> # optional
```
**Key decisions:**
- DEFAULT: timestamped JSON (`.json`). Only change format if user explicitly requests HTML/SARIF/CSV/XML.
- Always include `--include-fixes` (enables Step 6 auto-fix)
- Omit `--target` to scan entire workspace
- For diff-based scans: get files via `git diff --name-only`, filter to scannable types, pass as `--target`
- Default to timestamped JSON; only change format on explicit request.
- Always pass `--include-fixes` (enables Step 6 auto-fix).
- Omit `--target` to scan the whole workspace.
- Diff scans: `git diff --name-only` → filter scannable types → pass as `--target`.
**Special cases:** See `<skill_dir>/references/special-behaviors.md` for SFGE/ApexGuru/AppExchange/diff filtering.
Special cases (SFGE/ApexGuru/AppExchange/diff): `<skill_dir>/references/special-behaviors.md`.
---
## Step 4: Execute the Scan
**⚠️ TOOL REQUIREMENT: Use Bash tool ONLY. DO NOT use run_code_analyzer (MCP tool) or any MCP tool.**
Use the **Bash tool only** — never the `run_code_analyzer` MCP tool.
**Rules:** Foreground only (no `run_in_background`), hardcoded filename (not `$TIMESTAMP`), timeout 1200000ms, no `sleep`, log output to timestamped file.
**Steps:**
1. Generate timestamp: `date +%Y%m%d-%H%M%S` → capture output (e.g., `20260512-143022`) **using Bash tool**
2. Tell user:
1. Generate the timestamp via Bash: `date +%Y%m%d-%H%M%S` → e.g. `20260512-143022`.
2. Tell the user:
```
Starting scan...
Results: ./code-analyzer-results-20260512-143022.json
Log: ./code-analyzer-results-20260512-143022.log
May take several minutes for large codebases.
```
3. Run command with literal timestamp in filename and `tee` to capture log (timeout: 1200000):
⚠️ **IMPORTANT:** Use the Bash tool, NOT the run_code_analyzer MCP tool.
3. Run with the **literal** timestamp baked in (not `$TIMESTAMP`), foreground, timeout 1200000ms, `tee` to a `.log`:
```bash
sf code-analyzer run --rule-selector Recommended --output-file "./code-analyzer-results-20260512-143022.json" --include-fixes 2>&1 | tee "./code-analyzer-results-20260512-143022.log"
sf code-analyzer run --rule-selector Recommended \
--output-file "./code-analyzer-results-20260512-143022.json" \
--include-fixes 2>&1 | tee "./code-analyzer-results-20260512-143022.log"
```
4. After completion: Exit 0 = success. Error output → check both the log file and `<skill_dir>/references/error-handling.md`.
5. IMMEDIATELY parse results (Step 5). Do NOT ask user what they want.
4. Exit 0 = success. On error, read both the log file and `<skill_dir>/references/error-handling.md`.
5. **Immediately** parse results (Step 5) — do not ask the user what to do next.
---
## Step 5: Parse and Present Results
### Parsing Rules:
1. **Execute the parse script using `<skill_dir>`** — see below
2. **NEVER use `jq` to parse results** — jq one-liners WILL fail due to shell quoting issues
3. **Run it IMMEDIATELY after the scan** — do NOT ask the user "what would you like next?"
### Script Execution
All scripts are bundled in the `scripts/` subdirectory of the same directory that contains this SKILL.md file. Use the absolute path to that directory — do NOT use `./scripts/` as that resolves relative to the current working directory, not the skill directory.
Run the parse script straight after the scan — do not pause to ask:
```bash
node <skill_dir>/scripts/parse-results.js "./code-analyzer-results-TIMESTAMP.json"
node "<skill_dir>/scripts/parse-results.js" "./code-analyzer-results-TIMESTAMP.json"
```
⚠️ **DO NOT:**
- ❌ Invent or generate script code yourself
- ❌ Use bare relative paths like `node scripts/parse-results.js` (won't resolve from user's CWD)
- ❌ Use heredocs or inline script content
- ❌ Use `jq` as a substitute for the parse script
- ❌ Use `jq` as a substitute for the parse script (shell quoting will break)
- ❌ Read the JSON file directly
### How to Present Results:
**ALWAYS present a concise summary, then point to the output file for full details.**
### Presentation template
```
## Scan Complete
@ -305,44 +254,30 @@ node <skill_dir>/scripts/parse-results.js "./code-analyzer-results-TIMESTAMP.jso
| # | Rule | Engine | Sev | File | Line |
|---|------|--------|-----|------|------|
| 1 | ApexCRUDViolation | pmd | 2 | AccountService.cls | 42 |
| 2 | ApexSOQLInjection | pmd | 1 | QueryHelper.cls | 18 |
| ... (show up to 10 most critical) |
| ... up to 10 most critical |
### Top Rules by Frequency
| Rule | Engine | Count |
|------|--------|-------|
| no-var | eslint | 170 |
| ApexDoc | pmd | 165 |
| ... |
Full results: `./code-analyzer-results-20260512-143022.json`
```
### Result Presentation Rules:
Scale to result size: **0** → "no violations found"; **110** → all in one table; **1150** → severity counts + top 10; **505000** → counts + top 10 violations + top 10 rules + top 5 files; **5000+** → same, plus suggest narrowing scope (severity/category/folder). Always end with the output path and offer next actions: filter / explain rule / apply fixes.
- **0 violations**: "Scan complete — no violations found! Output: `<path>`"
- **1-10**: Show all violations in table
- **11-50**: Show severity counts + top 10 violations
- **50-5000**: Show counts + top 10 violations + top 10 rules + top 5 files
- **5000+**: Same as 50-5000, plus suggest narrowing scope (severity/category/folder)
**Always end with:** Output file path + next-action offers (explain rules / apply fixes)
**For large result sets:** See `<skill_dir>/references/special-behaviors.md`.
Large-result handling: `<skill_dir>/references/special-behaviors.md`.
---
## Step 6: Apply Engine-Provided Fixes (Post-Scan)
After presenting results, check if violations have **engine-provided fixes** (deterministic, not AI-generated).
Engine-provided fixes are **deterministic** (not AI-generated). Flow: vendor filter (if needed) → discover → present → **wait for user confirmation** → apply → summarize.
**Rules:** NEVER apply without confirmation. Use EXACT scripts from `<skill_dir>/scripts/`. Filter vendor files if needed, then: Discover → Apply → Summarize.
### 6.1 Vendor file filter (when needed)
**Flow:** Filter vendor (6.1 if needed) → discover (6.2) → present (6.3) → ASK user → apply (6.4) → summarize (6.5) → present results.
### 6.1 — Check for vendor files (if needed)
If user said "fix my code" or "project source", or if top files by violation count are vendor libraries (jQuery, Bootstrap, *.min.js), run:
Run if the user said "fix my code" / "project source", or if top-violation files are vendor libs (jQuery, Bootstrap, `*.min.js`):
```bash
node "<skill_dir>/scripts/filter-violations.js" \
@ -351,31 +286,23 @@ node "<skill_dir>/scripts/filter-violations.js" \
--report
```
Present: "Excluded X vendor files (Y violations) - jQuery, Bootstrap, etc. Applying fixes to Z project files only."
Report: "Excluded X vendor files (Y violations) — jQuery, Bootstrap, etc. Applying fixes to Z project files only." Use the filtered file in 6.2+. Detection logic: `<skill_dir>/references/vendor-file-handling.md`.
Use filtered file for Step 6.3+. **See:** `<skill_dir>/references/vendor-file-handling.md` for detailed logic.
### 6.2 — Discover fixable violations
### 6.2 Discover
```bash
node "<skill_dir>/scripts/discover-fixes.js" "./code-analyzer-results-TIMESTAMP.json"
```
(Use filtered file from Step 6.1 if created.)
### 6.3 — Present fixable violations and ASK for confirmation
After running the discovery script, present results:
### 6.3 Present + ASK (then STOP)
```
### Engine-Provided Fixes Available
**X of Y violations** have auto-fixes provided by the analysis engine:
| Rule | Engine | Sev | Fixable Count |
|------|--------|-----|---------------|
| no-var | eslint | 3 | 170 |
| no-hardcoded-values-slds2 | eslint | 4 | 76 |
| ... |
These are safe, deterministic fixes generated by the engines (not AI-generated).
@ -383,39 +310,30 @@ These are safe, deterministic fixes generated by the engines (not AI-generated).
Would you like me to apply these fixes? (yes / no / select specific rules)
```
### ⚠️ STOP HERE AND WAIT FOR USER RESPONSE.
⚠️ **Stop and wait for the user's reply, even if they originally said "scan and fix everything".** Apply only on a fresh "yes" / "apply" / "go ahead" in the next turn.
**Even if the user originally said "scan and fix everything", you MUST still stop here and wait.** Present the table, ask the question, and WAIT for a response in the NEXT turn.
### 6.4 — Apply fixes ONLY after user confirms
**Only proceed after user says "yes", "apply", "go ahead" IN A SEPARATE RESPONSE.**
### 6.4 Apply
```bash
node "<skill_dir>/scripts/apply-fixes.js" "./code-analyzer-results-TIMESTAMP.json"
```
(Filtered file if 6.1 created one.)
(Use filtered file if Step 6.1 created one.)
### 6.5 — After applying, ALWAYS run the summary script
⚠️ **MANDATORY**: After the apply script completes, you MUST run the summary script as your VERY NEXT action.
### 6.5 Summarize (MANDATORY immediately after 6.4)
```bash
node "<skill_dir>/scripts/summarize-fixes.js" "./code-analyzer-results-TIMESTAMP.json"
```
Then present to the user:
Then present:
```
### Engine-Provided Fixes Applied Successfully ✓
**Applied X auto-fixes across Y files.**
| Severity | Fixes Applied |
|----------|---------------|
| Critical (1) | X |
| High (2) | X |
| ... |
| Rule | Fixes Applied |
@ -426,56 +344,130 @@ Then present to the user:
Want me to re-run the scan to verify the fixes resolved the violations?
```
### 6.6 — If user declines: Skip. If selects rules: filter. If "all": run as-is.
### 6.6 — Handling the user's choice
- **Decline / "no":** skip apply, skip summarize. Do not re-scan.
- **"Select rules":** filter the discovery list to those rules and pass the filtered file to `apply-fixes.js`.
- **"All" / "yes":** run `apply-fixes.js` against the full (or vendor-filtered) results file as-is.
### 6.7 — Re-scan (optional): Re-run with new timestamp, compare before/after counts.
---
## Rules / Constraints
| Constraint | Rationale |
|-----------|-----------|
| Timestamped output (JSON + log) | Prevents overwrite; enables history tracking |
| Use `tee` for logs | Keeps logs in working dir with matching timestamp |
| Never use `--format` flag | Removed in v4+; use `--output-file <path>.<ext>` instead |
| Foreground scans, 1200000ms timeout | SFGE takes 10-20min; backgrounding loses output |
| Execute scripts from `<skill_dir>/scripts/` | Never write inline scripts or heredocs |
| Never apply fixes without confirmation | User must explicitly approve code modifications |
| Check for vendor files before fixes | If 50%+ vendor (jQuery, Bootstrap), filter first |
| Run fix scripts in order | Filter (if needed) → Discover → Apply → Summarize |
| SFGE needs explicit `--workspace` | Prevents template file compilation errors |
| Look up partial rule names first | Guessing fails; use `sf code-analyzer rules` to find exact name |
| ONLY Bash tool, never MCP | run_code_analyzer MCP tool bypasses script workflow |
| Never invoke other skills for fixes | This skill owns complete workflow end-to-end |
### 6.7 — Optional re-scan for verification
If the user accepts the offer in 6.5, re-run the same scan with a **new timestamp** (do not overwrite the original). Compare violation counts before vs. after and show the delta — fixes that resolved cleanly will drop out; remaining violations either need manual remediation or are unrelated.
---
## Gotchas
## Step 7: Query and Filter Existing Results
| Issue | Why It Happens | Solution |
|-------|---------------|----------|
| `--format` flag error | Removed in v4+ | Use `--output-file <path>.<ext>` |
| Scan returns 0 results | Invalid rule selector | Run `sf code-analyzer rules --rule-selector <selector>` to verify |
| SFGE compilation error | Template files in workspace | Set `--workspace "force-app"` |
| jq parsing fails | Shell quoting issues | Use `node "<skill_dir>/scripts/parse-results.js"` |
| Inline scripts written | LLM generates custom code | NEVER write scripts — use existing from <skill_dir>/scripts/ |
| Scan times out | Large SFGE | Increase timeout to 1200000ms |
| run_code_analyzer MCP used | LLM prefers MCP over Bash | Use Bash tool ONLY |
| Other skills invoked | LLM delegates to other skills | Use apply-fixes.js from this skill only |
| Most violations are vendor | Includes jQuery, Bootstrap, *.min.js | Run filter-violations.js before applying fixes |
After Step 5, the user may want to **drill into specific subsets** without re-running the entire scan. This step handles all result-exploration requests.
### When to trigger
Activate when the user asks to slice, filter, rank, or explore existing results:
- "Show me just the security violations"
- "What's in AccountService.cls?"
- "Show only PMD issues" / "Filter to critical and high"
- "What ESLint rules fired?" / "Show violations in the lwc folder"
- "Top 20 most severe" / "Which file has the most violations?"
- "What are the most common rules?" / "How many violations per engine?" / "Break it down by severity"
**Important:** Any question about existing scan results — filtering, ranking, counting, aggregating — MUST use `query-results.js`. NEVER write inline Python, `jq`, or ad-hoc scripts to parse the results JSON. The query script already provides `topRules`, `topFiles`, and `severityCounts` in its output.
### How to execute
Run the query script against the **same results file** from Step 4 (no re-scan needed):
```bash
node "<skill_dir>/scripts/query-results.js" "./code-analyzer-results-TIMESTAMP.json" [options]
```
| User says | Options |
|-----------|---------|
| "security violations" | `--category Security` |
| "PMD issues only" | `--engine pmd` |
| "critical and high" / "sev 1-2" | `--severity 1,2` |
| "in AccountService.cls" | `--file AccountService.cls` |
| "the ApexCRUDViolation rule" | `--rule ApexCRUDViolation` |
| "top 20" | `--top 20` |
| "sort by file" | `--sort file` |
| "just give me counts" | `--summary` |
| "which file has the most violations?" | `--sort file --summary` (read `topFiles`) |
| "which file has most PMD violations?" | `--engine pmd --summary` (read `topFiles`) |
| "most common rules?" | `--summary` (read `topRules`) |
| "how many per engine?" | use Step 5's summary, or run with `--engine X --summary` per engine |
| Combinations | `--engine pmd --severity 1,2 --top 5` |
Output format and presentation templates: `<skill_dir>/references/post-scan-workflows.md`.
---
## Output Expectations
## Step 8: Describe a Rule
Every scan produces: timestamped JSON file, concise summary (severity/top violations/rules/files), next-action offers. If fixes applied: summary by severity/rule, offer verification.
When the user asks "what does this rule mean?" or "how do I fix this?", use this step to look up and explain a specific rule.
### When to trigger
- "What is ApexCRUDViolation?"
- "Explain this rule" / "Why is this flagged?"
- "What does no-var mean?"
- "How do I fix OperationWithLimitsInLoop?"
- "Tell me about this violation"
### How to execute
```bash
node "<skill_dir>/scripts/describe-rule.js" "<rule-name>" [--engine <engine>]
```
Pass `--engine` when known (from scan context); omit for a broader search. Returns one of `success` / `multiple_matches` / `not_found`. Status handling and templates: `<skill_dir>/references/post-scan-workflows.md`.
---
## Step 9: List Available Rules
Triggers: "what security rules are available?", "list all PMD rules", "rules for JavaScript", "Recommended rules", "how many ESLint rules?", "rules for Apex".
```bash
node "<skill_dir>/scripts/list-rules.js" "<selector>" [options]
```
| User says | Selector | Options |
|-----------|----------|---------|
| "security rules" | `Security` | |
| "PMD rules" | `pmd` | |
| "ESLint security rules" | `eslint:Security` | |
| "JavaScript rules" | `JavaScript` | |
| "Apex rules" | `Apex` | |
| "Recommended rules" | `Recommended` | |
| "high severity rules" | `(1,2)` | |
| "just give me counts" | `Recommended` | `--count-only` |
| "top 10 security rules" | `Security` | `--top 10` |
Filters: `--engine`, `--severity`, `--top` (default 100), `--count-only`. The script pre-validates selector tokens (catches typos like `secruity`) before calling the CLI. Presentation: `<skill_dir>/references/post-scan-workflows.md`.
---
## Constraints & Gotchas
| Item | Why / Fix |
|------|-----------|
| Use timestamped JSON + `.log` via `tee` | Prevents overwrite; matches log to results |
| `--format` flag | Removed in v4+; use `--output-file <path>.<ext>` |
| Foreground, 1200000ms timeout | SFGE can take 1020 min; backgrounding loses output |
| Run scripts with absolute `<skill_dir>` path | `./scripts/` resolves against the user's CWD, not the skill dir |
| Never apply fixes without confirmation | User must approve code modifications |
| Vendor file check before fixes | If 50%+ vendor (jQuery/Bootstrap/`*.min.js`), filter first |
| Fix-script order: filter (if needed) → discover → apply → summarize | Skipping summary leaves the user without an outcome report |
| SFGE needs explicit `--workspace` | Otherwise template files cause compilation errors |
| Look up partial rule names first | Guessing returns 0 results; use `sf code-analyzer rules` |
| `ONLY` Bash tool, never MCP | `run_code_analyzer` and other MCP tools bypass the script workflow |
| Never invoke other skills for fixes | This skill owns the full workflow end-to-end |
| Query existing results, don't re-scan | Step 7 filters existing JSON instantly |
| Scan returns 0 results | Invalid rule selector — verify with `sf code-analyzer rules --rule-selector <selector>` |
| `jq` parsing fails | Shell quoting — use `parse-results.js` / `query-results.js` instead |
| Inline scripts written by LLM | Never write scripts — use existing ones in `<skill_dir>/scripts/` |
| Ranking/aggregation answered by ad-hoc Python | Always use `query-results.js`; output already has `topFiles`/`topRules`/`severityCounts` |
---
## Reference File Index
`<skill_dir>` is the absolute path to the directory containing this SKILL.md file.
**Scripts** (always execute via `node` with the absolute `<skill_dir>/` prefix, never Read):
### Scripts (Always execute, never read)
| File | When to use |
|------|-------------|
| `<skill_dir>/scripts/parse-results.js` | Step 5 — extract summary from scan JSON |
@ -483,16 +475,21 @@ Every scan produces: timestamped JSON file, concise summary (severity/top violat
| `<skill_dir>/scripts/discover-fixes.js` | Step 6.2 — identify fixable violations |
| `<skill_dir>/scripts/apply-fixes.js` | Step 6.4 — apply engine fixes after user confirms |
| `<skill_dir>/scripts/summarize-fixes.js` | Step 6.5 — summarize applied changes |
| `<skill_dir>/scripts/query-results.js` | Step 7 — filter/drill into existing results without re-scanning |
| `<skill_dir>/scripts/describe-rule.js` | Step 8 — look up rule description and documentation |
| `<skill_dir>/scripts/list-rules.js` | Step 9 — list/browse available rules by selector with validation |
**References** (read on demand):
### References (Read when needed)
| File | When to read |
|------|-------------|
| `<skill_dir>/references/quick-start.md` | Command syntax templates |
| `<skill_dir>/references/flag-reference.md` | Flag docs, rule selector syntax |
| `<skill_dir>/references/error-handling.md` | Scan failure diagnosis |
| `<skill_dir>/references/engine-reference.md` | Engine capabilities, file types, rule tags |
| `<skill_dir>/references/command-examples.md` | Uncommon command scenarios |
| `<skill_dir>/references/special-behaviors.md` | SFGE/ApexGuru/AppExchange/diff/large scans |
| `<skill_dir>/references/vendor-file-handling.md` | Vendor file detection and filtering logic |
|------|--------------|
| `references/quick-start.md` | Command-syntax templates |
| `references/flag-reference.md` | Full flag docs, rule-selector syntax |
| `references/error-handling.md` | Scan-failure diagnosis |
| `references/engine-reference.md` | Engine capabilities, file types, rule tags |
| `references/command-examples.md` | Less-common command scenarios |
| `references/special-behaviors.md` | SFGE/ApexGuru/AppExchange/diff/large scans |
| `references/vendor-file-handling.md` | Vendor-file detection and filtering |
| `references/post-scan-workflows.md` | Steps 79 — querying, rule description, rule listing |
Examples in `<skill_dir>/examples/` show output structure validation and command patterns (basic/large/security scans, fix workflows).
`examples/` contains output-structure validation and command patterns (basic/large/security scans, fix workflows).

View File

@ -0,0 +1,286 @@
# Post-Scan Workflows
After presenting initial scan results (Step 5), the user may ask follow-up questions to explore results or understand violations. This reference covers three post-scan workflows:
1. **Result Querying** — filter/drill into existing results without re-scanning
2. **Rule Description** — explain what a rule does and how to fix violations
3. **Rule Listing** — browse available rules without running a scan
---
## Result Querying (Step 7)
### When to Use
Trigger this workflow when the user asks to explore existing results:
- "Show me just the security violations"
- "What's in AccountService.cls?"
- "Show only PMD issues"
- "Filter to severity 1 and 2"
- "What ESLint rules fired?"
- "Show violations in the lwc folder"
- "Top 20 by file"
### How It Works
The `query-results.js` script re-filters the SAME results JSON file (from Step 4) with different criteria. No re-scan is needed — it is instant.
### Script Reference
```bash
node "<skill_dir>/scripts/query-results.js" "<results-file.json>" [options]
```
**Filter options (combine any):**
| Option | Description | Example |
|--------|-------------|---------|
| `--engine <name>` | Filter by engine | `--engine pmd` |
| `--severity <n>` | Filter by severity (comma-separated) | `--severity 1,2` |
| `--category <tag>` | Filter by category/tag | `--category Security` |
| `--rule <name>` | Filter by exact rule name | `--rule ApexCRUDViolation` |
| `--file <substring>` | Filter by file path substring | `--file AccountService` |
| `--top <n>` | Return top N results (default: 10) | `--top 20` |
| `--sort <field>` | Sort by: severity, rule, engine, file | `--sort file` |
| `--sort-dir <dir>` | Sort direction: asc, desc | `--sort-dir desc` |
| `--summary` | Show only counts (no individual violations) | `--summary` |
**Options can be combined freely:**
```bash
# Security violations in PMD, top 5
node "<skill_dir>/scripts/query-results.js" "./results.json" --engine pmd --category Security --top 5
# All Critical+High in a specific file
node "<skill_dir>/scripts/query-results.js" "./results.json" --severity 1,2 --file AccountService.cls
# Summary of ESLint issues only
node "<skill_dir>/scripts/query-results.js" "./results.json" --engine eslint --summary
```
### Output Format
The script outputs JSON with this structure:
```json
{
"query": { "engine": "pmd", "severity": [1,2], ... },
"totalViolations": 500,
"totalMatches": 23,
"severityCounts": { "1": 5, "2": 18, "3": 0, "4": 0, "5": 0 },
"topRules": [{ "rule": "ApexCRUDViolation", "engine": "pmd", "count": 12 }, ...],
"topFiles": [{ "file": "AccountService.cls", "count": 8 }, ...],
"violations": [
{ "rule": "...", "engine": "...", "severity": 1, "message": "...", "file": "...", "startLine": 42, "tags": [...] },
...
]
}
```
When `--summary` is used, the `violations` array is omitted.
### Presentation Rules
Present query results using the same format as Step 5, but with a header indicating the active filter:
```
## Filtered Results: [description of filter]
**X matches** out of Y total violations.
| Severity | Count |
|----------|-------|
| Critical (1) | X |
| High (2) | X |
| ... |
### Matching Violations
| # | Rule | Engine | Sev | File | Line |
|---|------|--------|-----|------|------|
| 1 | ... | ... | ... | ... | ... |
### Top Rules (within filter)
| Rule | Engine | Count |
|------|--------|-------|
| ... | ... | ... |
Full results: `<original-results-file>`
```
### Follow-Up Offers
After presenting filtered results, offer:
- "Want me to narrow further?" (add more filters)
- "Want me to explain any of these rules?" (→ Step 8)
- "Want me to apply fixes for these?" (→ Step 6, scoped to matched rules)
---
## Rule Description (Step 8)
### When to Use
Trigger this workflow when the user asks about a specific rule:
- "What is ApexCRUDViolation?"
- "Explain this rule"
- "What does no-var mean?"
- "How do I fix OperationWithLimitsInLoop?"
- "Tell me about the ApexSOQLInjection rule"
- "Why is this flagged?"
### How It Works
The `describe-rule.js` script calls `sf code-analyzer rules` with a targeted selector to extract rule metadata including description and documentation links.
### Script Reference
```bash
node "<skill_dir>/scripts/describe-rule.js" "<rule-name>" [--engine <engine>]
```
**Arguments:**
| Argument | Description | Example |
|----------|-------------|---------|
| `<rule-name>` | The rule name to look up | `ApexCRUDViolation` |
| `--engine <engine>` | Narrow to a specific engine (optional) | `--engine pmd` |
**Examples:**
```bash
node "<skill_dir>/scripts/describe-rule.js" "ApexCRUDViolation" --engine pmd
node "<skill_dir>/scripts/describe-rule.js" "no-var" --engine eslint
node "<skill_dir>/scripts/describe-rule.js" "OperationWithLimitsInLoop"
```
### Output Format
**Success — single rule found:**
```json
{
"status": "success",
"rule": {
"name": "ApexCRUDViolation",
"engine": "pmd",
"severity": "2 (High)",
"tags": ["Security", "Recommended", "Apex"],
"description": "Validates that CRUD and FLS checks are performed before DML operations...",
"resources": ["https://pmd.github.io/latest/pmd_rules_apex_security.html#apexcrudviolation"]
}
}
```
**Multiple matches (partial name):**
```json
{
"status": "multiple_matches",
"message": "Rule \"CRUD\" not found as exact match. Found 3 potential matches:",
"candidates": [
{ "name": "ApexCRUDViolation", "engine": "pmd", "severity": "2", "tags": "Security, Recommended" },
...
]
}
```
**Not found:**
```json
{
"status": "not_found",
"message": "Rule \"FakeRule\" not found. Verify the rule name with: sf code-analyzer rules ..."
}
```
### Presentation Rules
**For a successful lookup**, present:
```
## Rule: ApexCRUDViolation
| Property | Value |
|----------|-------|
| Engine | pmd |
| Severity | 2 (High) |
| Tags | Security, Recommended, Apex |
### Description
Validates that CRUD and FLS checks are performed before DML operations. Without these
checks, data may be accessed or modified without proper user permissions, violating
the Salesforce security model.
### How to Fix
[Provide actionable fix guidance based on the description. If the description mentions
a fix pattern, elaborate. If resources are available, include the link.]
### Resources
- [PMD Documentation](https://pmd.github.io/...)
---
Want me to show all violations of this rule in your scan results?
```
**For multiple matches**, present:
```
I found multiple rules matching "CRUD":
| # | Rule | Engine | Severity |
|---|------|--------|----------|
| 1 | ApexCRUDViolation | pmd | 2 (High) |
| 2 | ... | ... | ... |
Which rule would you like details on?
```
**For not found**, present:
```
I couldn't find a rule named "FakeRule". Would you like me to:
- Search for similar rules? (I'll grep the full rule list)
- List all rules for a specific engine or category?
```
### After Describing a Rule
Offer next steps:
- "Want me to show all violations of this rule in your results?" (→ Step 7 with `--rule`)
- "Want me to apply the engine fix for this rule?" (→ Step 6)
- "Want me to explain another rule?"
---
## Rule Listing (Step 9)
### Presentation Rules
Present available rules in this format:
```
## Available Rules: Security
**Found X rules** across Y engines.
| Engine | Count |
|--------|-------|
| pmd | 12 |
| eslint | 6 |
| Severity | Count |
|----------|-------|
| Critical (1) | 3 |
| High (2) | 15 |
### Rules (top 25)
| # | Rule | Engine | Severity | Tags |
|---|------|--------|----------|------|
| 1 | ApexCRUDViolation | pmd | 2 (High) | Security, Recommended |
| 2 | ApexSOQLInjection | pmd | 1 (Critical) | Security, Recommended |
| ... |
Want me to explain any of these rules? Or run a scan with this selector?
```
### Follow-Up Offers
After listing rules:
- "Want me to explain any of these?" (→ Step 8)
- "Want me to scan with this selector?" (→ Steps 1-5 with the same selector)
- "Narrow to just high severity?" (re-run with `--severity 1,2`)

View File

@ -0,0 +1,382 @@
#!/usr/bin/env node
// Version: v1.1 | SHA256: placeholder
// Get detailed description and documentation for a Code Analyzer rule
// Usage: node describe-rule.js <rule-name> [--engine <engine>]
//
// This script runs `sf code-analyzer rules --view detail` with a targeted
// selector and parses the output to extract rule details including description,
// severity, tags, and documentation resources.
//
// The CLI output format is:
// === 1. RuleName
// severity: 2 (High)
// engine: pmd
// tags: Recommended, Security, Apex
// resource: https://...
// description: Some description text
const { execSync } = require("child_process");
function printUsage() {
console.error(`Usage: node describe-rule.js <rule-name> [--engine <engine>]
Arguments:
<rule-name> The rule name to look up (case-insensitive partial match)
Options:
--engine <engine> Narrow lookup to a specific engine (pmd, eslint, cpd, etc.)
Examples:
node describe-rule.js ApexCRUDViolation
node describe-rule.js ApexCRUDViolation --engine pmd
node describe-rule.js no-var --engine eslint
node describe-rule.js OperationWithLimitsInLoop`);
process.exit(1);
}
// Parse CLI arguments
const args = process.argv.slice(2);
if (args.length < 1 || args[0] === "--help" || args[0] === "-h") {
printUsage();
}
const ruleName = args[0];
let engine = null;
for (let i = 1; i < args.length; i++) {
if (args[i] === "--engine" && args[i + 1]) {
engine = args[++i].toLowerCase();
}
}
// Build the rule selector for the lookup
const selector = engine ? `${engine}:${ruleName}` : ruleName;
// Run `sf code-analyzer rules` with --view detail to get full rule info
let rawOutput;
try {
const cmd = `sf code-analyzer rules --rule-selector "${selector}" --view detail 2>&1`;
rawOutput = execSync(cmd, {
encoding: "utf8",
timeout: 60000,
maxBuffer: 2 * 1024 * 1024,
});
} catch (err) {
// execSync throws on non-zero exit, but we still want the output
rawOutput = err.stdout || err.stderr || (err.output && err.output.join("")) || "";
if (!rawOutput) {
console.log(JSON.stringify({
status: "error",
message: `Failed to run sf code-analyzer rules: ${err.message}`,
}));
process.exit(0);
}
}
// Parse the detail view output
// Format: === N. RuleName\n key: value\n key: value\n
const rules = parseDetailOutput(rawOutput);
if (rules.length === 0) {
// Try grep fallback for partial/substring match
const grepResult = tryGrepFallback(ruleName, engine);
if (grepResult) {
console.log(JSON.stringify(grepResult));
process.exit(0);
}
// Try fuzzy match as final fallback (catches typos like "Violtion" → "Violation")
const fuzzyResult = tryFuzzyFallback(ruleName, engine);
if (fuzzyResult) {
console.log(JSON.stringify(fuzzyResult));
process.exit(0);
}
console.log(JSON.stringify({
status: "not_found",
message: `Rule "${ruleName}" not found${engine ? ` in engine "${engine}"` : ""}. Verify the rule name with: sf code-analyzer rules --rule-selector ${engine || "all"} 2>&1 | grep -i "${ruleName}"`,
}));
process.exit(0);
}
// Find exact match (case-insensitive)
const exactMatch = rules.find(
(r) => r.name.toLowerCase() === ruleName.toLowerCase()
);
if (exactMatch) {
console.log(JSON.stringify({
status: "success",
rule: exactMatch,
}));
} else if (rules.length === 1) {
// Single result, use it
console.log(JSON.stringify({
status: "success",
rule: rules[0],
}));
} else {
// Multiple matches
console.log(JSON.stringify({
status: "multiple_matches",
message: `Found ${rules.length} rules matching "${ruleName}":`,
candidates: rules.map((r) => ({
name: r.name,
engine: r.engine,
severity: r.severity,
tags: r.tags.join(", "),
})),
}));
}
/**
* Parse the `sf code-analyzer rules --view detail` output format.
*
* Expected format:
* === 1. RuleName
* severity: 2 (High)
* engine: pmd
* tags: Recommended, Security, Apex
* resource: https://pmd.github.io/...
* description: Validates that CRUD permissions...
*/
function parseDetailOutput(output) {
const rules = [];
const lines = output.split("\n");
let currentRule = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Match rule header: === N. RuleName (must have a number prefix)
// Skip "=== Summary" which is the footer section
const headerMatch = line.match(/^===\s+(\d+)\.\s+(.+)$/);
if (headerMatch) {
if (currentRule && currentRule.name) {
rules.push(currentRule);
}
currentRule = {
name: headerMatch[2].trim(),
engine: "unknown",
severity: "unknown",
tags: [],
description: "",
resources: [],
};
continue;
}
// Skip lines if no current rule context
if (!currentRule) continue;
// Match key-value pairs (indented with spaces):
// severity: 3 (Moderate)
// engine: eslint
// tags: Recommended, BestPractices, JavaScript
// resource: https://...
// description: Some text here
const kvMatch = line.match(/^\s{2,}(\w+):\s+(.+)$/);
if (kvMatch) {
const key = kvMatch[1].toLowerCase();
const value = kvMatch[2].trim();
switch (key) {
case "severity":
currentRule.severity = value;
break;
case "engine":
currentRule.engine = value;
break;
case "tags":
currentRule.tags = value.split(",").map((t) => t.trim()).filter(Boolean);
break;
case "resource":
currentRule.resources.push(value);
break;
case "description":
currentRule.description = value;
// Description may continue on next lines (indented further)
while (
i + 1 < lines.length &&
lines[i + 1].match(/^\s{14,}/) &&
!lines[i + 1].match(/^\s{2,}\w+:/)
) {
i++;
currentRule.description += " " + lines[i].trim();
}
break;
}
}
}
// Push the last rule
if (currentRule && currentRule.name) {
rules.push(currentRule);
}
return rules;
}
/**
* Fallback: grep the full rule list for partial matches
*/
function tryGrepFallback(ruleName, engine) {
try {
const sel = engine || "Recommended";
const cmd = `sf code-analyzer rules --rule-selector "${sel}" 2>&1 | grep -i "${ruleName}"`;
const grepOutput = execSync(cmd, {
encoding: "utf8",
timeout: 60000,
maxBuffer: 2 * 1024 * 1024,
});
if (!grepOutput.trim()) return null;
// Parse table output lines
// Format: index name engine severity tags
const candidates = grepOutput
.trim()
.split("\n")
.filter((line) => line.trim() && !line.startsWith("─") && !line.startsWith("="))
.slice(0, 10)
.map((line) => {
const parts = line.trim().split(/\s{2,}/);
// Try to identify which part is the rule name (usually index 1 after the row number)
if (parts.length >= 4) {
return {
name: parts[1] || parts[0],
engine: parts[2] || "unknown",
severity: parts[3] || "unknown",
tags: parts[4] || "",
};
}
return { name: line.trim(), engine: "unknown", severity: "unknown", tags: "" };
});
if (candidates.length === 0) return null;
return {
status: "multiple_matches",
message: `Rule "${ruleName}" not found as exact match. Found ${candidates.length} potential matches:`,
candidates,
};
} catch (err) {
return null;
}
}
/**
* Fuzzy fallback: get all rule names and find closest matches by edit distance.
* Catches typos like "ApexCRUDVioltion" "ApexCRUDViolation"
*/
function tryFuzzyFallback(ruleName, engine) {
try {
const sel = engine || "Recommended";
const cmd = `sf code-analyzer rules --rule-selector "${sel}" 2>&1`;
const output = execSync(cmd, {
encoding: "utf8",
timeout: 60000,
maxBuffer: 2 * 1024 * 1024,
});
// Extract rule names from table output
// Lines with rule data have: index name engine severity tags
const ruleNames = [];
const ruleInfo = {};
output.split("\n").forEach((line) => {
const parts = line.trim().split(/\s{2,}/);
if (parts.length >= 4 && /^\d+$/.test(parts[0])) {
const name = parts[1];
ruleNames.push(name);
ruleInfo[name] = {
name: name,
engine: parts[2] || "unknown",
severity: parts[3] || "unknown",
tags: parts[4] || "",
};
}
});
if (ruleNames.length === 0) return null;
// Score each rule by edit distance to the query
const queryLower = ruleName.toLowerCase();
const scored = ruleNames
.map((name) => ({
name,
distance: levenshtein(queryLower, name.toLowerCase()),
// Also check if query is a subsequence (handles missing chars)
containsSubseq: isSubsequence(queryLower, name.toLowerCase()),
}))
.filter((r) => {
// Only include if distance is reasonable (within 30% of query length)
const maxDistance = Math.max(3, Math.floor(ruleName.length * 0.3));
return r.distance <= maxDistance || r.containsSubseq;
})
.sort((a, b) => a.distance - b.distance)
.slice(0, 5);
if (scored.length === 0) return null;
const candidates = scored.map((s) => ({
...ruleInfo[s.name],
distance: s.distance,
}));
// If the best match is very close (distance <= 2), mark as likely match
const best = scored[0];
if (best.distance <= 2) {
return {
status: "multiple_matches",
message: `Rule "${ruleName}" not found. Did you mean "${best.name}"? (${best.distance} character${best.distance === 1 ? "" : "s"} different)`,
candidates,
};
}
return {
status: "multiple_matches",
message: `Rule "${ruleName}" not found. Closest matches by name similarity:`,
candidates,
};
} catch (err) {
return null;
}
}
/**
* Levenshtein edit distance between two strings
*/
function levenshtein(a, b) {
const m = a.length;
const n = b.length;
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
for (let i = 0; i <= m; i++) dp[i][0] = i;
for (let j = 0; j <= n; j++) dp[0][j] = j;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (a[i - 1] === b[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
}
}
}
return dp[m][n];
}
/**
* Check if 'query' is a subsequence of 'target' (handles missing chars)
* e.g., "CRUDVioltion" is a subsequence of "CRUDViolation"
*/
function isSubsequence(query, target) {
let qi = 0;
for (let ti = 0; ti < target.length && qi < query.length; ti++) {
if (query[qi] === target[ti]) qi++;
}
// Consider it a match if at least 80% of query chars appear in order
return qi >= query.length * 0.8;
}

View File

@ -0,0 +1,260 @@
#!/usr/bin/env node
// Version: v1.0 | SHA256: placeholder
// List Code Analyzer rules matching a selector and return structured JSON
// Usage: node list-rules.js <selector> [options]
//
// This script runs `sf code-analyzer rules --rule-selector <selector>` and
// parses the table output into structured JSON for presentation.
//
// The CLI table format is:
// # Name Engine Severity Tags
// 1 @lwc/lwc/no-inner-html eslint 2 (High) Recommended, LWC, Security
//
// Output: JSON with {status, totalRules, rules: [{name, engine, severity, severityNum, tags}], engines, summary}
const { execSync } = require("child_process");
function printUsage() {
console.error(`Usage: node list-rules.js <selector> [options]
Arguments:
<selector> Rule selector (same syntax as --rule-selector)
Examples: "Security", "pmd", "eslint:Recommended",
"(pmd,eslint):Security:(1,2)", "Apex", "JavaScript"
Options:
--engine <name> Filter results to a specific engine after listing
--severity <n> Filter results to specific severity (1-5, comma-separated)
--top <n> Return at most N rules (default: 100)
--count-only Return only counts by engine/severity/category (no rule list)
Valid selector tokens:
Engines: eslint, regex, retire-js, flow, pmd, cpd, sfge
Severities: Critical/1, High/2, Moderate/3, Low/4, Info/5
Categories: Security, Performance, BestPractices, CodeStyle, Design, ErrorProne, Documentation
Languages: Apex, JavaScript, TypeScript, HTML, CSS, Visualforce, XML
Tags: Recommended, Custom, All, DevPreview, LWC, Fixable
Examples:
node list-rules.js "Security"
node list-rules.js "pmd:Security"
node list-rules.js "eslint:Recommended"
node list-rules.js "(pmd,eslint):Security:(1,2)"
node list-rules.js "Apex"
node list-rules.js "JavaScript:BestPractices"
node list-rules.js "Recommended" --count-only
node list-rules.js "all" --engine pmd --severity 1,2 --top 10`);
process.exit(1);
}
// Parse CLI arguments
const args = process.argv.slice(2);
if (args.length < 1 || args[0] === "--help" || args[0] === "-h") {
printUsage();
}
const selector = args[0];
const options = {
engine: null,
severity: null,
top: 100,
countOnly: false,
};
for (let i = 1; i < args.length; i++) {
switch (args[i]) {
case "--engine":
options.engine = (args[++i] || "").toLowerCase();
break;
case "--severity":
options.severity = (args[++i] || "")
.split(",")
.map((s) => parseInt(s.trim(), 10))
.filter((n) => n >= 1 && n <= 5);
break;
case "--top":
options.top = parseInt(args[++i] || "25", 10);
break;
case "--count-only":
options.countOnly = true;
break;
default:
console.error(`Unknown option: ${args[i]}`);
printUsage();
}
}
// Validate selector tokens before running CLI
const validationError = validateSelector(selector);
if (validationError) {
console.log(JSON.stringify({
status: "invalid_selector",
message: validationError,
hint: "Valid tokens: engines (pmd, eslint, cpd, retire-js, regex, flow, sfge), severities (1-5 or Critical/High/Moderate/Low/Info), categories (Security, Performance, BestPractices, CodeStyle, Design, ErrorProne, Documentation), languages (Apex, JavaScript, TypeScript, HTML, CSS, Visualforce, XML), tags (Recommended, Custom, All, DevPreview, LWC, Fixable)",
}));
process.exit(0);
}
// Run `sf code-analyzer rules`
let rawOutput;
try {
const cmd = `sf code-analyzer rules --rule-selector "${selector}" 2>&1`;
rawOutput = execSync(cmd, {
encoding: "utf8",
timeout: 60000,
maxBuffer: 2 * 1024 * 1024,
});
} catch (err) {
rawOutput = err.stdout || err.stderr || (err.output && err.output.join("")) || "";
if (!rawOutput) {
console.log(JSON.stringify({
status: "error",
message: `Failed to run sf code-analyzer rules: ${err.message}`,
}));
process.exit(0);
}
}
// Parse the table output
const rules = parseTableOutput(rawOutput);
if (rules.length === 0) {
console.log(JSON.stringify({
status: "no_rules_found",
message: `No rules matched selector "${selector}". Check the selector syntax or try a broader query.`,
hint: "Use tokens like: Security, pmd, eslint:Recommended, (1,2), Apex",
}));
process.exit(0);
}
// Apply post-filters
let filtered = rules;
if (options.engine) {
filtered = filtered.filter((r) => r.engine.toLowerCase() === options.engine);
}
if (options.severity) {
filtered = filtered.filter((r) => options.severity.includes(r.severityNum));
}
// Compute summary stats
const engineCounts = {};
const severityCounts = {};
const categoryCounts = {};
filtered.forEach((r) => {
engineCounts[r.engine] = (engineCounts[r.engine] || 0) + 1;
const sevKey = `${r.severityNum} (${severityName(r.severityNum)})`;
severityCounts[sevKey] = (severityCounts[sevKey] || 0) + 1;
(r.tags || []).forEach((tag) => {
const t = tag.trim();
if (["Security", "Performance", "BestPractices", "CodeStyle", "Design", "ErrorProne", "Documentation"].includes(t)) {
categoryCounts[t] = (categoryCounts[t] || 0) + 1;
}
});
});
// Build result
const result = {
status: "success",
selector,
totalRules: filtered.length,
summary: {
byEngine: engineCounts,
bySeverity: severityCounts,
byCategory: categoryCounts,
},
};
if (!options.countOnly) {
result.rules = filtered.slice(0, options.top).map((r) => ({
name: r.name,
engine: r.engine,
severity: r.severity,
severityNum: r.severityNum,
tags: r.tags,
}));
if (filtered.length > options.top) {
result.truncated = true;
result.showing = options.top;
}
}
console.log(JSON.stringify(result));
// --- Helper Functions ---
function parseTableOutput(output) {
const rules = [];
const lines = output.split("\n");
for (const line of lines) {
// Match table rows: starts with spaces + number
// Format: " 1 ruleName engine severity tags"
const match = line.match(/^\s+(\d+)\s{2,}(\S+)\s{2,}(\S+)\s{2,}(\d+\s*\([^)]+\))\s{2,}(.+)$/);
if (match) {
const severityStr = match[4].trim();
const sevNumMatch = severityStr.match(/^(\d)/);
rules.push({
name: match[2].trim(),
engine: match[3].trim(),
severity: severityStr,
severityNum: sevNumMatch ? parseInt(sevNumMatch[1], 10) : 0,
tags: match[5].trim().split(",").map((t) => t.trim()).filter(Boolean),
});
}
}
return rules;
}
function validateSelector(selector) {
if (!selector || !selector.trim()) {
return "Selector cannot be empty.";
}
const VALID_TOKENS = new Set([
// Engines
"eslint", "regex", "retire-js", "flow", "pmd", "cpd", "sfge",
// Severity names
"critical", "high", "moderate", "low", "info",
// Severity numbers
"1", "2", "3", "4", "5",
// General tags
"recommended", "custom", "all",
// Categories
"bestpractices", "codestyle", "design", "documentation", "errorprone", "security", "performance",
// Languages
"apex", "css", "html", "javascript", "typescript", "visualforce", "xml",
// Engine-specific
"devpreview", "lwc", "fixable",
]);
// Split by : (AND), then handle () groups (OR)
const groups = selector.split(":").map((s) => s.trim()).filter(Boolean);
const invalid = [];
for (const group of groups) {
let tokens;
if (group.startsWith("(") && group.endsWith(")")) {
// OR group: (token1,token2)
tokens = group.slice(1, -1).split(",").map((t) => t.trim()).filter(Boolean);
} else {
tokens = [group];
}
for (const token of tokens) {
if (!VALID_TOKENS.has(token.toLowerCase())) {
invalid.push(token);
}
}
}
if (invalid.length > 0) {
return `Invalid selector token(s): ${invalid.join(", ")}. Did you misspell a token?`;
}
return null;
}
function severityName(num) {
const names = { 1: "Critical", 2: "High", 3: "Moderate", 4: "Low", 5: "Info" };
return names[num] || "Unknown";
}

View File

@ -0,0 +1,230 @@
#!/usr/bin/env node
// Version: v1.0 | SHA256: placeholder
// Query and filter Code Analyzer results JSON with rich filtering capabilities
// Usage: node query-results.js <results-file.json> [options]
//
// Options:
// --engine <name> Filter by engine (pmd, eslint, cpd, retire-js, etc.)
// --severity <n> Filter by severity (1-5, comma-separated for multiple)
// --category <tag> Filter by category/tag (Security, Performance, etc.)
// --rule <name> Filter by exact rule name (case-insensitive)
// --file <substring> Filter by file path substring
// --top <n> Return top N results (default: 10)
// --sort <field> Sort by: severity, rule, engine, file (default: severity)
// --sort-dir <dir> Sort direction: asc, desc (default: asc)
// --summary Show only summary counts (no individual violations)
const fs = require("fs");
const path = require("path");
function printUsage() {
console.error(`Usage: node query-results.js <results-file.json> [options]
Options:
--engine <name> Filter by engine (pmd, eslint, cpd, retire-js, etc.)
--severity <n> Filter by severity (1-5, comma-separated for multiple: 1,2)
--category <tag> Filter by category/tag (Security, Performance, BestPractices, etc.)
--rule <name> Filter by exact rule name (case-insensitive)
--file <substring> Filter by file path substring (case-insensitive)
--top <n> Return top N results (default: 10)
--sort <field> Sort by: severity, rule, engine, file (default: severity)
--sort-dir <dir> Sort direction: asc, desc (default: asc)
--summary Show only summary counts (no individual violations)
Examples:
node query-results.js results.json --engine pmd --severity 1,2
node query-results.js results.json --category Security --top 20
node query-results.js results.json --file AccountService.cls
node query-results.js results.json --rule ApexCRUDViolation
node query-results.js results.json --summary`);
process.exit(1);
}
// Parse CLI arguments
const args = process.argv.slice(2);
if (args.length < 1 || args[0] === "--help" || args[0] === "-h") {
printUsage();
}
const filePath = args[0];
const options = {
engine: null,
severity: null,
category: null,
rule: null,
file: null,
top: 10,
sort: "severity",
sortDir: "asc",
summary: false,
};
// Parse named options
for (let i = 1; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case "--engine":
options.engine = (args[++i] || "").toLowerCase();
break;
case "--severity":
options.severity = (args[++i] || "")
.split(",")
.map((s) => parseInt(s.trim(), 10))
.filter((n) => n >= 1 && n <= 5);
break;
case "--category":
options.category = (args[++i] || "").toLowerCase();
break;
case "--rule":
options.rule = (args[++i] || "").toLowerCase();
break;
case "--file":
options.file = (args[++i] || "").toLowerCase();
break;
case "--top":
options.top = parseInt(args[++i] || "10", 10);
break;
case "--sort":
options.sort = args[++i] || "severity";
break;
case "--sort-dir":
options.sortDir = args[++i] || "asc";
break;
case "--summary":
options.summary = true;
break;
default:
console.error(`Unknown option: ${arg}`);
printUsage();
}
}
// Read and parse results file
let data;
try {
data = JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch (err) {
console.error(`Error reading results file: ${err.message}`);
process.exit(1);
}
const runDir = data.runDir || "";
const violations = data.violations || [];
// Apply filters
let filtered = violations.filter((v) => {
if (options.engine && v.engine.toLowerCase() !== options.engine) return false;
if (options.severity && !options.severity.includes(v.severity)) return false;
if (options.category) {
const tags = (v.tags || []).map((t) => t.toLowerCase());
if (!tags.includes(options.category)) return false;
}
if (options.rule && v.rule.toLowerCase() !== options.rule) return false;
if (options.file) {
const loc = v.locations && v.locations[v.primaryLocationIndex || 0];
const fileLower = ((loc && loc.file) || "").toLowerCase();
if (!fileLower.includes(options.file)) return false;
}
return true;
});
// Sort
const sortMul = options.sortDir === "desc" ? -1 : 1;
filtered.sort((a, b) => {
let cmp = 0;
switch (options.sort) {
case "severity":
cmp = a.severity - b.severity;
break;
case "rule":
cmp = a.rule.localeCompare(b.rule);
break;
case "engine":
cmp = a.engine.localeCompare(b.engine);
break;
case "file": {
const aLoc = a.locations && a.locations[a.primaryLocationIndex || 0];
const bLoc = b.locations && b.locations[b.primaryLocationIndex || 0];
const aFile = (aLoc && aLoc.file) || "";
const bFile = (bLoc && bLoc.file) || "";
cmp = aFile.localeCompare(bFile);
break;
}
}
if (cmp !== 0) return cmp * sortMul;
// Secondary sort: severity ascending
return (a.severity - b.severity) * sortMul;
});
// Build output
const totalMatches = filtered.length;
const limited = filtered.slice(0, options.top);
// Compute severity breakdown of matches
const sevCounts = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
filtered.forEach((v) => {
if (sevCounts[v.severity] !== undefined) sevCounts[v.severity]++;
});
// Compute rule frequency for matches
const ruleCounts = {};
const ruleEngines = {};
filtered.forEach((v) => {
ruleCounts[v.rule] = (ruleCounts[v.rule] || 0) + 1;
if (!ruleEngines[v.rule]) ruleEngines[v.rule] = v.engine;
});
const topRules = Object.entries(ruleCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, options.top)
.map(([rule, count]) => ({ rule, engine: ruleEngines[rule], count }));
// Compute file frequency for matches
const fileCounts = {};
filtered.forEach((v) => {
const loc = v.locations && v.locations[v.primaryLocationIndex || 0];
let file = (loc && loc.file) || "unknown";
if (runDir && file.startsWith(runDir)) file = file.substring(runDir.length + 1);
fileCounts[file] = (fileCounts[file] || 0) + 1;
});
const topFiles = Object.entries(fileCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, options.top)
.map(([file, count]) => ({ file, count }));
// Build result object
const result = {
query: {
engine: options.engine,
severity: options.severity,
category: options.category,
rule: options.rule,
file: options.file,
top: options.top,
sort: options.sort,
sortDir: options.sortDir,
},
totalViolations: violations.length,
totalMatches,
severityCounts: sevCounts,
topRules,
topFiles,
};
if (!options.summary) {
result.violations = limited.map((v) => {
const loc = v.locations && v.locations[v.primaryLocationIndex || 0];
let file = (loc && loc.file) || "unknown";
if (runDir && file.startsWith(runDir)) file = file.substring(runDir.length + 1);
return {
rule: v.rule,
engine: v.engine,
severity: v.severity,
message: v.message,
file: file,
startLine: (loc && loc.startLine) || 0,
tags: v.tags || [],
};
});
}
console.log(JSON.stringify(result));

View File

@ -0,0 +1,121 @@
---
name: using-salesforce-archive
description: "ALWAYS USE THIS SKILL for anything involving Salesforce Archive (also called Trusted Services Archive) — search, view, unarchive, analyze, mask, and erase (RTBF) archived records via the Archive Connect API, and reading archive job status from the ArchiveActivity object. TRIGGER when: user mentions Salesforce Archive, Trusted Services Archive, archive/unarchive records, ArchiveActivity, archive jobs, archive policy, archive analyzer, archived record search, archive storage, archive failure logs, right to be forgotten / RTBF on archived data, or masking archived PII — including phrasings like 'find records that were archived', 'restore archived data', 'why did the archive job fail', 'download the archive failure log', or 'monitor my archive jobs', AND even when they ask you to explain, give guidance, or write a runbook/doc about these topics rather than run code. SKIP when: the user wants generic data-export/backup unrelated to the Archive add-on, or wants to build the archive policy UI metadata."
metadata:
version: "1.0"
---
# Salesforce Archive
Operate Salesforce Archive (also called Trusted Services Archive) through its Connect API and the `ArchiveActivity` job-metadata object. This skill covers how to search and restore archived records, run the analyzer, handle RTBF erasure and PII masking, check storage, and — the part most often missed — how to read archive job status from `ArchiveActivity` and use a job's Id + Type to download its logs.
## Scope
- **In scope**: Calling the Archive Connect API operations under `/platform/data-resilience/archive/`; querying the `ArchiveActivity` object via SOQL/Connect; correlating a job's `ArchiveActivity` record with its log-download endpoints; the verify-after-write pattern for each async operation.
- **Out of scope**: Defining archive policies / `ArchivePolicyDefinition` metadata; building UI; generating Flows over archive data (`ArchiveActivity` is **not** Flow-queryable — see Gotchas); generic backup/export tooling unrelated to the add-on.
---
## Required Inputs
Gather or infer before acting:
- **Operation intent**: search, view, unarchive, analyze, mask, RTBF, storage check, or job-status/log lookup.
- **Target sObject** (`sobjectName`): required for search and unarchive.
- **Filters**: search and unarchive require `sobjectName` + at least one filter.
- **For log downloads**: the `requestId` (an `ArchiveActivity` Id, `8qv…` prefix) of a completed, log-producing job, and `reportType` = that activity's `Type`.
Preconditions (confirm or surface to the user if a call returns a not-permitted error):
- The **org** must have Salesforce Archive enabled (the `TrustedServicesArchive` / `TrustedServicesArchiveBt` org permission). Every operation is gated on this first.
- Each operation requires a **specific user permission** on top of the org gate — see the Permissions table below. There is no single "archive admin" role; access is per-capability.
---
## Permissions
Every operation first requires the org to have Salesforce Archive enabled (`hasTrustedServicesArchive` = `OrgPermissions.TrustedServicesArchive || TrustedServicesArchiveBt`). On top of that org gate, each capability is gated by a distinct **user permission**. A call the user isn't permitted for fails with a "not permitted" error — match the error to the missing permission below.
| Operation | User permission required |
|-----------|--------------------------|
| `search-archived-records`, `get-search-archived-records-next-page` | `ViewSearchPage` (Archive Search) — the Connect search endpoint runs the global-search path, gated by Archive Search, **not** `ViewArchivedRecords` |
| `search-archived-records-with-sharing-rules` (Agentforce) | `ViewArchivedRecords` |
| `unarchive-records` | `UnarchiveSdk` |
| `forget-archived-records` (RTBF) + `get-rtbf-status` | `Rtbf` |
| `mask-archived-records` + `get-masking-status` | `Rtbf` (masking shares the same `Rtbf` permission — **not** a separate entitlement) |
| `run-analyzer`, `get-analyzer-report`, `get-archive-storage-used` | `ArchiveAnalyzer` |
| `get-execution-details-stream-url`, `get-failed-records-stream-url` | `ViewActivitiesPage` (Archive Activities) |
> Source of truth: the Connect API resource classes in `trusted-services-archive-connect-impl``TrustedServicesArchiveSdkImpl` guards → `TrustedServicesArchive.accessChecks.xml`. Note `search-archived-records` routes through `performArchiverGlobalSearch` (gated by `canRunArchiveSearch` = `ViewSearchPage`); the separate Apex-SDK `searchArchivedRecords()` method is gated by `ViewArchivedRecords`, but the **Connect API** search endpoint does not use it.
---
## Workflow
All steps are sequential within a task. Read the referenced file the first time you touch that area.
1. **Identify the operation and read the contract** — do not rely on general knowledge of the Archive API, which has non-obvious contracts. Load `references/connect-api-operations.md` for the exact request/response shape, required inputs, and per-operation gotchas of every Archive Connect API operation. Do this before constructing any call (e.g. `dateRanges` plural vs singular, `isSuccess` flag vs HTTP status, `url: null` meaning no log).
2. **For job status / monitoring, read the data model** — when the task involves archive jobs, failures, progress, counts, or logs, load `references/archive-activity-entity.md` for the `ArchiveActivity` field reference and how it links to the Connect API. Query `ArchiveActivity` via SOQL or Connect — **not** Flow. For a worked end-to-end example (find failed/in-progress jobs, then pull their execution-detail and failed-records logs), load `examples/monitor-failed-jobs.md`.
3. **Construct and send the call** — follow the contract exactly. For searches, supply `sobjectName` + ≥1 filter; for date filtering use the plural `dateRanges` array of `{field, from, to}` with full ISO-8601 datetimes.
4. **Branch on the right signal** — some operations return HTTP 201 with a body-level success flag (`body.statusCode`, `body.isSuccess`). Read `references/connect-api-operations.md` for which signal to trust per operation; never assume the HTTP status alone means success.
5. **Verify after every write** — re-read state to confirm the effect (see the Verify-After-Write table below). Async operations (analyzer, RTBF, masking) return a request id you must poll.
---
## Verify-After-Write
| After this write | Confirm by |
|------------------|-----------|
| `run-analyzer` | Poll `get-analyzer-report` until the report is populated |
| `unarchive-records` | Re-run `search-archived-records` — confirm records left the archive |
| `forget-archived-records` (RTBF) | Poll `get-rtbf-status` with the returned `request_id` |
| `mask-archived-records` | Poll `get-masking-status` with the returned `request_id` |
---
## Rules / Constraints
| Constraint | Rationale |
|-----------|-----------|
| Search & unarchive require `sobjectName` + at least one filter | The controller rejects an unfiltered request with "Search must be based on at least 1 field" — a full-object operation is never allowed. |
| Date filters must be full ISO-8601 datetimes (`2020-01-01T00:00:00Z`) | A date-only value (`2020-01-01`) returns `400 JSON_PARSER_ERROR` because the field is typed `xsd:dateTime`. |
| Search uses `dateRanges` (plural array); unarchive uses `dateRange` (singular) | They are genuinely different fields on the two endpoints; using the wrong shape silently drops the filter or 400s. |
| Stop pagination when `scroll_id == "-1"` | Calling `get-search-archived-records-next-page` with `"-1"` returns 500. |
| Log downloads need a real `ArchiveActivity` Id as `requestId` + that activity's `Type` as `reportType` | The backend resolves the log by the activity record; a mismatched `reportType` returns no log. |
| Never use the deprecated lookups | `global-search-by-id`, `get-global-search-results`, and `view-archived-records` are deprecated with no successor and currently 500. Use `search-archived-records` (+ next-page) instead. |
| Excluded objects are not retrievable | `Feed`, `History`, `Relation`, `Share` are not searchable; Files/Attachments are not retrievable via this API — do not promise them. |
| Query `ArchiveActivity` via SOQL/Connect, never Flow | `ArchiveActivity` has `isProcessEnabled=false`, so a Flow "Get Records" element on it fails with "You can't get ArchiveActivity records in a flow." |
---
## Gotchas
| Issue | Resolution |
|-------|------------|
| Treating HTTP 201 as success | Several operations return 201 with a body-level outcome. Branch on `body.statusCode` (search) or `body.isSuccess` (`with-sharing-rules`), not the HTTP code. |
| `run-analyzer.isRunning` used as a signal | It is **always** `null`; the endpoint only populates `message`. Poll `get-analyzer-report` to confirm completion instead. |
| `search-archived-records-with-sharing-rules` (Agentforce) filters as an array | `filtersJson` must be a JSON-encoded **object map** `{"Field":"Value"}`, not an array of `{field,value}`; the array form returns `isSuccess:false "No valid filters provided"`. |
| Log `url` treated as present because status is 201 | `get-*-stream-url` returns `{url}`; `url: null` means no log was resolved. Always check `url != null`. |
| Misreading `get-archive-storage-used` | `usedStorage[]`/`availableStorage[]` are parallel positional arrays: index 0=org DATA, 1=org FILE, 2=archive RECORDS, 3=archive FILE. `availableStorage[2]`/`[3]` are **always 0** (archive tier is unmetered) — that means "not tracked", not "full". |
| Expecting `ArchiveActivity` in a Flow | It is not Flow-enabled (`isProcessEnabled=false`). Use SOQL/Connect/Reports. |
| Hitting unarchive caps | Unarchive processes ≤1000 matched records per request and ≤50 requests/hour/org, and restores the whole archived hierarchy of each match. |
| RTBF/masking caps | `criteria` ≤10 entries (one per object); ≤10,000 root records/day (shared between RTBF and masking); masking is irreversible. Both RTBF and masking are gated by the same `Rtbf` user permission. |
---
## Output Expectations
This is a knowledge/API skill — it produces API calls and their interpreted results, plus SOQL against `ArchiveActivity`. It does not generate deployable metadata. Deliverables per task: the correct operation invocation(s), the right success-signal branching, and a verify-after-write confirmation.
---
## Reference File Index
| File | When to read |
|------|-------------|
| `references/connect-api-operations.md` | Before constructing any Archive Connect API call — full per-operation contracts, success signals, and limits |
| `references/archive-activity-entity.md` | For any job-status / failure / progress / log task — `ArchiveActivity` field reference and its link to the log-download endpoints |
| `examples/monitor-failed-jobs.md` | To follow an end-to-end monitoring flow: find failed/in-progress jobs, then download their logs |

View File

@ -0,0 +1,47 @@
# Example — Monitor failed archive jobs and download their logs
End-to-end flow for the most common archive-admin task: find the jobs that failed or stalled, read why, and pull the logs that show which records didn't make it.
## 1. Find the jobs (query ArchiveActivity)
`ArchiveActivity` is not Flow-queryable, so use SOQL/Connect:
```sql
SELECT Id, Name, Status, Type, StartTime, ProgressPercentage,
TotalRecordCount, SucceededCount, FailedCount, SkippedRootRecordsCount,
RootEntityName, FailureReason
FROM ArchiveActivity
WHERE (Status = 'Failed' OR Status = 'In Progress' OR Status = 'Ended With Errors')
AND StartTime >= LAST_N_DAYS:7
ORDER BY StartTime DESC
```
Read each row's `ProgressPercentage`, `SucceededCount`/`FailedCount`, `RootEntityName`, and `FailureReason` to summarize health. In-progress jobs have a blank `EndTime`.
## 2. For each failed job, get the logs
Take the job's `Id` (`8qv…`) as `requestId` and its `Type` as `reportType`. `{activity.Type}` below is a placeholder for that job's own `Type` field value (e.g. `Archive`, `Purge`, `Unarchive`, `Analyzer`) — substitute the actual value per job; a hardcoded/mismatched `reportType` returns no log.
**Execution-detail log:**
```
GET /platform/data-resilience/archive/log/execution-details-stream-url
?requestId={activity.Id}&reportType={activity.Type}
```
**Failed-records log:**
```
GET /platform/data-resilience/archive/log/failed-records-stream-url
?requestId={activity.Id}&reportType={activity.Type}
```
Each returns `{ url }`. **Check `url != null`** before using it — a `null` url means no log was resolved for that job/type (e.g. the job produced no failed-records file), not an error to retry blindly.
## 3. Summarize
Per failed/in-progress job, report: Name, Status, ProgressPercentage, SucceededCount, FailedCount, RootEntityName (target object), FailureReason, and the two log URLs (or "no log available" when `url` is null).
## Pitfalls this flow avoids
- **Not** attempting a Flow over `ArchiveActivity` (`isProcessEnabled=false` → "You can't get ArchiveActivity records in a flow").
- Passing the activity's real `Type` as `reportType` (a mismatch returns no log).
- Treating a 201 with `url: null` as a usable log.

View File

@ -0,0 +1,59 @@
# ArchiveActivity — Data Model Reference
`ArchiveActivity` is the platform entity that records **job metadata for Salesforce Archive jobs** (archive, purge, analyze, unarchive, export). Every archive job produces one `ArchiveActivity` record; you read it to learn a job's status, progress, and outcome, and you use its `Id` + `Type` to download the job's logs.
## Entity facts
| Property | Value |
|----------|-------|
| API name | `ArchiveActivity` |
| Key prefix | `8qv` (record Ids start `8qv…`) |
| Owning product | Salesforce Archive (Trusted Services Archive) |
| Kind | **Standard platform entity** (not a custom object) |
| License gate | Requires the Trusted Services Archive add-on (`TrustedServicesArchive.hasTrustedServicesArchive`) — absent on orgs without the add-on |
| Name field | Auto-number, mask `ARCV-{0000000000}` |
| Flow-enabled? | **No**`isProcessEnabled=false`. A Flow "Get Records" element on it fails: "You can't get ArchiveActivity records in a flow." Query via SOQL / Connect / Reports. |
## Fields
| Field | Type | Meaning |
|-------|------|---------|
| `Name` | Auto-number | `ARCV-…` job identifier |
| `Status` | enum `ArchiveActivityStatus` | Lifecycle: scheduled, running / **In Progress**, completed, **Failed**, canceled, **Ended With Errors** |
| `Type` | enum `ArchiveActivityType` | The job mode: Archive, Purge, Analyze(r), Unarchive, Export-to-external-bucket, Export-and-download. **This is the `reportType` for log downloads.** |
| `StartTime` | DateTime | When execution began |
| `EndTime` | DateTime | When execution finished/terminated (blank while running) |
| `ArchivePolicyDefinition` | Lookup (FK) | Parent `ArchivePolicyDefinition`; child relationship `ArchiveActivities` |
| `RootEntityName` | Text | API name of the target sObject being archived |
| `TotalRecordCount` | Long | Records initially selected (succeeded + failed + skipped) |
| `AttemptedRootRecordsCount` | Long (formula) | `SkippedRootRecordsCount + FailedCount + SucceededCount` — top-level records actually attempted |
| `SkippedRootRecordsCount` | Long | Top-level records skipped (validation, exclusion filters, data-protection thresholds) |
| `SucceededCount` | Long | Records processed without error |
| `FailedCount` | Long | Records that failed (validation, missing refs, exceptions) |
| `ProgressPercentage` | Double | Percent complete |
| `FailureReason` | Long text | Why the job failed/partially completed (system error messages or policy-level failures) |
| `RecordsSizeInMb` | Text | Estimated total size of processed records (MB) |
> The entity also defines a `ProgressIcon` display formula for list views; it is not useful for programmatic monitoring — read `Status`, `ProgressPercentage`, and the count fields directly.
## How ArchiveActivity links to the Connect API
The log-download endpoints (`get-execution-details-stream-url`, `get-failed-records-stream-url`) take:
- `requestId` = an `ArchiveActivity` **`Id`** (`8qv…`) of a completed, log-producing job, and
- `reportType` = that same activity's **`Type`** value.
So the monitoring pattern is: **query `ArchiveActivity` → pick the job(s) of interest → pass each job's `Id` and `Type` to the stream-url operation → check `url != null`.** See `../examples/monitor-failed-jobs.md`.
## Example SOQL
```sql
-- Failed or in-progress jobs in the last 7 days, newest first
SELECT Id, Name, Status, Type, StartTime, EndTime, ProgressPercentage,
TotalRecordCount, SucceededCount, FailedCount, SkippedRootRecordsCount,
RootEntityName, FailureReason, ArchivePolicyDefinitionId
FROM ArchiveActivity
WHERE (Status = 'Failed' OR Status = 'In Progress' OR Status = 'Ended With Errors')
AND StartTime >= LAST_N_DAYS:7
ORDER BY StartTime DESC
```

View File

@ -0,0 +1,157 @@
# Salesforce Archive Connect API — Operations Reference
All operations are under the base path `/platform/data-resilience/archive/`. Contracts verified live against archived data. Read the operation you need before constructing the call — several contracts are non-obvious.
## Operations Summary
| Operation | Purpose | Method + Path | Verify with |
|-----------|---------|---------------|-------------|
| `search-archived-records` | read | `POST /search` | — |
| `search-archived-records-with-sharing-rules` | read (Agentforce) | `POST /search/with-sharing-rules` | — |
| `get-search-archived-records-next-page` | read | `GET /search/next/{scrollId}` | — |
| `run-analyzer` | write | `POST /analyzer/run` | `get-analyzer-report` |
| `get-analyzer-report` | read | `GET /analyzer/report` | — |
| `unarchive-records` | write | `POST /unarchive` | re-run `search-archived-records` |
| `forget-archived-records` (RTBF) | write | `POST /rtbf` | `get-rtbf-status` |
| `get-rtbf-status` | read | `GET /rtbf/{requestId}` | — |
| `mask-archived-records` | write | `POST /mask` | `get-masking-status` |
| `get-masking-status` | read | `GET /mask/{requestId}` | — |
| `get-execution-details-stream-url` | read | `GET /log/execution-details-stream-url` | — |
| `get-failed-records-stream-url` | read | `GET /log/failed-records-stream-url` | — |
| `get-archive-storage-used` | read | `GET /storage/archive-used` | — |
**Deprecated — do not use** (no successor; currently 500): `global-search-by-id`, `get-global-search-results`, `view-archived-records`. Use `search-archived-records` (+ `get-search-archived-records-next-page`) for all archived-record lookups.
### Verify-after-write dependencies
```mermaid
graph TD
run_analyzer["run-analyzer"] -.verify.-> get_analyzer_report["get-analyzer-report"]
forget_archived_records["forget-archived-records"] -.verify.-> get_rtbf_status["get-rtbf-status"]
mask_archived_records["mask-archived-records"] -.verify.-> get_masking_status["get-masking-status"]
unarchive_records["unarchive-records"] -.verify.-> search_archived_records["search-archived-records"]
```
---
## search-archived-records (`POST /search`)
Search archived records by object, filters, date ranges, and sort.
**Required**: `sobjectName` + at least 1 filter. Missing them returns a clean envelope validation error (`statusCode 400`).
**Inputs**:
- `sobjectName` *(string)* — API name of the sObject to search.
- `filters` *(array)* — Filter conditions, each `{field, value}` where **both are required strings**. `value` is a single string — **not** an array, **not** nullable (null/omitted → `400 "This field may not be null"`). There is **no `operator` field**. **At least 1, up to 6 filters, combined with AND only** (OR is not supported). Example: `[{"field":"Subject","value":"Foo"}]`.
- `dateRanges` *(array)* — Primary date filter: array of `{field, from, to}`. `from`/`to` must be full ISO-8601 datetime (`"2020-01-01T00:00:00Z"`); date-only → `400 JSON_PARSER_ERROR` (xsd:dateTime). Use the special field `archive_date` to filter by archive date instead of `CreatedDate`/`ModifiedDate`.
- `dateRange` *(object)* — Optional **singular** convenience range `{field, from, to}`; the controller folds it into `dateRanges` (one-element equivalent). Same singular shape that `unarchive-records` uses.
- `fields` *(array)* — Field API names to return.
- `pageSize` *(integer)* — Records per page; default 25, max 1000.
- `sortDirection` *(string)*`asc`/`desc`, case-insensitive, default `asc`; an invalid value → 400.
**Output**: HTTP **201**, `body = { records[], total_result_count, scroll_id }`, `body.statusCode = 200`, `errorMessage` null on success. **Branch on `body.statusCode`, not the HTTP code** — once past framework validation the response is always 200/201 even on logical failure, with the error in `errorMessage` + `statusCode`.
**Excluded objects**: `Feed`, `History`, `Relation`, `Share` are not searchable; Files/Attachments are not retrievable.
### Pagination
Read records inline from each response. If `body.scroll_id != "-1"`, call `get-search-archived-records-next-page` with that `scroll_id`. **STOP when `scroll_id == "-1"`** — calling next-page with `"-1"` (the terminal sentinel) → 500. There is no separate fetch-by-requestId step.
## get-search-archived-records-next-page (`GET /search/next/{scrollId}`)
**Input**: `scrollId` *(string, path param)* — the `body.scroll_id` from a prior search. Stop when it is `"-1"`; never call with `"-1"`.
**Output**: `body` (next page), `errorMessage`, `statusCode` — same envelope as search.
---
## search-archived-records-with-sharing-rules (`POST /search/with-sharing-rules`)
Archive search **optimized for Agentforce agents**. (There is no `/search/related` endpoint — this is the operation that takes the JSON filter map.) Gated by the **`ViewArchivedRecords`** user permission (unlike plain `search-archived-records`, which uses `ViewSearchPage`).
**Required**: `objectName`, `filtersJson`.
**Inputs**:
- `objectName` *(string)* — API name of the sObject.
- `filtersJson` *(string)* — JSON-encoded **OBJECT MAP** of `fieldName → value`, e.g. `"{\"Subject\":\"Foo\",\"Status\":\"New\"}"`. **NOT** an array of `{field,value}` objects — the array form is rejected with `isSuccess:false "No valid filters provided. Please provide filtersJson"`.
- `dateField` *(string)* — API name of the date field for temporal filtering.
- `startDate` / `endDate` *(string)* — search window bounds (ISO-8601).
- `maxResults` *(integer)* — max records to return; default 100.
**Output**: `isSuccess` *(boolean — **branch on THIS, not the HTTP status**)*, `totalResultCount`, `records` (rich-text summary of ~5 key fields per record), `recordsJson` (HTML-entity-encoded JSON string), `errorMessage`, `message`, `warnings` (schema-validation/auto-fix advisories). **Validation failures surface as HTTP 201 + `isSuccess:false` + `errorMessage`, never a 4xx.** Pattern: `isSuccess:false` + `errorMessage` → caller bug; `isSuccess:true` + `recordsJson` → success.
---
## run-analyzer (`POST /analyzer/run`) + get-analyzer-report (`GET /analyzer/report`)
`run-analyzer` triggers the analyzer; HTTP 201, output `message` *(string, human-readable status)*. **`isRunning` is ALWAYS `null`** — the controller only populates `message`; never branch on `isRunning`. Poll `get-analyzer-report` to confirm completion. Non-destructive / idempotent.
`get-analyzer-report` returns `body` with: `topRecords[]` (`{objectName, objectLabel, objectIcon, size, count, usagePercent}` per archivable sObject), `topFiles[]` (parallel shape for files), `fileGeneralStorage`/`dataGeneralStorage` (`{storageUsed, storageRemaining, usagePercent}`), and `createdDateReport` (`"DD/MM/YYYY HH:MM:SS"`).
---
## unarchive-records (`POST /unarchive`)
Restore archived records back into live storage by criteria.
**Inputs**:
- `sobjectName` *(string)* — sObject whose records to unarchive.
- `filters` *(array)* — criteria identifying which archived records to restore.
- `dateRange` *(object)* — optional **SINGULAR** range `{field, from, to}` (reads `getDateRange()`, unlike `/search` which uses plural `dateRanges`); full ISO-8601 datetimes. Omit to unarchive by filters alone.
**Caps**: ≤1000 matched records (else not processed); ≤50 unarchive requests/hour/org. Restores the **whole archived hierarchy** of each match. Requires the **`UnarchiveSdk`** user permission (on top of org-level Archive enablement).
**Output**: `body` (unarchive job details incl. job id), `errorMessage`, `statusCode`. **Verify** by re-running `search-archived-records`. **Rollback**: re-archive via a new archive job with the same criteria.
---
## forget-archived-records / RTBF (`POST /rtbf`) + get-rtbf-status (`GET /rtbf/{requestId}`)
Submits a Right-To-Be-Forgotten erasure request.
**Input**: `criteria` *(array of `{sobject, field, value}`)* — ≤10 items, **one per object type**; ≤10,000 root records erased per org/day; field/object names case-insensitive. Deletes the **entire archived hierarchy** of each match (no partial deletion). Note: the criterion must match a record archived **as a root** — filtering a parent (e.g. Account by Id) when only its children were archived matches nothing.
**Output**: HTTP 201, `body.request_id` *(UUID)*. Poll `get-rtbf-status` (path param `requestId` = that UUID) → `body.status` (e.g. `"Request is open. Scan is still in progress"`). **Rollback**: none — RTBF erasure is permanent.
---
## mask-archived-records (`POST /mask`) + get-masking-status (`GET /mask/{requestId}`)
Submits a PII-masking (anonymization) request — irreversibly replaces detected PII values with placeholders (e.g. `redacted@example.com`) while keeping the record + non-PII fields searchable.
**Input**: `criteria` *(array of `{sobject, field, value}`)* — same shape as RTBF.
**Behavior**: permanent; one-time per record (re-requests on an already-masked record are ignored); shares the 10,000/day RTBF rate limit; **PII fields are auto-detected** (you cannot choose them); records under legal hold / retention lock are excluded; cascades to child records. Available **only via this API** (not in the Archive UI).
**Permission**: gated by the **`Rtbf`** user permission — the SAME permission as RTBF (`maskArchivedRecords` runs the RTBF access check), not a separate masking entitlement.
**Output**: HTTP 201, `body.request_id` *(UUID)*. Poll `get-masking-status` (path param `requestId`); status reaches **HANDLED** when complete. **Rollback**: none — anonymization is permanent.
---
## Log Downloads — get-execution-details-stream-url / get-failed-records-stream-url
Both are `GET /log/...` and mint a one-time presigned download URL. `get-execution-details-stream-url` → the execution-detail log; `get-failed-records-stream-url` → the failed-records log (records that did not process). Identical contract.
**Output**: `{ url }`. **`url != null` is success; `url: null` means no log was resolved** (missing/incorrect `requestId`/`reportType`, or the activity produced no such log) — always check `url != null`; never treat `url:null` or the 201 status alone as success.
**Required inputs**:
- `requestId` *(string)* — the **`ArchiveActivity` Id** (`8qv…` key) of a completed, log-producing job present in the archiver backend — **not** a search requestId. A missing/non-matching id → `url:null`.
- `reportType` *(string)* — that activity's `Type`: `Archive | Unarchive | Analyzer | Purge | Export-to-external-bucket | Export-and-download`. Omitting it → `url:null`.
- `sobjectName` *(string, optional)* — the backend self-resolves it.
This is the bridge between `ArchiveActivity` (see `archive-activity-entity.md`) and downloadable logs.
---
## get-archive-storage-used (`GET /storage/archive-used`)
Returns `body.usedStorage[4]` (doubles — per-tier bytes consumed) and `body.availableStorage[4]` (per-tier capacity) — two **parallel positional arrays** (NOT a flat metric, NOT key/value maps). The 4 slots are the same in both:
| Index | Meaning |
|-------|---------|
| 0 | Salesforce org **DATA** storage |
| 1 | Salesforce org **FILE** storage |
| 2 | Archive-tier **RECORDS** storage |
| 3 | Archive-tier **FILE** storage |
**`availableStorage[2]` and `[3]` (the archive tier) are ALWAYS 0**: the controller hardcodes them (`ARCHIVER_AVAILABLE_STORAGE = 0`) because archive storage is unmetered, so a `0` there means "not tracked", NOT "no space left". Only `availableStorage[0]`/`[1]` (live org data/file remaining) are real. All values rounded to 2 decimals.