tadforce 0.0.0-stage → 0.3.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 (141) hide show
  1. package/.claude-plugin/marketplace.json +20 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/LICENSE +202 -0
  4. package/NOTICE +11 -0
  5. package/README.md +266 -2
  6. package/assets/logo-mark.svg +24 -0
  7. package/assets/logo.svg +31 -0
  8. package/bin/tadforce.js +4 -0
  9. package/commands/tadforce-review.md +8 -0
  10. package/commands/tadforce-update.md +6 -0
  11. package/commands/tadforce.md +10 -0
  12. package/package.json +60 -4
  13. package/skills/tadforce/SKILL.md +73 -0
  14. package/skills/tadforce/references/axes-rules.md +29 -0
  15. package/skills/tadforce/references/evidence-rules.md +40 -0
  16. package/skills/tadforce/references/exclusion-heuristics.md +40 -0
  17. package/skills/tadforce/references/figure-rules.md +42 -0
  18. package/skills/tadforce/references/markdown-conventions.md +67 -0
  19. package/skills/tadforce/references/modules/agentforce-ai.md +24 -0
  20. package/skills/tadforce/references/modules/analytics.md +19 -0
  21. package/skills/tadforce/references/modules/commerce.md +25 -0
  22. package/skills/tadforce/references/modules/data-cloud.md +23 -0
  23. package/skills/tadforce/references/modules/experience-cloud.md +24 -0
  24. package/skills/tadforce/references/modules/field-service.md +25 -0
  25. package/skills/tadforce/references/modules/industries.md +21 -0
  26. package/skills/tadforce/references/modules/integration-platforms.md +19 -0
  27. package/skills/tadforce/references/modules/marketing.md +24 -0
  28. package/skills/tadforce/references/modules/omnistudio.md +24 -0
  29. package/skills/tadforce/references/modules/platform-events.md +19 -0
  30. package/skills/tadforce/references/modules/revenue-cpq.md +25 -0
  31. package/skills/tadforce/references/modules/sales-cloud.md +26 -0
  32. package/skills/tadforce/references/modules/security-shield.md +18 -0
  33. package/skills/tadforce/references/modules/service-cloud.md +27 -0
  34. package/skills/tadforce/references/naming-external-systems.md +32 -0
  35. package/skills/tadforce/references/questions.md +59 -0
  36. package/skills/tadforce/references/salesforce-skills.md +27 -0
  37. package/skills/tadforce/references/workflow.md +95 -0
  38. package/skills/tadforce/references/writing-rules.md +47 -0
  39. package/skills/tadforce-review/SKILL.md +53 -0
  40. package/skills/tadforce-update/SKILL.md +48 -0
  41. package/src/build/brand.js +107 -0
  42. package/src/build/browser.js +70 -0
  43. package/src/build/directives.js +293 -0
  44. package/src/build/export.js +60 -0
  45. package/src/build/figures.js +119 -0
  46. package/src/build/mermaid.js +107 -0
  47. package/src/build/render.js +299 -0
  48. package/src/build/serve.js +59 -0
  49. package/src/cli.js +68 -0
  50. package/src/commands/docs.js +204 -0
  51. package/src/commands/doctor.js +370 -0
  52. package/src/commands/init.js +132 -0
  53. package/src/commands/org.js +151 -0
  54. package/src/commands/review.js +35 -0
  55. package/src/commands/spec.js +91 -0
  56. package/src/commands/update.js +154 -0
  57. package/src/facts/extractors/apex.js +111 -0
  58. package/src/facts/extractors/automation.js +85 -0
  59. package/src/facts/extractors/data.js +94 -0
  60. package/src/facts/extractors/integration.js +76 -0
  61. package/src/facts/extractors/misc.js +100 -0
  62. package/src/facts/extractors/security.js +92 -0
  63. package/src/facts/extractors/ui.js +120 -0
  64. package/src/facts/fingerprints.js +32 -0
  65. package/src/facts/heuristics/exclusions.js +143 -0
  66. package/src/facts/heuristics/modules.js +85 -0
  67. package/src/facts/heuristics/naming.js +31 -0
  68. package/src/facts/heuristics/signals.js +76 -0
  69. package/src/facts/index.js +197 -0
  70. package/src/facts/org.js +87 -0
  71. package/src/facts/xml.js +85 -0
  72. package/src/sf/sf.js +120 -0
  73. package/src/util/args.js +21 -0
  74. package/src/util/config.js +104 -0
  75. package/src/util/fsx.js +58 -0
  76. package/src/util/i18n.js +49 -0
  77. package/src/util/log.js +31 -0
  78. package/src/util/paths.js +11 -0
  79. package/src/validate/index.js +126 -0
  80. package/templates/axes/en/00-document-control.md +3 -0
  81. package/templates/axes/en/01-summary.md +7 -0
  82. package/templates/axes/en/02-method.md +9 -0
  83. package/templates/axes/en/03-findings.md +19 -0
  84. package/templates/axes/en/04-register.md +3 -0
  85. package/templates/axes/en/05-roadmap.md +3 -0
  86. package/templates/axes/en/99-appendix.md +5 -0
  87. package/templates/axes/fr/00-document-control.md +3 -0
  88. package/templates/axes/fr/01-summary.md +7 -0
  89. package/templates/axes/fr/02-method.md +9 -0
  90. package/templates/axes/fr/03-findings.md +19 -0
  91. package/templates/axes/fr/04-register.md +3 -0
  92. package/templates/axes/fr/05-roadmap.md +3 -0
  93. package/templates/axes/fr/99-appendix.md +5 -0
  94. package/templates/html/cover.html +12 -0
  95. package/templates/html/editor.js +502 -0
  96. package/templates/html/style.css +255 -0
  97. package/templates/html/template.html +88 -0
  98. package/templates/i18n/en.json +366 -0
  99. package/templates/i18n/fr.json +366 -0
  100. package/templates/manifest/package-tad.xml +543 -0
  101. package/templates/specs/sections.yml +494 -0
  102. package/templates/tad/en/00-document-control.md +7 -0
  103. package/templates/tad/en/01-summary.md +17 -0
  104. package/templates/tad/en/02-purpose.md +35 -0
  105. package/templates/tad/en/03-context.md +17 -0
  106. package/templates/tad/en/04-general.md +29 -0
  107. package/templates/tad/en/05-functional.md +7 -0
  108. package/templates/tad/en/06-data.md +71 -0
  109. package/templates/tad/en/07-application.md +49 -0
  110. package/templates/tad/en/08-ui.md +41 -0
  111. package/templates/tad/en/09-integrations.md +53 -0
  112. package/templates/tad/en/10-security.md +59 -0
  113. package/templates/tad/en/11-reporting.md +13 -0
  114. package/templates/tad/en/12-operations.md +25 -0
  115. package/templates/tad/en/13-nfr.md +9 -0
  116. package/templates/tad/en/14-decisions.md +3 -0
  117. package/templates/tad/en/15-outlook.md +3 -0
  118. package/templates/tad/en/99-appendices.md +119 -0
  119. package/templates/tad/fr/00-document-control.md +7 -0
  120. package/templates/tad/fr/01-summary.md +17 -0
  121. package/templates/tad/fr/02-purpose.md +35 -0
  122. package/templates/tad/fr/03-context.md +17 -0
  123. package/templates/tad/fr/04-general.md +29 -0
  124. package/templates/tad/fr/05-functional.md +7 -0
  125. package/templates/tad/fr/06-data.md +71 -0
  126. package/templates/tad/fr/07-application.md +49 -0
  127. package/templates/tad/fr/08-ui.md +41 -0
  128. package/templates/tad/fr/09-integrations.md +53 -0
  129. package/templates/tad/fr/10-security.md +59 -0
  130. package/templates/tad/fr/11-reporting.md +13 -0
  131. package/templates/tad/fr/12-operations.md +25 -0
  132. package/templates/tad/fr/13-nfr.md +9 -0
  133. package/templates/tad/fr/14-decisions.md +3 -0
  134. package/templates/tad/fr/15-outlook.md +3 -0
  135. package/templates/tad/fr/99-appendices.md +119 -0
  136. package/templates/vendor/LICENSE-html-docx-js.txt +21 -0
  137. package/templates/vendor/LICENSE-html2canvas.txt +22 -0
  138. package/templates/vendor/LICENSE-mermaid.txt +21 -0
  139. package/templates/vendor/html-docx.js +13216 -0
  140. package/templates/vendor/html2canvas.min.js +20 -0
  141. package/templates/vendor/mermaid.min.js +2314 -0
