@salesforce/afv-skills 1.53.0 → 1.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/package.json +1 -1
  2. package/skills/dx-org-analyze/README.md +310 -0
  3. package/skills/dx-org-analyze/SKILL.md +261 -0
  4. package/skills/dx-org-analyze/references/collection-details.md +88 -0
  5. package/skills/dx-org-analyze/references/report-format.md +67 -0
  6. package/skills/dx-org-analyze/scripts/collect_org_data.py +823 -0
  7. package/skills/dx-org-analyze/scripts/compute_diff.py +1103 -0
  8. package/skills/dx-org-analyze/scripts/introspect_org.py +436 -0
  9. package/skills/dx-org-analyze/tests/README.md +46 -0
  10. package/skills/dx-org-analyze/tests/fixtures/org_a.json +76 -0
  11. package/skills/dx-org-analyze/tests/fixtures/org_b.json +70 -0
  12. package/skills/dx-org-analyze/tests/test_compute_diff.sh +284 -0
  13. package/skills/dx-org-analyze/tests/test_consistency.sh +428 -0
  14. package/skills/experience-design-validate/SKILL.md +163 -0
  15. package/skills/experience-design-validate/references/ai.md +54 -0
  16. package/skills/experience-design-validate/references/components.md +61 -0
  17. package/skills/experience-design-validate/references/craft.md +158 -0
  18. package/skills/experience-design-validate/references/data.md +59 -0
  19. package/skills/experience-design-validate/references/forms-flows.md +57 -0
  20. package/skills/experience-design-validate/references/interaction.md +53 -0
  21. package/skills/experience-design-validate/references/navigation.md +51 -0
  22. package/skills/experience-design-validate/references/performance.md +56 -0
  23. package/skills/experience-design-validate/references/records.md +60 -0
  24. package/skills/experience-design-validate/references/responsive.md +48 -0
  25. package/skills/experience-design-validate/references/scoring-rubric.md +257 -0
  26. package/skills/experience-design-validate/references/state.md +53 -0
  27. package/skills/experience-design-validate/references/trust.md +47 -0
  28. package/skills/experience-design-validate/references/usability.md +35 -0
  29. package/skills/experience-design-validate/references/visual-system.md +183 -0
  30. package/skills/service-agentforce-contact-center-coordinate/SKILL.md +170 -0
  31. package/skills/service-agentforce-contact-center-coordinate/assets/escalation-flow.flow-meta.xml +72 -0
  32. package/skills/service-agentforce-contact-center-coordinate/assets/omni-flow.flow-meta.xml +85 -0
  33. package/skills/service-agentforce-contact-center-coordinate/assets/report-template.md +52 -0
  34. package/skills/service-agentforce-contact-center-coordinate/references/agentforce-prerequisite.md +31 -0
  35. package/skills/service-agentforce-contact-center-coordinate/references/messaging_channel.md +76 -0
  36. package/skills/service-agentforce-contact-center-coordinate/references/number_management_api.md +78 -0
  37. package/skills/service-agentforce-contact-center-coordinate/references/omni-flow-routing.md +74 -0
  38. package/skills/service-agentforce-contact-center-coordinate/references/setup_summary.md +36 -0
  39. package/skills/service-agentforce-contact-center-coordinate/references/verification_and_errors.md +42 -0
  40. package/skills/service-agentforce-contact-center-coordinate/scripts/check-agentforce-prereq.sh +54 -0
  41. package/skills/service-agentforce-contact-center-coordinate/scripts/create-routing-flows.sh +49 -0
  42. package/skills/service-agentforce-contact-center-coordinate/scripts/create-voice-agent.sh +73 -0
  43. package/skills/service-agentforce-contact-center-coordinate/scripts/create-voice-channel.sh +66 -0
  44. package/skills/service-agentforce-contact-center-coordinate/scripts/fetch-numbers.sh +21 -0
  45. package/skills/service-agentforce-contact-center-coordinate/scripts/lib.sh +46 -0
  46. package/skills/service-agentforce-contact-center-coordinate/scripts/prepare-agent-workdir.sh +21 -0
  47. package/skills/service-agentforce-contact-center-coordinate/scripts/procure-number.sh +24 -0
  48. package/skills/service-agentforce-contact-center-coordinate/scripts/resolve-acc-queue.sh +27 -0
  49. package/skills/service-agentforce-contact-center-coordinate/scripts/resolve-channel-line.sh +37 -0
  50. package/skills/service-agentforce-contact-center-coordinate/scripts/resolve-flow-definition.sh +33 -0
  51. package/skills/service-agentforce-contact-center-coordinate/scripts/verify-number-live.sh +63 -0
