askyara 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
askyara/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """AskYARA Salesforce Engineering Core Package."""
2
+
3
+ __version__ = "0.1.0"
askyara/cli.py ADDED
@@ -0,0 +1,77 @@
1
+ """AskYARA Command Line Interface."""
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from askyara.installer.doctor import run_doctor
8
+ from askyara.tools.project_detector import detect_salesforce_project
9
+ from askyara.tools.git_diff import get_git_diff_summary
10
+ from askyara.core.policy_engine import PolicyEngine
11
+
12
+
13
+ def main(argv: list[str] | None = None) -> int:
14
+ parser = argparse.ArgumentParser(
15
+ prog="askyara",
16
+ description="AskYARA Salesforce Engineering — AI Companion & Validator",
17
+ )
18
+ subparsers = parser.add_subparsers(dest="command", help="Available subcommands")
19
+
20
+ # doctor
21
+ doctor_parser = subparsers.add_parser("doctor", help="Inspect local environment and tools")
22
+ doctor_parser.add_argument("--json", action="store_true", help="Output machine-readable JSON")
23
+
24
+ # review
25
+ review_parser = subparsers.add_parser("review", help="Review local Salesforce changes")
26
+ review_parser.add_argument("--scope", default="git-diff", help="Review scope (git-diff, staged)")
27
+ review_parser.add_argument("--pack", help="Specific industry rule pack (e.g. omnistudio, fsc, health)")
28
+ review_parser.add_argument("--format", choices=["markdown", "json"], default="markdown")
29
+
30
+ # audit
31
+ audit_parser = subparsers.add_parser("audit", help="Audit the current Salesforce project")
32
+ audit_parser.add_argument("--path", default=".", help="Root path of Salesforce project")
33
+ audit_parser.add_argument("--pack", help="Specific industry rule pack (e.g. omnistudio, fsc, health)")
34
+
35
+ args = parser.parse_args(argv)
36
+
37
+ repo_root = Path(__file__).resolve().parent.parent.parent
38
+ defaults_file = repo_root / "rules" / "defaults.yaml"
39
+ packs_dir = repo_root / "rules" / "packs"
40
+
41
+ if args.command == "doctor":
42
+ return run_doctor(as_json=args.json)
43
+ elif args.command == "review":
44
+ project = detect_salesforce_project(Path("."))
45
+ active_packs = list(project.industry_packs)
46
+ if args.pack and args.pack not in active_packs:
47
+ active_packs.append(args.pack)
48
+
49
+ policy = PolicyEngine(defaults_file, packs_dir=packs_dir, active_packs=active_packs)
50
+ print(f"[*] Detected Salesforce Project: {project.root} (Confidence: {project.confidence})")
51
+ print(f"[*] Active Rule Packs: {', '.join(policy.loaded_packs)}")
52
+ diff = get_git_diff_summary(Path("."))
53
+ print(f"[*] Git Status: {len(diff.changed_files)} changed files detected.")
54
+ print("[*] Local deterministic review complete. Ready for agent synthesis.")
55
+ return 0
56
+ elif args.command == "audit":
57
+ project = detect_salesforce_project(Path(args.path))
58
+ active_packs = list(project.industry_packs)
59
+ if args.pack and args.pack not in active_packs:
60
+ active_packs.append(args.pack)
61
+
62
+ policy = PolicyEngine(defaults_file, packs_dir=packs_dir, active_packs=active_packs)
63
+ print(f"[*] Auditing Salesforce Project at {project.root}")
64
+ print(f"[*] Active Rule Packs: {', '.join(policy.loaded_packs)}")
65
+ print(f"[*] Total Active Rules: {len(policy.get_enabled_rules())}")
66
+ for asset, count in project.asset_counts.items():
67
+ if count > 0:
68
+ print(f"[*] {asset.replace('_', ' ').title()}: {count}")
69
+ print("[*] Project audit complete.")
70
+ return 0
71
+ else:
72
+ parser.print_help()
73
+ return 0
74
+
75
+
76
+ if __name__ == "__main__":
77
+ sys.exit(main())
@@ -0,0 +1 @@
1
+ """Core Domain Package."""
@@ -0,0 +1,27 @@
1
+ """Command Registry Loader."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+ import yaml
6
+
7
+
8
+ class CommandRegistry:
9
+ def __init__(self, commands_dir: Path) -> None:
10
+ self.commands_dir = commands_dir
11
+ self.commands: dict[str, dict[str, Any]] = {}
12
+ self.load_all()
13
+
14
+ def load_all(self) -> None:
15
+ if not self.commands_dir.exists():
16
+ return
17
+ for file in self.commands_dir.glob("*.yaml"):
18
+ with open(file, "r", encoding="utf-8") as f:
19
+ data = yaml.safe_load(f)
20
+ if isinstance(data, dict) and "id" in data:
21
+ self.commands[data["id"]] = data
22
+
23
+ def get(self, command_id: str) -> dict[str, Any] | None:
24
+ return self.commands.get(command_id)
25
+
26
+ def list_commands(self) -> list[dict[str, Any]]:
27
+ return list(self.commands.values())
@@ -0,0 +1,92 @@
1
+ """Finding and Evidence Data Models."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import Any
6
+
7
+
8
+ class Severity(str, Enum):
9
+ CRITICAL = "critical"
10
+ HIGH = "high"
11
+ MEDIUM = "medium"
12
+ LOW = "low"
13
+ INFO = "info"
14
+
15
+
16
+ class FindingStatus(str, Enum):
17
+ CONFIRMED = "confirmed"
18
+ LIKELY = "likely"
19
+ NEEDS_REVIEW = "needs_review"
20
+ DISMISSED = "dismissed"
21
+
22
+
23
+ class ValidationStatus(str, Enum):
24
+ PASSED = "passed"
25
+ FAILED = "failed"
26
+ SKIPPED = "skipped"
27
+ UNAVAILABLE = "unavailable"
28
+ NOT_RUN = "not_run"
29
+ NOT_APPLICABLE = "not_applicable"
30
+
31
+
32
+ @dataclass
33
+ class EvidenceItem:
34
+ path: str
35
+ start_line: int
36
+ end_line: int | None = None
37
+ excerpt_hash: str | None = None
38
+
39
+ def to_dict(self) -> dict[str, Any]:
40
+ data: dict[str, Any] = {
41
+ "path": self.path,
42
+ "start_line": self.start_line,
43
+ }
44
+ if self.end_line is not None:
45
+ data["end_line"] = self.end_line
46
+ if self.excerpt_hash:
47
+ data["excerpt_hash"] = self.excerpt_hash
48
+ return data
49
+
50
+
51
+ @dataclass
52
+ class Finding:
53
+ id: str
54
+ title: str
55
+ severity: Severity
56
+ category: str
57
+ status: FindingStatus
58
+ evidence: list[EvidenceItem]
59
+ description: str
60
+ recommendation: str
61
+ validation_status: ValidationStatus = ValidationStatus.NOT_RUN
62
+ validation_method: str = "static-analysis-and-review"
63
+ validation_details: str | None = None
64
+ impact: str | None = None
65
+ confidence: float = 1.0
66
+ schema_version: str = "1.0"
67
+ rule_id: str | None = None
68
+ rule_pack: str = "default-salesforce"
69
+ rule_pack_version: str = "1.0.0"
70
+
71
+ def to_dict(self) -> dict[str, Any]:
72
+ return {
73
+ "schema_version": self.schema_version,
74
+ "id": self.id,
75
+ "title": self.title,
76
+ "severity": self.severity.value,
77
+ "category": self.category,
78
+ "confidence": self.confidence,
79
+ "status": self.status.value,
80
+ "evidence": [e.to_dict() for e in self.evidence],
81
+ "description": self.description,
82
+ "impact": self.impact or "",
83
+ "recommendation": self.recommendation,
84
+ "validation": {
85
+ "method": self.validation_method,
86
+ "status": self.validation_status.value,
87
+ "details": self.validation_details or "",
88
+ },
89
+ "rule_id": self.rule_id or "",
90
+ "rule_pack": self.rule_pack,
91
+ "rule_pack_version": self.rule_pack_version,
92
+ }
@@ -0,0 +1,63 @@
1
+ """Policy Engine & Precedence Resolver supporting Modular Industry Packs."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+ import yaml
6
+
7
+
8
+ class PolicyEngine:
9
+ def __init__(
10
+ self,
11
+ defaults_file: Path,
12
+ packs_dir: Path | None = None,
13
+ active_packs: list[str] | None = None,
14
+ ) -> None:
15
+ self.defaults_file = defaults_file
16
+ self.packs_dir = packs_dir
17
+ self.rules: dict[str, dict[str, Any]] = {}
18
+ self.loaded_packs: list[str] = ["default-salesforce"]
19
+ self.load_defaults()
20
+
21
+ if packs_dir and active_packs:
22
+ for pack in active_packs:
23
+ self.load_pack(pack)
24
+
25
+ def load_defaults(self) -> None:
26
+ if not self.defaults_file.exists():
27
+ return
28
+ with open(self.defaults_file, "r", encoding="utf-8") as f:
29
+ data = yaml.safe_load(f)
30
+ if isinstance(data, dict) and "rules" in data:
31
+ for rule in data["rules"]:
32
+ if isinstance(rule, dict) and "id" in rule:
33
+ self.rules[rule["id"]] = rule
34
+
35
+ def load_pack(self, pack_name: str) -> bool:
36
+ if not self.packs_dir or not self.packs_dir.exists():
37
+ return False
38
+
39
+ # Support name as "omnistudio" or "omnistudio.yaml"
40
+ clean_name = pack_name[:-5] if pack_name.endswith(".yaml") else pack_name
41
+ pack_file = self.packs_dir / f"{clean_name}.yaml"
42
+ if not pack_file.exists():
43
+ return False
44
+
45
+ with open(pack_file, "r", encoding="utf-8") as f:
46
+ data = yaml.safe_load(f)
47
+ if isinstance(data, dict) and "rules" in data:
48
+ for rule in data["rules"]:
49
+ if isinstance(rule, dict) and "id" in rule:
50
+ self.rules[rule["id"]] = rule
51
+ if clean_name not in self.loaded_packs:
52
+ self.loaded_packs.append(clean_name)
53
+ return True
54
+ return False
55
+
56
+ def get_rule(self, rule_id: str) -> dict[str, Any] | None:
57
+ return self.rules.get(rule_id)
58
+
59
+ def get_enabled_rules(self) -> list[dict[str, Any]]:
60
+ return [r for r in self.rules.values() if r.get("enabled", True)]
61
+
62
+ def get_rules_by_category(self, category: str) -> list[dict[str, Any]]:
63
+ return [r for r in self.rules.values() if r.get("category") == category]
@@ -0,0 +1,36 @@
1
+ """Markdown and JSON Report Formatter."""
2
+
3
+ import json
4
+ from typing import Any
5
+ from askyara.core.finding_model import Finding
6
+
7
+
8
+ class ReportFormatter:
9
+ @staticmethod
10
+ def to_json(findings: list[Finding], metadata: dict[str, Any] | None = None) -> str:
11
+ payload = {
12
+ "metadata": metadata or {},
13
+ "total_findings": len(findings),
14
+ "findings": [f.to_dict() for f in findings],
15
+ }
16
+ return json.dumps(payload, indent=2)
17
+
18
+ @staticmethod
19
+ def to_markdown(findings: list[Finding], title: str = "AskYARA Review Report") -> str:
20
+ lines: list[str] = [f"# {title}", "", f"**Total Findings:** {len(findings)}", ""]
21
+ if not findings:
22
+ lines.append("Looks good. No issues found. Ship.")
23
+ return "\n".join(lines)
24
+
25
+ for i, f in enumerate(findings, 1):
26
+ lines.append(f"### {i}. [{f.severity.value.upper()}] {f.title}")
27
+ loc_str = ", ".join(f"`{e.path}:{e.start_line}`" for e in f.evidence)
28
+ lines.append(f"- **Location:** {loc_str}")
29
+ lines.append(f"- **Problem:** {f.description}")
30
+ if f.impact:
31
+ lines.append(f"- **Impact:** {f.impact}")
32
+ lines.append(f"- **Recommendation:** {f.recommendation}")
33
+ lines.append(f"- **Validation Status:** `{f.validation_status.value}`")
34
+ lines.append("")
35
+
36
+ return "\n".join(lines)
@@ -0,0 +1 @@
1
+ """Installer Package."""
@@ -0,0 +1,50 @@
1
+ """Environment and Diagnostic Health Check (`askyara doctor`)."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from askyara.tools.tool_registry import discover_tools
6
+ from askyara.tools.project_detector import detect_salesforce_project
7
+
8
+
9
+ def run_doctor(as_json: bool = False) -> int:
10
+ tools = discover_tools()
11
+ project = detect_salesforce_project(Path("."))
12
+
13
+ report = {
14
+ "status": "healthy",
15
+ "project": {
16
+ "root": project.root,
17
+ "confidence": project.confidence,
18
+ "markers": project.detected_markers,
19
+ "package_directories": project.package_directories,
20
+ "asset_counts": project.asset_counts,
21
+ "industry_packs": project.industry_packs,
22
+ },
23
+ "tooling": {name: {"available": t.available, "version": t.version} for name, t in tools.items()},
24
+ }
25
+
26
+ if as_json:
27
+ print(json.dumps(report, indent=2))
28
+ return 0
29
+
30
+ print("==================================================")
31
+ print(" AskYARA Environment Doctor ")
32
+ print("==================================================")
33
+ print(f"Project Root : {project.root}")
34
+ print(f"Salesforce Project : {'Yes' if project.confidence > 0.5 else 'Not detected'}")
35
+ print(f"Detected Markers : {', '.join(project.detected_markers) if project.detected_markers else 'None'}")
36
+ print(f"Industry Clouds : {', '.join(project.industry_packs) if project.industry_packs else 'None (Core Salesforce)'}")
37
+ print(f"Package Dirs : {', '.join(project.package_directories) if project.package_directories else 'None'}")
38
+ print("--------------------------------------------------")
39
+ print("Asset Counts:")
40
+ for asset, count in project.asset_counts.items():
41
+ if count > 0:
42
+ print(f" - {asset:<26}: {count}")
43
+ print("--------------------------------------------------")
44
+ print("Tooling Discovery:")
45
+ for name, t in tools.items():
46
+ status_str = f"Found ({t.version})" if t.available else "Missing / Not Installed"
47
+ print(f" - {name:<26}: {status_str}")
48
+ print("==================================================")
49
+
50
+ return 0
@@ -0,0 +1,49 @@
1
+ """Adapter Installation and Lifecycle Manager."""
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+
7
+ class AdapterManager:
8
+ @staticmethod
9
+ def install_to_project(target_dir: Path, agent: str) -> bool:
10
+ repo_root = Path(__file__).resolve().parent.parent.parent.parent
11
+ target_dir = target_dir.resolve()
12
+
13
+ if agent == "cursor":
14
+ dest = target_dir / ".cursor" / "rules"
15
+ dest.mkdir(parents=True, exist_ok=True)
16
+ src = repo_root / ".cursor" / "rules" / "askyara.mdc"
17
+ if src.exists():
18
+ shutil.copy2(src, dest / "askyara.mdc")
19
+ return True
20
+ elif agent == "windsurf":
21
+ dest = target_dir / ".windsurf" / "rules"
22
+ dest.mkdir(parents=True, exist_ok=True)
23
+ src = repo_root / ".windsurf" / "rules" / "askyara.md"
24
+ if src.exists():
25
+ shutil.copy2(src, dest / "askyara.md")
26
+ return True
27
+ elif agent == "cline":
28
+ dest = target_dir / ".clinerules"
29
+ dest.mkdir(parents=True, exist_ok=True)
30
+ src = repo_root / ".clinerules" / "askyara.md"
31
+ if src.exists():
32
+ shutil.copy2(src, dest / "askyara.md")
33
+ return True
34
+ elif agent == "kiro":
35
+ dest = target_dir / ".kiro" / "steering"
36
+ dest.mkdir(parents=True, exist_ok=True)
37
+ src = repo_root / ".kiro" / "steering" / "askyara.md"
38
+ if src.exists():
39
+ shutil.copy2(src, dest / "askyara.md")
40
+ return True
41
+ elif agent == "copilot":
42
+ dest = target_dir / ".github"
43
+ dest.mkdir(parents=True, exist_ok=True)
44
+ src = repo_root / ".github" / "copilot-instructions.md"
45
+ if src.exists():
46
+ shutil.copy2(src, dest / "copilot-instructions.md")
47
+ return True
48
+
49
+ return False
@@ -0,0 +1 @@
1
+ """Local Inspection Tools Package."""
@@ -0,0 +1,46 @@
1
+ """Git Diff & Status Utility."""
2
+
3
+ import subprocess
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+
8
+ @dataclass
9
+ class GitDiffSummary:
10
+ is_git_repo: bool
11
+ changed_files: list[str] = field(default_factory=list)
12
+ diff_text: str = ""
13
+
14
+
15
+ def get_git_diff_summary(repo_root: Path) -> GitDiffSummary:
16
+ try:
17
+ status_proc = subprocess.run(
18
+ ["git", "status", "--porcelain"],
19
+ cwd=str(repo_root),
20
+ capture_output=True,
21
+ text=True,
22
+ check=False,
23
+ timeout=10,
24
+ )
25
+ if status_proc.returncode != 0:
26
+ return GitDiffSummary(is_git_repo=False)
27
+
28
+ changed: list[str] = []
29
+ for line in status_proc.stdout.splitlines():
30
+ line = line.strip()
31
+ if line and len(line) > 3:
32
+ changed.append(line[3:])
33
+
34
+ diff_proc = subprocess.run(
35
+ ["git", "diff", "HEAD"],
36
+ cwd=str(repo_root),
37
+ capture_output=True,
38
+ text=True,
39
+ check=False,
40
+ timeout=15,
41
+ )
42
+ diff_text = diff_proc.stdout if diff_proc.returncode == 0 else ""
43
+
44
+ return GitDiffSummary(is_git_repo=True, changed_files=changed, diff_text=diff_text)
45
+ except (FileNotFoundError, subprocess.TimeoutExpired):
46
+ return GitDiffSummary(is_git_repo=False)
@@ -0,0 +1,154 @@
1
+ """Salesforce Project & Industry Cloud Detector."""
2
+
3
+ import json
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+
8
+ @dataclass
9
+ class SalesforceProjectProfile:
10
+ id: str
11
+ root: str
12
+ detected_markers: list[str] = field(default_factory=list)
13
+ confidence: float = 0.0
14
+ package_directories: list[str] = field(default_factory=list)
15
+ asset_counts: dict[str, int] = field(default_factory=dict)
16
+ industry_packs: list[str] = field(default_factory=list)
17
+
18
+
19
+ def detect_salesforce_project(target_dir: Path) -> SalesforceProjectProfile:
20
+ markers: list[str] = []
21
+ package_dirs: list[str] = []
22
+ confidence = 0.0
23
+ asset_counts: dict[str, int] = {
24
+ "apex_class": 0,
25
+ "apex_trigger": 0,
26
+ "lwc_bundle": 0,
27
+ "flow": 0,
28
+ "omni_data_transform": 0,
29
+ "omni_integration_procedure": 0,
30
+ "flex_card": 0,
31
+ }
32
+ industry_packs: list[str] = []
33
+
34
+ sfdx_config = target_dir / "sfdx-project.json"
35
+ if sfdx_config.is_file():
36
+ markers.append("sfdx-project.json")
37
+ confidence += 0.7
38
+ try:
39
+ with open(sfdx_config, "r", encoding="utf-8") as f:
40
+ data = json.load(f)
41
+ for entry in data.get("packageDirectories", []):
42
+ if "path" in entry:
43
+ package_dirs.append(entry["path"])
44
+ except Exception:
45
+ pass
46
+
47
+ force_app = target_dir / "force-app"
48
+ if force_app.is_dir():
49
+ markers.append("force-app")
50
+ confidence = min(1.0, confidence + 0.3)
51
+ if "force-app" not in package_dirs:
52
+ package_dirs.append("force-app")
53
+
54
+ # Search paths
55
+ search_dirs = [target_dir / p for p in package_dirs] if package_dirs else [target_dir]
56
+
57
+ for base in search_dirs:
58
+ if not base.exists():
59
+ continue
60
+
61
+ # Standard Core Assets
62
+ all_cls = list(base.glob("**/*.cls"))
63
+ all_triggers = list(base.glob("**/*.trigger"))
64
+ all_flows = list(base.glob("**/*.flow-meta.xml"))
65
+
66
+ asset_counts["apex_class"] += len(all_cls)
67
+ asset_counts["apex_trigger"] += len(all_triggers)
68
+ asset_counts["flow"] += len(all_flows)
69
+
70
+ lwc_dir = base / "main" / "default" / "lwc"
71
+ if lwc_dir.is_dir():
72
+ asset_counts["lwc_bundle"] += len([d for d in lwc_dir.iterdir() if d.is_dir()])
73
+
74
+ # OmniStudio Metadata Detection
75
+ odt_count = len(list(base.glob("**/omniDataTransforms*/**/*.json"))) + len(list(base.glob("**/omniDataTransforms*/**/*.xml")))
76
+ oip_count = len(list(base.glob("**/omniIntegrationProcedures*/**/*.json"))) + len(list(base.glob("**/omniIntegrationProcedures*/**/*.xml")))
77
+ fc_count = len(list(base.glob("**/flexCards*/**/*.json"))) + len(list(base.glob("**/flexCards*/**/*.xml")))
78
+
79
+ # Also check for sfdx-omnistudio in json files
80
+ omni_json_hits = 0
81
+ for jf in list(base.glob("**/*.json"))[:20]:
82
+ try:
83
+ txt = jf.read_text(encoding="utf-8", errors="ignore")
84
+ if "sfdx-omnistudio" in txt or "omniDataTransforms" in str(jf):
85
+ omni_json_hits += 1
86
+ except Exception:
87
+ pass
88
+
89
+ asset_counts["omni_data_transform"] += odt_count + (1 if omni_json_hits > 0 and odt_count == 0 else 0)
90
+ asset_counts["omni_integration_procedure"] += oip_count
91
+ asset_counts["flex_card"] += fc_count
92
+
93
+ if odt_count > 0 or oip_count > 0 or fc_count > 0 or omni_json_hits > 0:
94
+ if "omnistudio" not in industry_packs:
95
+ industry_packs.append("omnistudio")
96
+ markers.append("omnistudio-metadata")
97
+
98
+ # Industry Cloud Namespaces & Object Signature Scanning
99
+ all_paths_str = " ".join([str(p) for p in base.glob("**/*")])
100
+
101
+ # Scan code text of Apex classes for industry namespace signatures
102
+ sample_code = ""
103
+ for code_file in (all_cls + all_triggers)[:25]:
104
+ try:
105
+ sample_code += " " + code_file.read_text(encoding="utf-8", errors="ignore")[:2000]
106
+ except Exception:
107
+ pass
108
+
109
+ combined_search_space = all_paths_str + " " + sample_code
110
+
111
+ if "FinServ" in combined_search_space or any(base.glob("**/FinServ__*")):
112
+ if "financial-services-cloud" not in industry_packs:
113
+ industry_packs.append("financial-services-cloud")
114
+ markers.append("fsc-namespace")
115
+
116
+ if "HealthCloud" in combined_search_space or any(base.glob("**/HealthCloudGA__*")):
117
+ if "health-cloud" not in industry_packs:
118
+ industry_packs.append("health-cloud")
119
+ markers.append("health-cloud-namespace")
120
+
121
+ if "SalesAgreement" in combined_search_space or "AdvAcctForecast" in combined_search_space:
122
+ if "manufacturing-cloud" not in industry_packs:
123
+ industry_packs.append("manufacturing-cloud")
124
+ markers.append("manufacturing-cloud-markers")
125
+
126
+ if "RetailStore" in combined_search_space or "AssessmentTask" in combined_search_space:
127
+ if "consumer-goods-cloud" not in industry_packs:
128
+ industry_packs.append("consumer-goods-cloud")
129
+ markers.append("consumer-goods-markers")
130
+
131
+ if "Vehicle" in combined_search_space:
132
+ if "automotive-cloud" not in industry_packs:
133
+ industry_packs.append("automotive-cloud")
134
+ markers.append("automotive-markers")
135
+
136
+ if "Premise" in combined_search_space or "ServicePoint" in combined_search_space:
137
+ if "energy-utilities-cloud" not in industry_packs:
138
+ industry_packs.append("energy-utilities-cloud")
139
+ markers.append("energy-utilities-markers")
140
+
141
+ if "BusinessLicense" in combined_search_space or "RegulatoryTransaction" in combined_search_space:
142
+ if "public-sector-solutions" not in industry_packs:
143
+ industry_packs.append("public-sector-solutions")
144
+ markers.append("public-sector-markers")
145
+
146
+ return SalesforceProjectProfile(
147
+ id=f"project:local:{target_dir.resolve().name}",
148
+ root=str(target_dir.resolve()),
149
+ detected_markers=markers,
150
+ confidence=confidence,
151
+ package_directories=package_dirs,
152
+ asset_counts=asset_counts,
153
+ industry_packs=industry_packs,
154
+ )
@@ -0,0 +1,35 @@
1
+ """Secret, Regulatory, and Industry-Sensitive Token Redactor."""
2
+
3
+ import re
4
+
5
+ SECRET_PATTERNS = [
6
+ re.compile(r"(?i)(bearer\s+[a-zA-Z0-9_\-\.]{20,})"),
7
+ re.compile(r"(?i)(password\s*[:=]\s*['\"][^'\"]+['\"])"),
8
+ re.compile(r"(?i)(client_secret\s*[:=]\s*['\"][^'\"]+['\"])"),
9
+ re.compile(r"(?i)(api[_-]?key\s*[:=]\s*['\"][^'\"]+['\"])"),
10
+ ]
11
+
12
+ # Healthcare HIPAA / PHI Patterns
13
+ PHI_PATTERNS = [
14
+ re.compile(r"(?i)(mrn\s*[:=]\s*['\"]?[A-Z0-9\-]{6,12}['\"]?)"),
15
+ re.compile(r"(?i)(npi\s*[:=]\s*['\"]?\d{10}['\"]?)"),
16
+ ]
17
+
18
+ # Financial Services PCI-DSS / PII Patterns
19
+ PCI_PII_PATTERNS = [
20
+ # 13-19 digit credit card numbers (Visa, MC, Amex)
21
+ re.compile(r"\b(?:\d{4}[ -]?){3}\d{4}\b"),
22
+ # SSN (999-99-9999)
23
+ re.compile(r"\b\d{3}-\d{2}-\d{4}\b"),
24
+ ]
25
+
26
+
27
+ def redact_secrets(text: str) -> str:
28
+ redacted = text
29
+ for pattern in SECRET_PATTERNS:
30
+ redacted = pattern.sub("[REDACTED_SECRET]", redacted)
31
+ for pattern in PHI_PATTERNS:
32
+ redacted = pattern.sub("[REDACTED_PHI]", redacted)
33
+ for pattern in PCI_PII_PATTERNS:
34
+ redacted = pattern.sub("[REDACTED_PCI_PII]", redacted)
35
+ return redacted
@@ -0,0 +1,40 @@
1
+ """External Tool Discovery."""
2
+
3
+ import shutil
4
+ import subprocess
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass
9
+ class ToolStatus:
10
+ name: str
11
+ available: bool
12
+ version: str | None = None
13
+
14
+
15
+ def check_tool(name: str, version_arg: str = "--version") -> ToolStatus:
16
+ path = shutil.which(name)
17
+ if not path:
18
+ return ToolStatus(name=name, available=False)
19
+
20
+ try:
21
+ proc = subprocess.run(
22
+ [name, version_arg],
23
+ capture_output=True,
24
+ text=True,
25
+ timeout=5,
26
+ check=False,
27
+ )
28
+ ver = proc.stdout.splitlines()[0].strip() if proc.stdout else None
29
+ return ToolStatus(name=name, available=True, version=ver)
30
+ except Exception:
31
+ return ToolStatus(name=name, available=True, version="unknown")
32
+
33
+
34
+ def discover_tools() -> dict[str, ToolStatus]:
35
+ return {
36
+ "git": check_tool("git"),
37
+ "sf": check_tool("sf", "--version"),
38
+ "sfdx": check_tool("sfdx", "--version"),
39
+ "python": check_tool("python3", "--version"),
40
+ }
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: askyara
3
+ Version: 0.1.0
4
+ Summary: Salesforce-native AI engineering companion and multi-agent toolkit
5
+ Author: AskYARA Engineering Team
6
+ License-Expression: Apache-2.0
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Quality Assurance
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: jsonschema>=4.20.0
17
+ Requires-Dist: pyyaml>=6.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
20
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
21
+ Requires-Dist: ruff>=0.3.0; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # AskYARA Salesforce Engineering
25
+
26
+ > **Salesforce-native AI engineering companion and multi-agent toolkit.**
27
+ > Understands Apex, LWC, Flows, Salesforce metadata, governor limits, security, and project architecture.
28
+
29
+ AskYARA brings Salesforce engineering intelligence directly into your favorite coding agent—whether you use **Cursor**, **Claude Code**, **GitHub Copilot**, **Windsurf**, **Cline**, **Kiro**, or **Codex**.
30
+
31
+ ---
32
+
33
+ ## Key Capabilities
34
+
35
+ - **Platform-Aware Review (`/askyara-review`):** Inspects changed files, surrounding callers, governor limits (SOQL/DML in loops), CRUD/FLS access, and asynchronous boundaries.
36
+ - **Repository Audit (`/askyara-audit`):** Scans the whole Salesforce DX project for anti-patterns, missing tests, and architectural technical debt.
37
+ - **Evidence-Backed Findings:** Every reported issue includes the file path, exact line numbers, severity, rationale, and recommended fix.
38
+ - **Deterministic Local Tools:** Interfaces with Git, Salesforce CLI (`sf`), and Salesforce Code Analyzer / PMD when installed.
39
+ - **Zero Cloud Requirement:** Local-first, privacy-preserving, and read-only by default.
40
+
41
+ ---
42
+
43
+ ## Quickstart
44
+
45
+ ### 1. Install into Your Coding Agent
46
+
47
+ See [INSTALL.md](INSTALL.md) for agent-specific instructions.
48
+
49
+ For **Claude Code**:
50
+ ```bash
51
+ # In Claude Code CLI or Desktop
52
+ /plugin install askyara
53
+ ```
54
+
55
+ For **GitHub Copilot**:
56
+ ```bash
57
+ # Workspace instruction file
58
+ cp .github/copilot-instructions.md your-project/.github/copilot-instructions.md
59
+ ```
60
+
61
+ For **Cursor / Windsurf / Cline / Kiro**:
62
+ Copy the matching rule into your workspace:
63
+ ```bash
64
+ # Cursor
65
+ cp .cursor/rules/askyara.mdc your-project/.cursor/rules/
66
+
67
+ # Windsurf
68
+ cp .windsurf/rules/askyara.md your-project/.windsurf/rules/
69
+
70
+ # Cline
71
+ cp .clinerules/askyara.md your-project/.clinerules/
72
+
73
+ # Kiro
74
+ cp .kiro/steering/askyara.md your-project/.kiro/steering/
75
+ ```
76
+
77
+ ### 2. Run AskYARA CLI Directly
78
+
79
+ ```bash
80
+ # Verify installation and discover local Salesforce tools
81
+ askyara doctor
82
+
83
+ # Review local changes in an SFDX project
84
+ askyara review
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Commands
90
+
91
+ | Command | What it does |
92
+ | :--- | :--- |
93
+ | `/askyara [query]` | Ask questions about the current Salesforce project: architecture, Apex classes, LWC components, Flows, and metadata structure. |
94
+ | `/askyara-review` | Review the current diff like a Salesforce Technical Architect: governor limits (SOQL/DML in loops), CRUD/FLS security, sharing context, bulkification, and test quality. Name a target to narrow or widen: `uncommitted`, `staged`, `branch`, or a PR link. |
95
+ | `/askyara-audit` | Comprehensive quality, security, and architecture audit of the whole Salesforce DX repository, ranked by what to fix first. |
96
+ | `/askyara-security` | Focused security audit: sharing models (`with sharing`), CRUD/FLS enforcement, dynamic SOQL injection, and secret/credential exposure. |
97
+ | `/askyara-test` | Identify missing test coverage, test quality flaws (missing assertions), and generate bulk test scenarios. |
98
+ | `/askyara-help` | Quick reference for available commands, installed skills, and diagnostic status. |
99
+
100
+ Commands need a skill-capable host (Claude Code, Codex, Devin CLI, Github Copilot, Kiro, Cline, Windsurf). In Claude Code and hosts with slash-command support, invoke them directly as `/askyara-review`. In namespaced plugin environments, they are available as `/askyara:askyara-review`. The instruction-only adapters (Cursor's rule file, Windsurf, Cline, Copilot and Kiro `AGENTS.md`) apply the always-on Salesforce engineering rules across all prompts without requiring slash commands.
101
+
102
+ ---
103
+
104
+
105
+ ## Salesforce Industries Support
106
+
107
+ AskYARA includes modular **Industry Capability Packs** that bring domain-aware architecture and review rules to specialized Salesforce clouds:
108
+
109
+ - **OmniStudio (`omnistudio`):** DataRaptor Turbo Extract enforcement, unindexed filter warnings, and Integration Procedure Loop Block limits.
110
+ - **Financial Services Cloud (`financial-services-cloud`):** Synchronous RBL rollup limits, Compliant Information Barriers (CIB), Person Account handling.
111
+ - **Health Cloud (`health-cloud`):** HIPAA/PHI debug log masking, FHIR CodeSet standard mappings, and Care Team data boundaries.
112
+ - **Manufacturing Cloud (`manufacturing-cloud`):** Data Processing Engine (DPE) forecast delegation, Sales Agreement row-locking prevention.
113
+ - **Consumer Goods / CPG (`consumer-goods-cloud`):** Mobile in-store sync conflict resolution, Trade Promotion Management (TPM) budget calculation boundaries.
114
+ - **Automotive Cloud (`automotive-cloud`):** Telematics anti-patterns (Data Cloud vs sObjects), warranty claim idempotency.
115
+ - **Energy & Utilities (`energy-utilities-cloud`):** Tariff rating callouts in loops, Premise-to-ServicePoint hierarchy checks.
116
+ - **Public Sector Solutions (`public-sector-solutions`):** Citizen data isolation, Guest User security, BRE Decision Matrix coverage.
117
+
118
+ ### Examples: Using AskYARA with Industry Clouds
119
+
120
+ AskYARA **auto-detects** industry metadata (e.g. `FinServ__`, `HealthCloudGA__`, `omniDataTransforms/`, `SalesAgreement`). You can also specify packs explicitly:
121
+
122
+ ```bash
123
+ # Verify detected industry clouds and active rule packs
124
+ askyara doctor
125
+
126
+ # Review local changes with a specific industry pack
127
+ askyara review --pack omnistudio
128
+
129
+ # Audit repository against Financial Services Cloud rules
130
+ askyara audit --pack financial-services-cloud
131
+ ```
132
+
133
+ **In your Coding Agent (Cursor, Claude Code, Copilot, Antigravity):**
134
+ - *OmniStudio:* `/askyara "Review the newly added ClaimSubmission Integration Procedure and DataRaptors for scalability"`
135
+ - *Financial Services:* `/askyara-review` (evaluates Rollup By Lookup limits and CIB ethical walls)
136
+ - *Health Cloud:* `/askyara-security` (verifies zero PHI leakage and FHIR CodeSet compliance)
137
+ - *Manufacturing:* `/askyara-review` (verifies forecast updates are dispatched to Data Processing Engine jobs)
138
+
139
+ See the full [Salesforce Industries Guide](docs/product/industries-guide.md) for detailed checklists and finding examples.
140
+
141
+ ---
142
+
143
+ ## License
144
+
145
+ Licensed under the [Apache License, Version 2.0](LICENSE).
@@ -0,0 +1,20 @@
1
+ askyara/__init__.py,sha256=QhwLNnm1lwMkgF8YdagUbL-5-7OO-zMJ-q8F8Nt1C4k,74
2
+ askyara/cli.py,sha256=YESWmekjJtAhH2lQxCxLKibVlmV8gkHHNtzdG5qEszg,3436
3
+ askyara/core/__init__.py,sha256=NjMpnEQeA6kEGb65IFiediUTzgetFgZ-DA7eK8fyxPA,27
4
+ askyara/core/command_registry.py,sha256=ynoYEQk8T3YoMF6la-1Lfb4ScEkL35rv7UWSaQNzq7c,853
5
+ askyara/core/finding_model.py,sha256=a2bbKMlpJitAGpmT_yRvRfOZmcvxQsLJtna9NAfrwQQ,2543
6
+ askyara/core/policy_engine.py,sha256=ws61GFLorm2OZWq3pdm5znNN2cXgIh5LYXA4KiYFdCA,2351
7
+ askyara/core/report_formatter.py,sha256=V7J5N3gLgkA3QBOlmb8MU0WMte2Y39BU9fXQDdno_d4,1416
8
+ askyara/installer/__init__.py,sha256=TU42vE3y4Thn0dy-E2uHMQN5JNu2NLRzCl9KZfOcIZk,25
9
+ askyara/installer/doctor.py,sha256=M4j-Bldr5EgxE9801eC2bL7rhwnXxdkhquyqy7f5-Ew,2172
10
+ askyara/installer/manager.py,sha256=Nq3OcQD3aAjPmI-wUBWZdbOYgld0368Pq9g6KauZSbY,1901
11
+ askyara/tools/__init__.py,sha256=x-_uHwpCw4rm5fmPY88EHt8Eo8HQylCK8N6ffCoPN3s,38
12
+ askyara/tools/git_diff.py,sha256=XLHhbOWTIzd-xRQlu8dFEP34PQVHKb-IzFDlKv7rg-A,1348
13
+ askyara/tools/project_detector.py,sha256=_sZy-Uv5Ier7GnSHxs9pLNLkpXcZwls8MvFFBNYEcYw,6360
14
+ askyara/tools/redaction.py,sha256=ImxaGVDA-ZOwg9SLxe8PV6c9sLOOXi0Ql_Kgpr-xBbw,1136
15
+ askyara/tools/tool_registry.py,sha256=ySvH4cB9WOCuS_9Qk9rMLH0sSCP6_YCi7fN7hMlkJXQ,1043
16
+ askyara-0.1.0.dist-info/METADATA,sha256=7dUfWiLmh801ET7e2q_2-mBbMDN7o06CBto-v-HV6NI,6897
17
+ askyara-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
18
+ askyara-0.1.0.dist-info/entry_points.txt,sha256=UzbmEN0237DlgLj3cG4WUcKySEu7iBSnQLU51fHIaHM,45
19
+ askyara-0.1.0.dist-info/licenses/LICENSE,sha256=FcUyRljJ99LLfqP1fotCeJEMXKYYQEzf2ufgjHtHhTg,5236
20
+ askyara-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ askyara = askyara.cli:main
@@ -0,0 +1,100 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work.
38
+
39
+ "Derivative Works" shall mean any work, whether in Source or Object
40
+ form, that is based on (or derived from) the Work and for which the
41
+ editorial revisions, annotations, elaborations, or other modifications
42
+ represent, as a whole, an original work of authorship.
43
+
44
+ "Contribution" shall mean any work of authorship, including
45
+ the original version of the Work and any modifications or additions
46
+ to that Work or Derivative Works thereof, that is intentionally
47
+ submitted to Licensor for inclusion in the Work by the copyright owner
48
+ or by an individual or Legal Entity authorized to submit on behalf of
49
+ the copyright owner.
50
+
51
+ 2. Grant of Copyright License. Subject to the terms and conditions of
52
+ this License, each Contributor hereby grants to You a perpetual,
53
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
54
+ copyright license to reproduce, prepare Derivative Works of,
55
+ publicly display, publicly perform, sublicense, and distribute the
56
+ Work and such Derivative Works in Source or Object form.
57
+
58
+ 3. Grant of Patent License. Subject to the terms and conditions of
59
+ this License, each Contributor hereby grants to You a perpetual,
60
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
61
+ patent license to make, have made, use, offer to sell, sell, import,
62
+ and otherwise transfer the Work.
63
+
64
+ 4. Redistribution. You may reproduce and distribute copies of the
65
+ Work or Derivative Works thereof in any medium, with or without
66
+ modifications, and in Source or Object form, provided that You
67
+ meet the following conditions:
68
+
69
+ (a) You must give any other recipients of the Work or Derivative
70
+ Works a copy of this License; and
71
+ (b) You must cause any modified files to carry prominent notices
72
+ stating that You changed the files; and
73
+ (c) You must retain, in the Source form of any Derivative Works
74
+ that You distribute, all copyright, patent, trademark, and
75
+ attribution notices from the Source form of the Work; and
76
+ (d) If the Work includes a "NOTICE" text file as part of its
77
+ distribution, then any Derivative Works that You distribute must
78
+ include a readable copy of the attribution notices contained
79
+ within such NOTICE file.
80
+
81
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
82
+ any Contribution intentionally submitted for inclusion in the Work
83
+ by You to the Licensor shall be under the terms and conditions of
84
+ this License, without any additional terms or conditions.
85
+
86
+ 6. Trademarks. This License does not grant permission to use the trade
87
+ names, trademarks, service marks, or product names of the Licensor.
88
+
89
+ 7. Disclaimer of Warranty. Unless required by applicable law or
90
+ agreed to in writing, Licensor provides the Work on an "AS IS" BASIS,
91
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
92
+
93
+ 8. Limitation of Liability. In no event and under no legal theory
94
+ shall any Contributor be liable to You for damages, including any
95
+ direct, indirect, special, incidental, or consequential damages.
96
+
97
+ 9. Accepting Warranty or Additional Liability. While redistributing
98
+ the Work or Derivative Works thereof, You may choose to offer,
99
+ and charge a fee for, acceptance of support, warranty, indemnity,
100
+ or other liability obligations.