@@ -0,0 +1,111 @@
1
+ import { files, baseName, safeRead, readXml, str } from '../xml.js';
2
+
3
+ /** Split Apex source into code without comments/strings, and the list of string literals. */
4
+ export function scanApex(src) {
5
+ let code = '', i = 0;
6
+ const strings = [];
7
+ while (i < src.length) {
8
+ const ch = src[i], nx = src[i + 1];
9
+ if (ch === '/' && nx === '*') { const e = src.indexOf('*/', i + 2); i = e < 0 ? src.length : e + 2; code += ' '; continue; }
10
+ if (ch === '/' && nx === '/') { const e = src.indexOf('\n', i); i = e < 0 ? src.length : e; continue; }
11
+ if (ch === "'") {
12
+ let j = i + 1, s = '';
13
+ while (j < src.length && src[j] !== "'") { if (src[j] === '\\') { s += src[j + 1] ?? ''; j += 2; } else s += src[j++]; }
14
+ strings.push(s); code += "''"; i = j + 1; continue;
15
+ }
16
+ code += ch; i++;
17
+ }
18
+ return { code, strings };
19
+ }
20
+
21
+ const KIND_RULES = [
22
+ ['batch', /implements[^{]*\bDatabase\.Batchable\b/i],
23
+ ['queueable', /implements[^{]*\bQueueable\b/i],
24
+ ['schedulable', /implements[^{]*\bSchedulable\b/i],
25
+ ['future', /@future\b/i],
26
+ ['auraEnabled', /@AuraEnabled\b/i],
27
+ ['invocable', /@InvocableMethod\b/i],
28
+ ['soap', /\bwebservice\s+static\b/i],
29
+ ['callout', /\bnew\s+Http\s*\(|\bHttpRequest\b/i],
30
+ ['registrationHandler', /implements[^{]*\bAuth\.RegistrationHandler\b/i],
31
+ ['emailHandler', /implements[^{]*\bMessaging\.InboundEmailHandler\b/i],
32
+ ['stateful', /\bDatabase\.Stateful\b/i],
33
+ ];
34
+
35
+ export function extractApex(roots) {
36
+ const classes = [];
37
+ for (const f of files(roots, 'classes', '.cls')) {
38
+ const name = baseName(f, '.cls');
39
+ const raw = safeRead(f);
40
+ const { code, strings } = scanApex(raw);
41
+ const meta = readXml(f + '-meta.xml');
42
+ const declIdx = code.search(/\b(class|interface|enum)\s+\w+/i);
43
+ const decl = declIdx >= 0 ? code.slice(0, declIdx + 60) : code.slice(0, 400);
44
+ const declKind = (/\b(class|interface|enum)\s+\w+/i.exec(decl) || [])[1]?.toLowerCase() || 'class';
45
+ const isTest = /@istest\b/i.test(code.slice(0, Math.max(0, declIdx))) || (/@istest\b/i.test(code) && /Test$/i.test(name) && declKind === 'class');
46
+ const sharing = (/\b(with|without|inherited)\s+sharing\b/i.exec(decl) || [])[1]?.toLowerCase() || (declKind === 'class' ? 'none' : 'n/a');
47
+ const noComments = raw.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/\/\/[^\n]*/g, '');
48
+ const rest = /@RestResource\s*\(\s*urlMapping\s*=\s*'([^']+)'/i.exec(noComments);
49
+ const kinds = KIND_RULES.filter(([, re]) => re.test(code)).map(([k]) => k);
50
+ if (rest) kinds.push('rest');
51
+ if (/\babstract\s+class\b/i.test(decl)) kinds.push('abstract');
52
+ if (declKind !== 'class') kinds.push(declKind);
53
+ if (/^global\b/i.test(decl.trim()) || /\bglobal\s+(with|without|inherited|abstract|virtual|class)/i.test(decl)) kinds.push('global');
54
+ const namedCredentials = [...new Set(strings.map((s) => /^callout:([A-Za-z0-9_]+)/.exec(s)?.[1]).filter(Boolean))].sort();
55
+ const hardcodedIds = strings.filter((s) => /^[a-zA-Z0-9]{15}([a-zA-Z0-9]{3})?$/.test(s) && /^0[0-9A-Za-z]{2}/.test(s) && /\d/.test(s) && /[A-Za-z]/.test(s.slice(3))).length;
56
+ classes.push({
57
+ name,
58
+ apiVersion: str(meta.apiVersion),
59
+ test: !!isTest,
60
+ sharing: isTest ? 'n/a' : sharing,
61
+ kinds: [...new Set(kinds)].sort(),
62
+ restUrl: rest ? rest[1] : null,
63
+ namedCredentials,
64
+ userMode: (code.match(/\bWITH\s+USER_MODE\b|AccessLevel\.USER_MODE/gi) || []).length,
65
+ securityEnforced: (code.match(/\bWITH\s+SECURITY_ENFORCED\b/gi) || []).length,
66
+ stripInaccessible: (code.match(/Security\.stripInaccessible/gi) || []).length,
67
+ hardcodedIds,
68
+ soqlInLoops: countSoqlInLoops(code),
69
+ lines: raw.split('\n').filter((l) => l.trim()).length,
70
+ _tokens: tokens(code),
71
+ });
72
+ }
73
+
74
+ const triggers = [];
75
+ for (const f of files(roots, 'triggers', '.trigger')) {
76
+ const name = baseName(f, '.trigger');
77
+ const { code } = scanApex(safeRead(f));
78
+ const m = /\btrigger\s+(\w+)\s+on\s+(\w+)\s*\(([^)]*)\)/i.exec(code);
79
+ const meta = readXml(f + '-meta.xml');
80
+ triggers.push({
81
+ name,
82
+ object: m ? m[2] : null,
83
+ events: m ? m[3].split(',').map((e) => e.trim().toLowerCase().replace(/\s+/g, ' ')).filter(Boolean) : [],
84
+ apiVersion: str(meta.apiVersion),
85
+ active: str(meta.status) ? str(meta.status) === 'Active' : true,
86
+ lines: code.split('\n').filter((l) => l.trim()).length,
87
+ _tokens: tokens(code),
88
+ });
89
+ }
90
+ return { classes, triggers };
91
+ }
92
+
93
+ export function tokens(code) {
94
+ return new Set(code.match(/[A-Za-z_][A-Za-z0-9_]*/g) || []);
95
+ }
96
+
97
+ /** Rough count of SOQL/DML statements placed inside for/while loops (review signal, not proof). */
98
+ function countSoqlInLoops(code) {
99
+ let n = 0;
100
+ const re = /\b(for|while)\s*\([^)]*\)\s*\{/gi;
101
+ let m;
102
+ while ((m = re.exec(code))) {
103
+ let depth = 1, j = m.index + m[0].length;
104
+ const start = j;
105
+ while (j < code.length && depth) { if (code[j] === '{') depth++; else if (code[j] === '}') depth--; j++; }
106
+ const body = code.slice(start, j);
107
+ // a `for (x : [SELECT ...])` loop header is fine; only statements inside the body count
108
+ n += (body.match(/\[\s*SELECT\b|\bDatabase\.query\s*\(|\b(insert|update|upsert|delete)\s+\w/gi) || []).length;
109
+ }
110
+ return n;
111
+ }
@@ -0,0 +1,85 @@
1
+ import { files, baseName, readXml, arr, bool, str, byName } from '../xml.js';
2
+
3
+ const desc = (v) => { const s = str(v); return s ? s.replace(/\s+/g, ' ').slice(0, 280) : null; };
4
+
5
+ export function extractAutomation(roots) {
6
+ const flowDefs = {};
7
+ for (const f of files(roots, 'flowDefinitions', '.flowDefinition-meta.xml')) {
8
+ const x = readXml(f);
9
+ flowDefs[baseName(f, '.flowDefinition-meta.xml')] = str(x.activeVersionNumber);
10
+ }
11
+
12
+ const flows = files(roots, 'flows', '.flow-meta.xml').map((f) => {
13
+ const x = readXml(f);
14
+ const name = baseName(f, '.flow-meta.xml');
15
+ const start = x.start || {};
16
+ const sched = start.schedule || {};
17
+ let status = str(x.status) || 'Unknown';
18
+ if (flowDefs[name] === '0') status = 'Inactive';
19
+ const actionCalls = arr(x.actionCalls);
20
+ return {
21
+ name,
22
+ label: str(x.label),
23
+ processType: str(x.processType),
24
+ status,
25
+ active: status === 'Active',
26
+ apiVersion: str(x.apiVersion),
27
+ object: str(start.object),
28
+ triggerType: str(start.triggerType),
29
+ recordTriggerType: str(start.recordTriggerType),
30
+ scheduleFrequency: str(sched.frequency),
31
+ description: desc(x.description),
32
+ screens: arr(x.screens).length,
33
+ subflows: [...new Set(arr(x.subflows).map((s) => str(s.flowName)).filter(Boolean))].sort(),
34
+ apexActions: [...new Set(actionCalls.filter((a) => str(a.actionType) === 'apex').map((a) => str(a.actionName)).filter(Boolean))].sort(),
35
+ recordOps: {
36
+ create: arr(x.recordCreates).length,
37
+ update: arr(x.recordUpdates).length,
38
+ lookup: arr(x.recordLookups).length,
39
+ delete: arr(x.recordDeletes).length,
40
+ },
41
+ };
42
+ }).sort(byName);
43
+
44
+ const omni = (folder, suffix, extra = () => ({})) => files(roots, folder, suffix).map((f) => {
45
+ const x = readXml(f);
46
+ return {
47
+ name: baseName(f, suffix),
48
+ label: str(x.name),
49
+ type: str(x.type),
50
+ subType: str(x.subType),
51
+ language: str(x.language),
52
+ active: bool(x.isActive),
53
+ version: str(x.versionNumber),
54
+ ...extra(x),
55
+ };
56
+ }).sort(byName);
57
+
58
+ const omnistudio = {
59
+ omniScripts: omni('omniScripts', '.os-meta.xml'),
60
+ integrationProcedures: omni('omniIntegrationProcedures', '.oip-meta.xml'),
61
+ dataMappers: omni('omniDataTransforms', '.rpt-meta.xml', (x) => ({ inputType: str(x.inputType), outputType: str(x.outputType) })),
62
+ flexCards: omni('omniUiCard', '.ouc-meta.xml'),
63
+ };
64
+
65
+ // Legacy automation (review signals)
66
+ let workflowRules = 0, workflowFieldUpdates = 0, workflowAlerts = 0;
67
+ for (const f of files(roots, 'workflows', '.workflow-meta.xml')) {
68
+ const x = readXml(f);
69
+ workflowRules += arr(x.rules).length;
70
+ workflowFieldUpdates += arr(x.fieldUpdates).length;
71
+ workflowAlerts += arr(x.alerts).length;
72
+ }
73
+ const count = (folder, suffix) => files(roots, folder, suffix).length;
74
+ const legacy = {
75
+ workflowRules, workflowFieldUpdates, workflowAlerts,
76
+ processBuilders: flows.filter((f) => f.processType === 'Workflow').length,
77
+ approvalProcesses: count('approvalProcesses', '.approvalProcess-meta.xml'),
78
+ assignmentRules: count('assignmentRules', '.assignmentRules-meta.xml'),
79
+ autoResponseRules: count('autoResponseRules', '.autoResponseRules-meta.xml'),
80
+ escalationRules: count('escalationRules', '.escalationRules-meta.xml'),
81
+ duplicateRules: count('duplicateRules', '.duplicateRule-meta.xml'),
82
+ matchingRules: count('matchingRules', '.matchingRule-meta.xml'),
83
+ };
84
+ return { flows, omnistudio, rules: legacy };
85
+ }
@@ -0,0 +1,94 @@
1
+ import path from 'node:path';
2
+ import { bundles, files, baseName, readXml, arr, bool, str, byName } from '../xml.js';
3
+
4
+ const RELATION_TYPES = new Set(['Lookup', 'MasterDetail', 'Hierarchy', 'ExternalLookup', 'IndirectLookup', 'MetadataRelationship']);
5
+
6
+ export function objectKind(name, meta = {}) {
7
+ if (meta.customSettingsType) return 'customSetting';
8
+ if (name.endsWith('__mdt')) return 'customMetadataType';
9
+ if (name.endsWith('__e')) return 'platformEvent';
10
+ if (name.endsWith('__b')) return 'bigObject';
11
+ if (name.endsWith('__x')) return 'externalObject';
12
+ if (name.endsWith('__c')) return 'custom';
13
+ return 'standard';
14
+ }
15
+
16
+ export function namespaceOf(apiName) {
17
+ const m = /^([A-Za-z][A-Za-z0-9]{0,14})__[A-Za-z0-9_]+?__(c|mdt|e|b|x|r)$/.exec(apiName);
18
+ return m ? m[1] : null;
19
+ }
20
+
21
+ const desc = (v) => { const s = str(v); return s ? s.replace(/\s+/g, ' ').slice(0, 280) : null; };
22
+
23
+ export function extractData(roots) {
24
+ const objects = [];
25
+ for (const b of bundles(roots, 'objects')) {
26
+ const meta = readXml(path.join(b.dir, `${b.name}.object-meta.xml`));
27
+ const kind = objectKind(b.name, meta);
28
+ const fields = files([b.dir], 'fields', '.field-meta.xml').map((f) => {
29
+ const x = readXml(f);
30
+ const name = baseName(f, '.field-meta.xml');
31
+ return {
32
+ name,
33
+ label: str(x.label),
34
+ type: str(x.type) || (x.formula ? 'Formula' : null),
35
+ referenceTo: arr(x.referenceTo).map(String),
36
+ relationshipName: str(x.relationshipName),
37
+ externalId: bool(x.externalId),
38
+ unique: bool(x.unique),
39
+ required: bool(x.required),
40
+ formula: x.formula != null,
41
+ namespace: namespaceOf(name),
42
+ custom: name.endsWith('__c'),
43
+ };
44
+ });
45
+ const recordTypes = files([b.dir], 'recordTypes', '.recordType-meta.xml').map((f) => {
46
+ const x = readXml(f);
47
+ return { name: baseName(f, '.recordType-meta.xml'), label: str(x.label), active: x.active == null ? true : bool(x.active) };
48
+ });
49
+ const validationRules = files([b.dir], 'validationRules', '.validationRule-meta.xml').map((f) => {
50
+ const x = readXml(f);
51
+ return { name: baseName(f, '.validationRule-meta.xml'), active: bool(x.active) };
52
+ });
53
+ objects.push({
54
+ name: b.name,
55
+ label: str(meta.label),
56
+ kind,
57
+ namespace: namespaceOf(b.name),
58
+ description: desc(meta.description),
59
+ sharingModel: str(meta.sharingModel),
60
+ externalSharingModel: str(meta.externalSharingModel),
61
+ historyTracking: bool(meta.enableHistory),
62
+ fieldsTotal: fields.length,
63
+ customFields: fields.filter((f) => f.custom && !f.namespace).length,
64
+ customFieldNames: fields.filter((f) => f.custom).map((f) => f.name).sort(),
65
+ externalIds: fields.filter((f) => f.externalId).map((f) => f.name).sort(),
66
+ relations: fields.filter((f) => RELATION_TYPES.has(f.type) && f.referenceTo.length).map((f) => ({ field: f.name, type: f.type, to: f.referenceTo, relationshipName: f.relationshipName }))
67
+ .sort((a, b) => a.field.localeCompare(b.field)),
68
+ formulaFields: fields.filter((f) => f.formula).length,
69
+ recordTypes: recordTypes.sort(byName),
70
+ validationRules: validationRules.sort(byName),
71
+ fieldSets: files([b.dir], 'fieldSets', '.fieldSet-meta.xml').length,
72
+ businessProcesses: files([b.dir], 'businessProcesses', '.businessProcess-meta.xml').length,
73
+ });
74
+ }
75
+
76
+ // Custom Metadata records: <Type>.<Record>.md-meta.xml
77
+ const cmdt = {};
78
+ for (const f of files(roots, 'customMetadata', '.md-meta.xml')) {
79
+ const base = baseName(f, '.md-meta.xml');
80
+ const dot = base.indexOf('.');
81
+ if (dot < 0) continue;
82
+ const type = base.slice(0, dot) + '__mdt';
83
+ (cmdt[type] ||= []).push(base.slice(dot + 1));
84
+ }
85
+ const cmdtTypes = Object.keys(cmdt).sort().map((type) => ({ type, records: cmdt[type].length, names: cmdt[type].sort() }));
86
+
87
+ const globalValueSets = files(roots, 'globalValueSets', '.globalValueSet-meta.xml').map((f) => baseName(f, '.globalValueSet-meta.xml')).sort();
88
+ const cdc = files(roots, 'platformEventChannelMembers', '.platformEventChannelMember-meta.xml').map((f) => {
89
+ const x = readXml(f);
90
+ return { name: baseName(f, '.platformEventChannelMember-meta.xml'), entity: str(x.selectedEntity), channel: str(x.eventChannel) };
91
+ });
92
+
93
+ return { objects: objects.sort(byName), cmdtTypes, globalValueSets, cdc: cdc.sort(byName) };
94
+ }
@@ -0,0 +1,76 @@
1
+ import { files, baseName, readXml, arr, bool, str, byName, host } from '../xml.js';
2
+
3
+ export function extractIntegration(roots) {
4
+ const namedCredentials = files(roots, 'namedCredentials', '.namedCredential-meta.xml').map((f) => {
5
+ const x = readXml(f);
6
+ const params = arr(x.namedCredentialParameters);
7
+ const url = str(x.endpoint) || str((params.find((p) => str(p.parameterType) === 'Url') || {}).parameterValue);
8
+ const ec = str((params.find((p) => str(p.parameterType) === 'Authentication') || {}).externalCredential);
9
+ return {
10
+ name: baseName(f, '.namedCredential-meta.xml'),
11
+ label: str(x.label),
12
+ type: str(x.namedCredentialType) || 'Legacy',
13
+ host: host(url),
14
+ externalCredential: ec,
15
+ protocol: str(x.protocol),
16
+ principalType: str(x.principalType),
17
+ generateAuthorizationHeader: x.generateAuthorizationHeader == null ? null : bool(x.generateAuthorizationHeader),
18
+ };
19
+ }).sort(byName);
20
+
21
+ const externalCredentials = files(roots, 'externalCredentials', '.externalCredential-meta.xml').map((f) => {
22
+ const x = readXml(f);
23
+ return { name: baseName(f, '.externalCredential-meta.xml'), label: str(x.label), protocol: str(x.authenticationProtocol), principals: arr(x.externalCredentialParameters).filter((p) => /Principal/.test(str(p.parameterType) || '')).length };
24
+ }).sort(byName);
25
+
26
+ const remoteSites = files(roots, 'remoteSiteSettings', '.remoteSite-meta.xml').map((f) => {
27
+ const x = readXml(f);
28
+ return { name: baseName(f, '.remoteSite-meta.xml'), host: host(str(x.url)), active: x.isActive == null ? true : bool(x.isActive), insecure: bool(x.disableProtocolSecurity) };
29
+ }).sort(byName);
30
+
31
+ const connectedApps = files(roots, 'connectedApps', '.connectedApp-meta.xml').map((f) => {
32
+ const x = readXml(f);
33
+ const oauth = x.oauthConfig || {};
34
+ return {
35
+ name: baseName(f, '.connectedApp-meta.xml'),
36
+ label: str(x.label),
37
+ oauth: !!x.oauthConfig,
38
+ scopes: arr(oauth.scopes).map(String).sort(),
39
+ saml: !!x.samlConfig,
40
+ canvas: !!x.canvasConfig,
41
+ };
42
+ }).sort(byName);
43
+
44
+ const authProviders = files(roots, 'authproviders', '.authprovider-meta.xml').map((f) => {
45
+ const x = readXml(f);
46
+ return { name: baseName(f, '.authprovider-meta.xml'), label: str(x.friendlyName), type: str(x.providerType), registrationHandler: str(x.registrationHandler) };
47
+ }).sort(byName);
48
+
49
+ const cspTrustedSites = files(roots, 'cspTrustedSites', '.cspTrustedSite-meta.xml').map((f) => {
50
+ const x = readXml(f);
51
+ return { name: baseName(f, '.cspTrustedSite-meta.xml'), host: host(str(x.endpointUrl)), context: str(x.context), active: x.isActive == null ? true : bool(x.isActive) };
52
+ }).sort(byName);
53
+
54
+ const corsOrigins = files(roots, 'corsWhitelistOrigins', '.corsWhitelistOrigin-meta.xml').map((f) => {
55
+ const x = readXml(f);
56
+ return { name: baseName(f, '.corsWhitelistOrigin-meta.xml'), host: host(str(x.urlPattern)) || str(x.urlPattern) };
57
+ }).sort(byName);
58
+
59
+ const callCenters = files(roots, 'callCenters', '.callCenter-meta.xml').map((f) => {
60
+ const x = readXml(f);
61
+ return { name: baseName(f, '.callCenter-meta.xml'), label: str(x.displayName), adapterHost: host(str(x.adapterUrl)), type: str(x.contactCenterChannels) ? 'contact-center' : 'open-cti' };
62
+ }).sort(byName);
63
+
64
+ return {
65
+ namedCredentials,
66
+ externalCredentials,
67
+ remoteSites,
68
+ connectedApps,
69
+ authProviders,
70
+ cspTrustedSites,
71
+ corsOrigins,
72
+ callCenters,
73
+ externalServices: files(roots, 'externalServiceRegistrations', '.externalServiceRegistration-meta.xml').map((f) => baseName(f, '.externalServiceRegistration-meta.xml')).sort(),
74
+ externalDataSources: files(roots, 'dataSources', '.dataSource-meta.xml').map((f) => baseName(f, '.dataSource-meta.xml')).sort(),
75
+ };
76
+ }
@@ -0,0 +1,100 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { files, bundles, baseName, readXml, arr, str, hasFolder, safeRead } from '../xml.js';
4
+
5
+ export function extractMisc(roots) {
6
+ const labels = files(roots, 'labels', '.labels-meta.xml').flatMap((f) => arr(readXml(f).labels).map((l) => ({ name: str(l.fullName), category: str(l.categories) })));
7
+ const reports = files(roots, 'reports', '.report-meta.xml', { recursive: true });
8
+ const reportFolders = [...new Set(reports.map((f) => path.basename(path.dirname(f))))].sort();
9
+ const dashboards = files(roots, 'dashboards', '.dashboard-meta.xml', { recursive: true });
10
+ const emailTemplates = files(roots, 'email', '.email-meta.xml', { recursive: true });
11
+ const settings = files(roots, 'settings', '.settings-meta.xml').map((f) => baseName(f, '.settings-meta.xml')).sort();
12
+ const installedPackages = files(roots, 'installedPackages', '.installedPackage-meta.xml').map((f) => ({ namespace: baseName(f, '.installedPackage-meta.xml'), version: str(readXml(f).versionNumber) }))
13
+ .sort((a, b) => a.namespace.localeCompare(b.namespace));
14
+
15
+ const count = (folder, suffix) => files(roots, folder, suffix, { recursive: true }).length;
16
+ const signals = {
17
+ serviceChannels: count('serviceChannels', '.serviceChannel-meta.xml'),
18
+ routingConfigs: count('queueRoutingConfigs', '.queueRoutingConfig-meta.xml'),
19
+ presenceStatuses: count('servicePresenceStatuses', '.servicePresenceStatus-meta.xml'),
20
+ presenceConfigs: count('presenceUserConfigs', '.presenceUserConfig-meta.xml'),
21
+ networks: count('networks', '.network-meta.xml'),
22
+ sites: count('sites', '.site-meta.xml'),
23
+ experiences: bundles(roots, 'experiences').length + bundles(roots, 'digitalExperiences').length,
24
+ bots: bundles(roots, 'bots').length,
25
+ genAi: count('genAiPlanners', '') + count('genAiPlannerBundles', '') + count('genAiPlugins', '') + count('genAiFunctions', '') + bundles(roots, 'aiAuthoringBundles').length,
26
+ entitlementProcesses: count('entitlementProcesses', ''),
27
+ dataStreams: count('dataStreamDefinitions', ''),
28
+ };
29
+
30
+ // Components documented by name only (routing, channels, sites, paths, rules, agents…)
31
+ const NAMED = ['serviceChannels', 'queueRoutingConfigs', 'servicePresenceStatuses', 'presenceUserConfigs', 'presenceDeclineReasons', 'networks', 'sites',
32
+ 'entitlementProcesses', 'milestoneTypes', 'pathAssistants', 'duplicateRules', 'matchingRules', 'approvalProcesses', 'notificationtypes', 'genAiPlanners',
33
+ 'genAiPlugins', 'genAiFunctions', 'genAiPromptTemplates', 'brandingSets', 'navigationMenus', 'bots', 'experiences', 'digitalExperiences', 'aiAuthoringBundles', 'workflows', 'globalValueSets'];
34
+ const named = {};
35
+ for (const folder of NAMED) {
36
+ const list = [...files(roots, folder, '').map((f) => path.basename(f).split('.')[0]), ...bundles(roots, folder).map((b) => b.name)];
37
+ if (list.length) named[folder] = [...new Set(list)].sort();
38
+ }
39
+
40
+ return {
41
+ named,
42
+ labels: labels.filter((l) => l.name).sort((a, b) => a.name.localeCompare(b.name)),
43
+ staticResources: files(roots, 'staticresources', '.resource-meta.xml').map((f) => baseName(f, '.resource-meta.xml')).sort(),
44
+ reports: { total: reports.length, folders: reportFolders, byFolder: Object.fromEntries(reportFolders.map((fo) => [fo, reports.filter((f) => path.basename(path.dirname(f)) === fo).length])), names: reports.map((f) => baseName(f, '.report-meta.xml')).sort() },
45
+ dashboards: { total: dashboards.length, names: dashboards.map((f) => baseName(f, '.dashboard-meta.xml')).sort() },
46
+ reportTypes: files(roots, 'reportTypes', '.reportType-meta.xml').map((f) => baseName(f, '.reportType-meta.xml')).sort(),
47
+ emailTemplates: emailTemplates.map((f) => baseName(f, '.email-meta.xml')).sort(),
48
+ settings,
49
+ installedPackages,
50
+ signals,
51
+ folders: metadataFolders(roots),
52
+ };
53
+ }
54
+
55
+ /** Every metadata folder present, with its file count — a raw inventory for appendices. */
56
+ function metadataFolders(roots) {
57
+ const counts = {};
58
+ for (const r of roots) {
59
+ for (const e of fs.readdirSync(r, { withFileTypes: true })) {
60
+ if (!e.isDirectory()) continue;
61
+ counts[e.name] = (counts[e.name] || 0) + files([r], e.name, '-meta.xml', { recursive: true }).length;
62
+ }
63
+ }
64
+ return counts;
65
+ }
66
+
67
+ const NS_PRODUCTS = {
68
+ FinServ: 'Financial Services Cloud',
69
+ HealthCloudGA: 'Health Cloud',
70
+ npsp: 'Nonprofit Success Pack', npe01: 'Nonprofit Success Pack', npo02: 'Nonprofit Success Pack', npe03: 'Nonprofit Success Pack', npe4: 'Nonprofit Success Pack', npe5: 'Nonprofit Success Pack',
71
+ SBQQ: 'Salesforce CPQ', blng: 'Salesforce Billing',
72
+ FSL: 'Field Service',
73
+ et4ae5: 'Marketing Cloud Connect',
74
+ pi: 'Marketing Cloud Account Engagement',
75
+ vlocity_cmt: 'OmniStudio (Communications, Media & Energy)', vlocity_ins: 'OmniStudio (Insurance)', vlocity_ps: 'OmniStudio (Public Sector)', omnistudio: 'OmniStudio',
76
+ dsfs: 'DocuSign eSignature', echosign_dev1: 'Adobe Acrobat Sign', mkto_si: 'Marketo Sales Insight',
77
+ sf_chttr_apps: 'Chatter apps', LightningMDE: 'Lightning Markdown Editor', pse: 'Certinia PSA', c2g: 'Certinia Financials',
78
+ sfLma: 'License Management App', CHANNEL_ORDERS: 'Channel Order App', wave: 'CRM Analytics',
79
+ cgcloud: 'Consumer Goods Cloud', hed: 'Education Data Architecture', slackv2: 'Slack for Salesforce', amazonconnect: 'Amazon Connect CTI Adapter',
80
+ sbaa: 'Salesforce Advanced Approvals', APXTConga4: 'Conga Composer', Apttus_Config2: 'Conga CPQ', rh2: 'Rollup Helper', dlrs: 'Declarative Lookup Rollup Summaries',
81
+ SDOC: 'S-Docs', geopointe: 'Geopointe', fferpcore: 'Certinia ERP Core', sf_com_apps: 'Salesforce Connected Apps', LID: 'LinkedIn Sales Navigator', ltnadptn: 'Lightning Adoption', maps: 'Salesforce Maps', LLC_BI: 'nCino', Loan: 'nCino (Loan)',
82
+ };
83
+
84
+ /** Namespaces referenced by metadata (managed packages), with a product name when known. */
85
+ export function detectNamespaces(roots, installedPackages) {
86
+ const refs = {};
87
+ const re = /\b([A-Za-z][A-Za-z0-9]{1,14})__[A-Za-z][A-Za-z0-9_]*?__(c|r|mdt|e|x|b)\b/g;
88
+ for (const f of files(roots, '', '', { recursive: true }).filter((f) => /\.(xml|cls|trigger|js|html|page|cmp|json)$/.test(f))) {
89
+ for (const m of safeRead(f).matchAll(re)) refs[m[1]] = (refs[m[1]] || 0) + 1;
90
+ }
91
+ for (const p of installedPackages) refs[p.namespace] = refs[p.namespace] || 0;
92
+ return Object.keys(refs).sort().map((ns) => ({
93
+ namespace: ns,
94
+ references: refs[ns],
95
+ product: NS_PRODUCTS[ns] || null,
96
+ installed: installedPackages.some((p) => p.namespace === ns),
97
+ }));
98
+ }
99
+
100
+ export const hasAny = (roots, folders) => folders.some((f) => hasFolder(roots, f));
@@ -0,0 +1,92 @@
1
+ import { files, baseName, readXml, arr, bool, str, byName } from '../xml.js';
2
+
3
+ // System permissions worth surfacing in a review (powerful or sensitive).
4
+ const POWERFUL = ['ModifyAllData', 'ViewAllData', 'ManageUsers', 'AuthorApex', 'CustomizeApplication', 'ManageProfilesPermissionsets', 'ViewSetup', 'ApiEnabled', 'ManageSharing', 'ExportReport', 'ModifyMetadata', 'ManageInternalUsers', 'ResetPasswords', 'PasswordNeverExpires'];
5
+
6
+ function enabledPowerful(x) {
7
+ return arr(x.userPermissions).filter((p) => bool(p.enabled) && POWERFUL.includes(str(p.name))).map((p) => str(p.name)).sort();
8
+ }
9
+
10
+ export function extractSecurity(roots) {
11
+ const profiles = files(roots, 'profiles', '.profile-meta.xml').map((f) => {
12
+ const x = readXml(f);
13
+ return {
14
+ name: baseName(f, '.profile-meta.xml'),
15
+ userLicense: str(x.userLicense),
16
+ custom: bool(x.custom),
17
+ objectPermissions: arr(x.objectPermissions).length,
18
+ fieldPermissions: arr(x.fieldPermissions).length,
19
+ powerful: enabledPowerful(x),
20
+ loginIpRanges: arr(x.loginIpRanges).length,
21
+ };
22
+ }).sort(byName);
23
+
24
+ const permissionSets = files(roots, 'permissionsets', '.permissionset-meta.xml').map((f) => {
25
+ const x = readXml(f);
26
+ return {
27
+ name: baseName(f, '.permissionset-meta.xml'),
28
+ label: str(x.label),
29
+ license: str(x.license),
30
+ objectPermissions: arr(x.objectPermissions).length,
31
+ fieldPermissions: arr(x.fieldPermissions).length,
32
+ classAccesses: arr(x.classAccesses).length,
33
+ customPermissions: arr(x.customPermissions).filter((c) => bool(c.enabled)).length,
34
+ powerful: enabledPowerful(x),
35
+ };
36
+ }).sort(byName);
37
+
38
+ const permissionSetGroups = files(roots, 'permissionsetgroups', '.permissionsetgroup-meta.xml').map((f) => {
39
+ const x = readXml(f);
40
+ return {
41
+ name: baseName(f, '.permissionsetgroup-meta.xml'),
42
+ label: str(x.label),
43
+ permissionSets: arr(x.permissionSets).map(String).sort(),
44
+ mutingPermissionSets: arr(x.mutingPermissionSets).map(String).sort(),
45
+ status: str(x.status),
46
+ };
47
+ }).sort(byName);
48
+
49
+ const roles = files(roots, 'roles', '.role-meta.xml').map((f) => {
50
+ const x = readXml(f);
51
+ return { name: baseName(f, '.role-meta.xml'), label: str(x.name), parent: str(x.parentRole) };
52
+ }).sort(byName);
53
+
54
+ const queues = files(roots, 'queues', '.queue-meta.xml').map((f) => {
55
+ const x = readXml(f);
56
+ return { name: baseName(f, '.queue-meta.xml'), label: str(x.name), objects: arr(x.queueSobject).map((q) => str(q.sobjectType)).filter(Boolean).sort() };
57
+ }).sort(byName);
58
+
59
+ const groups = files(roots, 'groups', '.group-meta.xml').map((f) => {
60
+ const x = readXml(f);
61
+ return { name: baseName(f, '.group-meta.xml'), label: str(x.name), includeBosses: bool(x.doesIncludeBosses) };
62
+ }).sort(byName);
63
+
64
+ const sharingRules = files(roots, 'sharingRules', '.sharingRules-meta.xml').map((f) => {
65
+ const x = readXml(f);
66
+ const rule = (r, kind) => ({ name: str(r.fullName), label: str(r.label), kind, access: str(r.accessLevel) });
67
+ const rules = [
68
+ ...arr(x.sharingCriteriaRules).map((r) => rule(r, 'criteria')),
69
+ ...arr(x.sharingOwnerRules).map((r) => rule(r, 'owner')),
70
+ ...arr(x.sharingGuestRules).map((r) => rule(r, 'guest')),
71
+ ...arr(x.sharingTerritoryRules).map((r) => rule(r, 'territory')),
72
+ ].sort((a, b) => String(a.name).localeCompare(String(b.name)));
73
+ return { object: baseName(f, '.sharingRules-meta.xml'), rules };
74
+ }).filter((s) => s.rules.length).sort((a, b) => a.object.localeCompare(b.object));
75
+
76
+ const securitySettings = readXml(files(roots, 'settings', 'Security.settings-meta.xml')[0] || '');
77
+ const session = securitySettings.sessionSettings || {};
78
+ const pick = (o, keys) => Object.fromEntries(keys.filter((k) => o[k] != null).map((k) => [k, str(o[k])]));
79
+
80
+ return {
81
+ profiles,
82
+ permissionSets,
83
+ permissionSetGroups,
84
+ roles,
85
+ queues,
86
+ groups,
87
+ sharingRules,
88
+ customPermissions: files(roots, 'customPermissions', '.customPermission-meta.xml').map((f) => baseName(f, '.customPermission-meta.xml')).sort(),
89
+ session: pick(session, ['sessionTimeout', 'forceLogoutOnSessionTimeout', 'lockSessionsToIp', 'lockSessionsToDomain', 'requireHttpOnly', 'enableClickjackNonsetupUser', 'enableCSRFOnGet', 'enableCSRFOnPost', 'enforceIpRangesEveryRequest', 'identityConfirmationOnTwoFactorRegistrationEnabled']),
90
+ passwordPolicies: pick(securitySettings.passwordPolicies || {}, ['complexity', 'expiration', 'historyRestriction', 'lockoutInterval', 'maxLoginAttempts', 'minimumPasswordLength']),
91
+ };
92
+ }