@@ -0,0 +1,436 @@
1
+ #!/usr/bin/env python3
2
+ """Generate a single-org introspection report.
3
+
4
+ Takes one collected org data directory (output of collect_org_data.py)
5
+ and produces a summary report in markdown and/or JSON.
6
+
7
+ Usage:
8
+ introspect_org.py --org /tmp/org-data --output /tmp/report
9
+ introspect_org.py --org /tmp/org-data --output /tmp/report --format markdown
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import os
15
+ import sys
16
+ from datetime import date
17
+
18
+ # ── Reuse categorization from compute_diff ──────────────────────────
19
+
20
+ PERM_CATEGORIES = [
21
+ ("Commerce", ["commerce", "b2b", "b2c", "order", "cart", "store", "checkout"]),
22
+ ("Platform/API", ["platform", "api", "rest", "soap", "bulk", "streaming", "connect", "metadata"]),
23
+ ("Security", ["security", "auth", "login", "mfa", "sso", "encryption", "certificate", "session", "password", "oauth"]),
24
+ ("Einstein/AI", ["einstein", "ai", "predict", "ml", "copilot", "agent"]),
25
+ ("Service", ["service", "case", "knowledge", "chat", "messaging", "omni", "entitlement"]),
26
+ ("Data", ["data", "record", "report", "dashboard", "analytics", "tableau"]),
27
+ ("Trialforce", ["trial", "trialforce", "tso", "tmo", "signup", "template", "edition"]),
28
+ ("Sales", ["sales", "opportunity", "lead", "forecast", "quote", "contract"]),
29
+ ("Work/HR", ["work", "hr", "employee", "shift", "wellness"]),
30
+ ]
31
+
32
+ VALUE_CATEGORIES = [
33
+ ("Identity", ["name", "edition", "org", "domain", "id", "type", "instance", "locale"]),
34
+ ("Storage", ["storage", "file", "size", "space", "quota"]),
35
+ ("Feature Limits", ["max", "limit", "count", "num", "enable"]),
36
+ ("API", ["api", "call", "request", "bandwidth"]),
37
+ ]
38
+
39
+
40
+ def classify(name, categories, default="Other"):
41
+ lower = name.lower()
42
+ for cat, keywords in categories:
43
+ if any(kw in lower for kw in keywords):
44
+ return cat
45
+ return default
46
+
47
+
48
+ # ── Data loading (same as compute_diff.py) ──────────────────────────
49
+
50
+ def load_org_data(path):
51
+ if os.path.isdir(path):
52
+ combined = os.path.join(path, "combined.json")
53
+ if os.path.exists(combined):
54
+ with open(combined) as f:
55
+ return json.load(f)
56
+ manifest_path = os.path.join(path, "manifest.json")
57
+ if not os.path.exists(manifest_path):
58
+ raise FileNotFoundError(f"Directory {path} has no manifest.json or combined.json")
59
+ with open(manifest_path) as f:
60
+ manifest = json.load(f)
61
+ data = {}
62
+ for cat, fname in manifest.get("files", {}).items():
63
+ fpath = os.path.join(path, fname)
64
+ with open(fpath) as f:
65
+ chunk = json.load(f)
66
+ if isinstance(chunk, dict):
67
+ data.update(chunk)
68
+ else:
69
+ data[cat] = chunk
70
+ return data
71
+
72
+ with open(path) as f:
73
+ return json.load(f)
74
+
75
+
76
+ # ── Analysis ────────────────────────────────────────────────────────
77
+
78
+ def analyze_metadata(data):
79
+ types = data.get("metadata_types", [])
80
+ components = data.get("metadata_components", {})
81
+ by_type = {}
82
+ total = 0
83
+ namespaced_total = 0
84
+ for mtype, items in sorted(components.items()):
85
+ org_native = [c for c in items if not c.get("namespace")]
86
+ namespaced = [c for c in items if c.get("namespace")]
87
+ org_native_names = sorted(
88
+ c.get("fullName") or c.get("name", "") for c in org_native
89
+ )
90
+ namespaced_by_ns = {}
91
+ for c in namespaced:
92
+ ns = c.get("namespace", "unknown")
93
+ namespaced_by_ns.setdefault(ns, []).append(
94
+ c.get("fullName") or c.get("name", "")
95
+ )
96
+ for ns in namespaced_by_ns:
97
+ namespaced_by_ns[ns] = sorted(namespaced_by_ns[ns])
98
+ by_type[mtype] = {
99
+ "total": len(items),
100
+ "org_native": len(org_native),
101
+ "namespaced": len(namespaced),
102
+ "org_native_names": org_native_names,
103
+ "namespaced_by_ns": namespaced_by_ns,
104
+ }
105
+ total += len(org_native)
106
+ namespaced_total += len(namespaced)
107
+ return {
108
+ "types_discovered": len(types),
109
+ "types_with_components": len(components),
110
+ "total_org_native": total,
111
+ "total_namespaced": namespaced_total,
112
+ "by_type": by_type,
113
+ }
114
+
115
+
116
+ def analyze_permissions(data):
117
+ perms = data.get("permissions", {})
118
+ by_category = {}
119
+ enabled = 0
120
+ disabled = 0
121
+ non_boolean = 0
122
+ for name, val in perms.items():
123
+ cat = classify(name, PERM_CATEGORIES)
124
+ if cat not in by_category:
125
+ by_category[cat] = {"enabled": 0, "disabled": 0, "non_boolean": 0, "items": []}
126
+ v = val if isinstance(val, str) else val.get("value", str(val)) if isinstance(val, dict) else str(val)
127
+ if v.lower() in ("enabled", "true"):
128
+ by_category[cat]["enabled"] += 1
129
+ enabled += 1
130
+ elif v.lower() in ("disabled", "false"):
131
+ by_category[cat]["disabled"] += 1
132
+ disabled += 1
133
+ else:
134
+ by_category[cat]["non_boolean"] += 1
135
+ non_boolean += 1
136
+ by_category[cat]["items"].append((name, v))
137
+ return {
138
+ "total": len(perms),
139
+ "enabled": enabled,
140
+ "disabled": disabled,
141
+ "non_boolean": non_boolean,
142
+ "by_category": by_category,
143
+ }
144
+
145
+
146
+ def analyze_org_values(data):
147
+ vals = data.get("org_values", {})
148
+ by_category = {}
149
+ for name, val in vals.items():
150
+ cat = classify(name, VALUE_CATEGORIES)
151
+ if cat not in by_category:
152
+ by_category[cat] = []
153
+ by_category[cat].append((name, val))
154
+ return {"total": len(vals), "by_category": by_category}
155
+
156
+
157
+ def analyze_packages(data):
158
+ pkgs = data.get("installed_packages", [])
159
+ return {
160
+ "count": len(pkgs),
161
+ "packages": [
162
+ {"name": p.get("name", ""), "namespace": p.get("namespace") or "", "version": p.get("version", "")}
163
+ for p in pkgs
164
+ ],
165
+ }
166
+
167
+
168
+ def analyze_licenses(data):
169
+ lics = data.get("licenses", {})
170
+ result = {}
171
+ for category in ("user", "permission_set", "package"):
172
+ items = lics.get(category, [])
173
+ result[category] = {
174
+ "count": len(items),
175
+ "items": items,
176
+ }
177
+ return result
178
+
179
+
180
+ def analyze_system_permissions(data):
181
+ sp_raw = data.get("system_permissions", {})
182
+ if isinstance(sp_raw, dict):
183
+ sp = sp_raw.get("permission_sets", [])
184
+ else:
185
+ sp = sp_raw
186
+ total_enabled = 0
187
+ sets = []
188
+ for ps in sp:
189
+ name = ps.get("name", ps.get("label", "Unknown"))
190
+ perms = ps.get("permissions", {})
191
+ enabled = sum(1 for v in perms.values() if v)
192
+ total_enabled += enabled
193
+ sets.append({"name": name, "enabled": enabled, "total": len(perms)})
194
+ return {"permission_sets": len(sp), "total_enabled": total_enabled, "sets": sets}
195
+
196
+
197
+ def analyze_deep_data(data):
198
+ dd = data.get("deep_data", {})
199
+ result = {}
200
+ for category, records in dd.items():
201
+ result[category] = len(records) if isinstance(records, list) else 0
202
+ return result
203
+
204
+
205
+ # ── Render markdown ─────────────────────────────────────────────────
206
+
207
+ def render_markdown(data, meta, perms, vals, pkgs, lics, sys_perms, deep, label):
208
+ lines = []
209
+ lines.append(f"# Org Analysis Report: {label}\n")
210
+ lines.append(f"| Field | Value |")
211
+ lines.append(f"|-------|-------|")
212
+ lines.append(f"| **Org ID** | {data.get('org_id', 'N/A')} |")
213
+ lines.append(f"| **Name** | {data.get('org_name', 'N/A')} |")
214
+ lines.append(f"| **Edition** | {data.get('org_edition', 'N/A')} |")
215
+ lines.append(f"| **Date** | {date.today()} |")
216
+ lines.append("")
217
+
218
+ # Summary
219
+ lines.append("---\n")
220
+ lines.append("## Summary\n")
221
+ lines.append("| Category | Count |")
222
+ lines.append("|----------|-------|")
223
+ lines.append(f"| Metadata types discovered | {meta['types_discovered']} |")
224
+ lines.append(f"| Types with components | {meta['types_with_components']} |")
225
+ lines.append(f"| Org-native components | {meta['total_org_native']} |")
226
+ lines.append(f"| Namespaced components | {meta['total_namespaced']} |")
227
+ lines.append(f"| Org settings | {perms['total']} ({perms['enabled']} enabled, {perms['disabled']} disabled) |")
228
+ lines.append(f"| Org limits | {vals['total']} |")
229
+ lines.append(f"| Installed packages | {pkgs['count']} |")
230
+ lines.append(f"| System permission sets | {sys_perms['permission_sets']} |")
231
+ deep_total = sum(deep.values())
232
+ lines.append(f"| Deep data records | {deep_total} |")
233
+ errors = data.get("errors", [])
234
+ if errors:
235
+ lines.append(f"| Collection warnings | {len(errors)} |")
236
+ lines.append("")
237
+
238
+ # Metadata inventory
239
+ lines.append("---\n")
240
+ lines.append("## Metadata Inventory\n")
241
+ lines.append("| Type | Org-Native | Namespaced | Total |")
242
+ lines.append("|------|-----------|------------|-------|")
243
+ for mtype, counts in sorted(meta["by_type"].items()):
244
+ if counts["total"] > 0:
245
+ lines.append(f"| {mtype} | {counts['org_native']} | {counts['namespaced']} | {counts['total']} |")
246
+ lines.append(f"| **Total** | **{meta['total_org_native']}** | **{meta['total_namespaced']}** | **{meta['total_org_native'] + meta['total_namespaced']}** |")
247
+ lines.append("")
248
+
249
+ # Detailed metadata listing
250
+ lines.append("---\n")
251
+ lines.append("## Metadata Components by Type\n")
252
+ for mtype, counts in sorted(meta["by_type"].items()):
253
+ if counts["total"] == 0:
254
+ continue
255
+ lines.append(f"#### {mtype} ({counts['org_native']} org-native, {counts['namespaced']} namespaced)\n")
256
+ if counts["org_native_names"]:
257
+ lines.append(f"**Org-native ({counts['org_native']}):** " +
258
+ ", ".join(f"`{n}`" for n in counts["org_native_names"]))
259
+ lines.append("")
260
+ if counts["namespaced_by_ns"]:
261
+ for ns, names in sorted(counts["namespaced_by_ns"].items()):
262
+ lines.append(f"**Namespaced — `{ns}` ({len(names)}):** " +
263
+ ", ".join(f"`{n}`" for n in names))
264
+ lines.append("")
265
+ lines.append("")
266
+
267
+ # Installed packages
268
+ lines.append("---\n")
269
+ lines.append("## Installed Packages\n")
270
+ if pkgs["packages"]:
271
+ lines.append("| Package | Namespace | Version |")
272
+ lines.append("|---------|-----------|---------|")
273
+ for p in pkgs["packages"]:
274
+ lines.append(f"| {p['name']} | {p.get('namespace') or '—'} | {p['version']} |")
275
+ else:
276
+ lines.append("No installed packages.\n")
277
+ lines.append("")
278
+
279
+ # Org settings by category
280
+ lines.append("---\n")
281
+ lines.append("## Org Settings\n")
282
+ lines.append("| Category | Enabled | Disabled | Non-Boolean |")
283
+ lines.append("|----------|---------|----------|-------------|")
284
+ for cat in sorted(perms["by_category"].keys()):
285
+ info = perms["by_category"][cat]
286
+ lines.append(f"| {cat} | {info['enabled']} | {info['disabled']} | {info['non_boolean']} |")
287
+ lines.append(f"| **Total** | **{perms['enabled']}** | **{perms['disabled']}** | **{perms['non_boolean']}** |")
288
+ lines.append("")
289
+
290
+ # Org limits by category
291
+ lines.append("---\n")
292
+ lines.append("## Org Limits\n")
293
+ for cat in sorted(vals["by_category"].keys()):
294
+ items = vals["by_category"][cat]
295
+ lines.append(f"### {cat}\n")
296
+ lines.append("| Limit | Value |")
297
+ lines.append("|-------|-------|")
298
+ for name, val in sorted(items):
299
+ lines.append(f"| {name} | {val} |")
300
+ lines.append("")
301
+
302
+ # Licenses
303
+ lines.append("---\n")
304
+ lines.append("## Licenses\n")
305
+ for category in ("user", "permission_set", "package"):
306
+ info = lics[category]
307
+ title = category.replace("_", " ").title()
308
+ lines.append(f"### {title} Licenses ({info['count']})\n")
309
+ if info["items"]:
310
+ sample = info["items"][0]
311
+ if isinstance(sample, dict):
312
+ keys = [k for k in sample.keys() if k not in ("attributes",)]
313
+ if keys:
314
+ lines.append("| " + " | ".join(keys) + " |")
315
+ lines.append("| " + " | ".join(["---"] * len(keys)) + " |")
316
+ for item in info["items"]:
317
+ vals_row = []
318
+ for k in keys:
319
+ v = item.get(k, "")
320
+ if k in ("AllowedLicenses", "TotalLicenses") and v == -1:
321
+ v = "-1 (unlimited)"
322
+ vals_row.append(str(v))
323
+ lines.append("| " + " | ".join(vals_row) + " |")
324
+ else:
325
+ for item in info["items"]:
326
+ lines.append(f"- {item}")
327
+ else:
328
+ lines.append("None.\n")
329
+ lines.append("")
330
+
331
+ # System permissions
332
+ lines.append("---\n")
333
+ lines.append("## System Permissions\n")
334
+ lines.append(f"**{sys_perms['permission_sets']}** permission sets, **{sys_perms['total_enabled']}** total enabled permissions.\n")
335
+ if sys_perms["sets"]:
336
+ lines.append("| Permission Set | Enabled | Total Fields |")
337
+ lines.append("|---------------|---------|-------------|")
338
+ for s in sorted(sys_perms["sets"], key=lambda x: x["enabled"], reverse=True):
339
+ lines.append(f"| {s['name']} | {s['enabled']} | {s['total']} |")
340
+ lines.append("")
341
+
342
+ # Deep data
343
+ lines.append("---\n")
344
+ lines.append("## Deep Data\n")
345
+ lines.append("| Category | Records |")
346
+ lines.append("|----------|---------|")
347
+ for cat, count in sorted(deep.items()):
348
+ lines.append(f"| {cat} | {count} |")
349
+ lines.append(f"| **Total** | **{deep_total}** |")
350
+ lines.append("")
351
+
352
+ # Collection warnings
353
+ if errors:
354
+ lines.append("---\n")
355
+ lines.append("## Collection Warnings\n")
356
+ for err in errors:
357
+ lines.append(f"- {err}")
358
+ lines.append("")
359
+
360
+ lines.append("---\n")
361
+ lines.append("*Report generated by dx-org-analyze. Read-only — no changes were made to the org.*\n")
362
+
363
+ return "\n".join(lines)
364
+
365
+
366
+ # ── Main ────────────────────────────────────────────────────────────
367
+
368
+ def main():
369
+ parser = argparse.ArgumentParser(description="Generate a single-org introspection report.")
370
+ parser.add_argument("--org", required=True, help="Path to org data directory or JSON file (from collect_org_data.py)")
371
+ parser.add_argument("--output", required=True, help="Output path prefix (writes .json and/or .md)")
372
+ parser.add_argument("--format", choices=["markdown", "json", "both"], default="both",
373
+ help="Output format (default: both)")
374
+ parser.add_argument("--label", default=None, help="Display name for the org (default: org name from data)")
375
+ args = parser.parse_args()
376
+
377
+ try:
378
+ data = load_org_data(args.org)
379
+ except (FileNotFoundError, json.JSONDecodeError) as e:
380
+ print(f"Error loading org data: {e}", file=sys.stderr)
381
+ sys.exit(1)
382
+
383
+ label = args.label or data.get("org_name", "Unknown Org")
384
+
385
+ meta = analyze_metadata(data)
386
+ perms = analyze_permissions(data)
387
+ vals = analyze_org_values(data)
388
+ pkgs = analyze_packages(data)
389
+ lics = analyze_licenses(data)
390
+ sys_perms = analyze_system_permissions(data)
391
+ deep = analyze_deep_data(data)
392
+
393
+ result = {
394
+ "org_id": data.get("org_id"),
395
+ "org_name": data.get("org_name"),
396
+ "org_edition": data.get("org_edition"),
397
+ "label": label,
398
+ "metadata": meta,
399
+ "permissions": perms,
400
+ "org_values": vals,
401
+ "packages": pkgs,
402
+ "licenses": lics,
403
+ "system_permissions": sys_perms,
404
+ "deep_data": deep,
405
+ "errors": data.get("errors", []),
406
+ }
407
+
408
+ if args.format in ("json", "both"):
409
+ json_path = args.output + ".json"
410
+ serializable = json.loads(json.dumps(result, default=str))
411
+ with open(json_path, "w") as f:
412
+ json.dump(serializable, f, indent=2)
413
+ print(f"Wrote {json_path}")
414
+
415
+ if args.format in ("markdown", "both"):
416
+ md_path = args.output + ".md"
417
+ md = render_markdown(data, meta, perms, vals, pkgs, lics, sys_perms, deep, label)
418
+ with open(md_path, "w") as f:
419
+ f.write(md)
420
+ print(f"Wrote {md_path}")
421
+
422
+ # Print summary
423
+ print(f"\n# Org Analysis Report: {label}")
424
+ print(f" Edition: {data.get('org_edition', 'N/A')}")
425
+ print(f" Metadata: {meta['types_discovered']} types, {meta['total_org_native']} org-native + {meta['total_namespaced']} namespaced components")
426
+ print(f" Org settings: {perms['total']} ({perms['enabled']} enabled)")
427
+ print(f" Org limits: {vals['total']}")
428
+ print(f" Packages: {pkgs['count']}")
429
+ print(f" Licenses: {lics['user']['count']} user, {lics['permission_set']['count']} PSL, {lics['package']['count']} package")
430
+ print(f" System permissions: {sys_perms['permission_sets']} sets, {sys_perms['total_enabled']} enabled")
431
+ deep_total = sum(deep.values())
432
+ print(f" Deep data: {deep_total} records")
433
+
434
+
435
+ if __name__ == "__main__":
436
+ main()
@@ -0,0 +1,46 @@
1
+ # dx-org-analyze Tests
2
+
3
+ Structural consistency tests and diff computation tests for the `dx-org-analyze` skill.
4
+
5
+ ## What's covered
6
+
7
+ | Test | What it checks |
8
+ |------|----------------|
9
+ | `test_consistency.sh` | Frontmatter fields, SF CLI auth documented, drift formula, severity levels, public API usage, no internal tool references, core rules |
10
+ | `test_compute_diff.sh` | Diff computation against fixture data: metadata, permissions, org values, packages, licenses, markdown report structure |
11
+
12
+ ## What's not covered
13
+
14
+ - **Live org comparison** — requires two authenticated orgs. Keep as manual smoke test.
15
+ - **Drift calculation accuracy** — verify manually against known org pairs.
16
+ - **Package comparison** — requires real managed packages on the instance.
17
+ - **OrganizationSettingsDetail** — not available on all editions; test graceful fallback manually.
18
+
19
+ ## Running
20
+
21
+ ```bash
22
+ # Run all tests
23
+ bash tests/test_consistency.sh && bash tests/test_compute_diff.sh
24
+
25
+ # Run individually
26
+ bash tests/test_consistency.sh # Skill structure tests
27
+ bash tests/test_compute_diff.sh # Diff computation tests
28
+ ```
29
+
30
+ Exit code `0` on pass, `1` on fail.
31
+
32
+ ## Manual smoke checklist
33
+
34
+ - [ ] Triggers on "compare these two orgs"
35
+ - [ ] Lists authenticated orgs from `sf org list`
36
+ - [ ] Asks user to pick source and target
37
+ - [ ] Handles `sf org login web` when org not authenticated
38
+ - [ ] Report order: Drift Score → Summary → Details
39
+ - [ ] Drift percentages are 0-100%
40
+ - [ ] Shared components show field/body-level diff
41
+ - [ ] Permissions categorized correctly
42
+ - [ ] Handles 0 packages gracefully
43
+ - [ ] Handles different editions without breaking
44
+ - [ ] Gracefully degrades when OrganizationSettingsDetail unavailable
45
+ - [ ] Falls back to SecurityHealthCheckRisks for settings
46
+ - [ ] REST /limits/ endpoint populates org values
@@ -0,0 +1,76 @@
1
+ {
2
+ "org_id": "00Dxx0000000001",
3
+ "org_name": "TestOrgA",
4
+ "org_edition": "Developer Edition",
5
+ "base_url": "https://org-a.example.com",
6
+ "collected_at": "2026-05-13T00:00:00Z",
7
+ "api_version": "62.0",
8
+ "metadata_types": ["ApexClass", "CustomObject", "Profile"],
9
+ "metadata_components": {
10
+ "ApexClass": [
11
+ {"fullName": "SharedClass", "namespace": null},
12
+ {"fullName": "OnlyInA", "namespace": null},
13
+ {"fullName": "ManagedPkg", "namespace": "pkg"}
14
+ ],
15
+ "CustomObject": [
16
+ {"fullName": "Project__c", "namespace": null}
17
+ ],
18
+ "Profile": [
19
+ {"fullName": "Admin", "namespace": null},
20
+ {"fullName": "Standard User", "namespace": null},
21
+ {"fullName": "Custom Profile", "namespace": null}
22
+ ]
23
+ },
24
+ "permissions": {
25
+ "ApiEnabled": {"value": "true"},
26
+ "ApexRest": {"value": "true"},
27
+ "SharedPerm": {"value": "true"},
28
+ "DisabledInA": {"value": "false"}
29
+ },
30
+ "org_values": {
31
+ "OrgEdition": "DE",
32
+ "MaxCustomObjects": "400",
33
+ "DataStorage": "5",
34
+ "SharedValue": "same"
35
+ },
36
+ "licenses": {
37
+ "user": [
38
+ {"Name": "Salesforce", "TotalLicenses": 10, "UsedLicenses": 1, "Status": "Active"},
39
+ {"Name": "Chatter Free", "TotalLicenses": 5000, "UsedLicenses": 0, "Status": "Active"},
40
+ {"Name": "OnlyInA License", "TotalLicenses": 5, "UsedLicenses": 0, "Status": "Active"}
41
+ ],
42
+ "permission_set": [
43
+ {"DeveloperName": "SalesforceCPQ", "TotalLicenses": 10, "UsedLicenses": 0}
44
+ ],
45
+ "package": []
46
+ },
47
+ "installed_packages": [
48
+ {"name": "Shared Package", "namespace": "sharedpkg", "version": "1.5"},
49
+ {"name": "Only In A", "namespace": "apkg", "version": "2.0"},
50
+ {"name": "Version Mismatch", "namespace": "vmpkg", "version": "3.1"}
51
+ ],
52
+ "system_permissions": {
53
+ "fields_discovered": ["PermissionsApiEnabled", "PermissionsAuthorApex", "PermissionsBulkApiHardDelete", "PermissionsManageUsers", "PermissionsViewSetup"],
54
+ "permission_sets": [
55
+ {
56
+ "name": "Admin",
57
+ "label": "System Administrator",
58
+ "is_profile": true,
59
+ "permissions": {"PermissionsApiEnabled": true, "PermissionsAuthorApex": true, "PermissionsManageUsers": true, "PermissionsViewSetup": true}
60
+ },
61
+ {
62
+ "name": "StandardUser",
63
+ "label": "Standard User",
64
+ "is_profile": true,
65
+ "permissions": {"PermissionsApiEnabled": true, "PermissionsViewSetup": true}
66
+ },
67
+ {
68
+ "name": "OnlyInAPermSet",
69
+ "label": "Only In A PermSet",
70
+ "is_profile": false,
71
+ "permissions": {"PermissionsBulkApiHardDelete": true}
72
+ }
73
+ ]
74
+ },
75
+ "errors": []
76
+ }
@@ -0,0 +1,70 @@
1
+ {
2
+ "org_id": "00Dxx0000000002",
3
+ "org_name": "TestOrgB",
4
+ "org_edition": "Professional Edition",
5
+ "base_url": "https://org-b.example.com",
6
+ "collected_at": "2026-05-13T00:00:00Z",
7
+ "api_version": "62.0",
8
+ "metadata_types": ["ApexClass", "Profile"],
9
+ "metadata_components": {
10
+ "ApexClass": [
11
+ {"fullName": "SharedClass", "namespace": null},
12
+ {"fullName": "OnlyInB", "namespace": null}
13
+ ],
14
+ "Profile": [
15
+ {"fullName": "Admin", "namespace": null},
16
+ {"fullName": "Standard User", "namespace": null}
17
+ ]
18
+ },
19
+ "permissions": {
20
+ "SharedPerm": {"value": "true"},
21
+ "DisabledInA": {"value": "true"},
22
+ "OnlyInB": {"value": "true"}
23
+ },
24
+ "org_values": {
25
+ "OrgEdition": "PE",
26
+ "MaxCustomObjects": "50",
27
+ "DataStorage": "10240",
28
+ "SharedValue": "same"
29
+ },
30
+ "licenses": {
31
+ "user": [
32
+ {"Name": "Salesforce", "TotalLicenses": 25, "UsedLicenses": 3, "Status": "Active"},
33
+ {"Name": "Chatter Free", "TotalLicenses": 5000, "UsedLicenses": 0, "Status": "Active"},
34
+ {"Name": "OnlyInB License", "TotalLicenses": 100, "UsedLicenses": 0, "Status": "Active"}
35
+ ],
36
+ "permission_set": [
37
+ {"DeveloperName": "SalesforceCPQ", "TotalLicenses": 25, "UsedLicenses": 0}
38
+ ],
39
+ "package": []
40
+ },
41
+ "installed_packages": [
42
+ {"name": "Shared Package", "namespace": "sharedpkg", "version": "1.5"},
43
+ {"name": "Only In B", "namespace": "bpkg", "version": "1.0"},
44
+ {"name": "Version Mismatch", "namespace": "vmpkg", "version": "4.0"}
45
+ ],
46
+ "system_permissions": {
47
+ "fields_discovered": ["PermissionsApiEnabled", "PermissionsAuthorApex", "PermissionsBulkApiHardDelete", "PermissionsManageUsers", "PermissionsViewSetup"],
48
+ "permission_sets": [
49
+ {
50
+ "name": "Admin",
51
+ "label": "System Administrator",
52
+ "is_profile": true,
53
+ "permissions": {"PermissionsApiEnabled": true, "PermissionsAuthorApex": true, "PermissionsManageUsers": true, "PermissionsViewSetup": true, "PermissionsBulkApiHardDelete": true}
54
+ },
55
+ {
56
+ "name": "StandardUser",
57
+ "label": "Standard User",
58
+ "is_profile": true,
59
+ "permissions": {"PermissionsApiEnabled": true}
60
+ },
61
+ {
62
+ "name": "OnlyInBPermSet",
63
+ "label": "Only In B PermSet",
64
+ "is_profile": false,
65
+ "permissions": {"PermissionsManageUsers": true}
66
+ }
67
+ ]
68
+ },
69
+ "errors": []
70
+ }