tadforce 0.0.0-stage → 0.2.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.
- package/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +13 -0
- package/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.fr.md +125 -0
- package/README.md +256 -2
- package/assets/logo-mark.svg +24 -0
- package/assets/logo.svg +31 -0
- package/bin/tadforce.js +4 -0
- package/commands/tadforce-review.md +8 -0
- package/commands/tadforce-update.md +6 -0
- package/commands/tadforce.md +10 -0
- package/package.json +60 -4
- package/skills/tadforce/SKILL.md +73 -0
- package/skills/tadforce/references/axes-rules.md +29 -0
- package/skills/tadforce/references/evidence-rules.md +40 -0
- package/skills/tadforce/references/exclusion-heuristics.md +40 -0
- package/skills/tadforce/references/figure-rules.md +42 -0
- package/skills/tadforce/references/markdown-conventions.md +67 -0
- package/skills/tadforce/references/modules/agentforce-ai.md +24 -0
- package/skills/tadforce/references/modules/analytics.md +19 -0
- package/skills/tadforce/references/modules/commerce.md +25 -0
- package/skills/tadforce/references/modules/data-cloud.md +23 -0
- package/skills/tadforce/references/modules/experience-cloud.md +24 -0
- package/skills/tadforce/references/modules/field-service.md +25 -0
- package/skills/tadforce/references/modules/industries.md +21 -0
- package/skills/tadforce/references/modules/integration-platforms.md +19 -0
- package/skills/tadforce/references/modules/marketing.md +24 -0
- package/skills/tadforce/references/modules/omnistudio.md +24 -0
- package/skills/tadforce/references/modules/platform-events.md +19 -0
- package/skills/tadforce/references/modules/revenue-cpq.md +25 -0
- package/skills/tadforce/references/modules/sales-cloud.md +26 -0
- package/skills/tadforce/references/modules/security-shield.md +18 -0
- package/skills/tadforce/references/modules/service-cloud.md +27 -0
- package/skills/tadforce/references/naming-external-systems.md +32 -0
- package/skills/tadforce/references/questions.md +59 -0
- package/skills/tadforce/references/salesforce-skills.md +27 -0
- package/skills/tadforce/references/workflow.md +85 -0
- package/skills/tadforce/references/writing-rules.md +47 -0
- package/skills/tadforce-review/SKILL.md +53 -0
- package/skills/tadforce-update/SKILL.md +48 -0
- package/src/build/brand.js +107 -0
- package/src/build/browser.js +70 -0
- package/src/build/directives.js +293 -0
- package/src/build/export.js +60 -0
- package/src/build/figures.js +119 -0
- package/src/build/mermaid.js +107 -0
- package/src/build/render.js +299 -0
- package/src/build/serve.js +59 -0
- package/src/cli.js +68 -0
- package/src/commands/docs.js +191 -0
- package/src/commands/doctor.js +366 -0
- package/src/commands/init.js +132 -0
- package/src/commands/org.js +151 -0
- package/src/commands/review.js +35 -0
- package/src/commands/spec.js +91 -0
- package/src/commands/update.js +153 -0
- package/src/facts/extractors/apex.js +111 -0
- package/src/facts/extractors/automation.js +85 -0
- package/src/facts/extractors/data.js +94 -0
- package/src/facts/extractors/integration.js +76 -0
- package/src/facts/extractors/misc.js +100 -0
- package/src/facts/extractors/security.js +92 -0
- package/src/facts/extractors/ui.js +120 -0
- package/src/facts/fingerprints.js +32 -0
- package/src/facts/heuristics/exclusions.js +143 -0
- package/src/facts/heuristics/modules.js +85 -0
- package/src/facts/heuristics/naming.js +31 -0
- package/src/facts/heuristics/signals.js +76 -0
- package/src/facts/index.js +197 -0
- package/src/facts/org.js +87 -0
- package/src/facts/xml.js +85 -0
- package/src/sf/sf.js +120 -0
- package/src/util/args.js +21 -0
- package/src/util/config.js +104 -0
- package/src/util/fsx.js +58 -0
- package/src/util/i18n.js +33 -0
- package/src/util/log.js +31 -0
- package/src/util/paths.js +11 -0
- package/src/validate/index.js +126 -0
- package/templates/axes/en/00-document-control.md +3 -0
- package/templates/axes/en/01-summary.md +7 -0
- package/templates/axes/en/02-method.md +9 -0
- package/templates/axes/en/03-findings.md +19 -0
- package/templates/axes/en/04-register.md +3 -0
- package/templates/axes/en/05-roadmap.md +3 -0
- package/templates/axes/en/99-appendix.md +5 -0
- package/templates/axes/fr/00-document-control.md +3 -0
- package/templates/axes/fr/01-summary.md +7 -0
- package/templates/axes/fr/02-method.md +9 -0
- package/templates/axes/fr/03-findings.md +19 -0
- package/templates/axes/fr/04-register.md +3 -0
- package/templates/axes/fr/05-roadmap.md +3 -0
- package/templates/axes/fr/99-appendix.md +5 -0
- package/templates/html/cover.html +12 -0
- package/templates/html/editor.js +502 -0
- package/templates/html/style.css +255 -0
- package/templates/html/template.html +88 -0
- package/templates/i18n/en.json +366 -0
- package/templates/i18n/fr.json +366 -0
- package/templates/manifest/package-tad.xml +543 -0
- package/templates/specs/sections.yml +494 -0
- package/templates/tad/en/00-document-control.md +7 -0
- package/templates/tad/en/01-summary.md +17 -0
- package/templates/tad/en/02-purpose.md +35 -0
- package/templates/tad/en/03-context.md +17 -0
- package/templates/tad/en/04-general.md +29 -0
- package/templates/tad/en/05-functional.md +7 -0
- package/templates/tad/en/06-data.md +71 -0
- package/templates/tad/en/07-application.md +49 -0
- package/templates/tad/en/08-ui.md +41 -0
- package/templates/tad/en/09-integrations.md +53 -0
- package/templates/tad/en/10-security.md +59 -0
- package/templates/tad/en/11-reporting.md +13 -0
- package/templates/tad/en/12-operations.md +25 -0
- package/templates/tad/en/13-nfr.md +9 -0
- package/templates/tad/en/14-decisions.md +3 -0
- package/templates/tad/en/15-outlook.md +3 -0
- package/templates/tad/en/99-appendices.md +119 -0
- package/templates/tad/fr/00-document-control.md +7 -0
- package/templates/tad/fr/01-summary.md +17 -0
- package/templates/tad/fr/02-purpose.md +35 -0
- package/templates/tad/fr/03-context.md +17 -0
- package/templates/tad/fr/04-general.md +29 -0
- package/templates/tad/fr/05-functional.md +7 -0
- package/templates/tad/fr/06-data.md +71 -0
- package/templates/tad/fr/07-application.md +49 -0
- package/templates/tad/fr/08-ui.md +41 -0
- package/templates/tad/fr/09-integrations.md +53 -0
- package/templates/tad/fr/10-security.md +59 -0
- package/templates/tad/fr/11-reporting.md +13 -0
- package/templates/tad/fr/12-operations.md +25 -0
- package/templates/tad/fr/13-nfr.md +9 -0
- package/templates/tad/fr/14-decisions.md +3 -0
- package/templates/tad/fr/15-outlook.md +3 -0
- package/templates/tad/fr/99-appendices.md +119 -0
- package/templates/vendor/LICENSE-html-docx-js.txt +21 -0
- package/templates/vendor/LICENSE-html2canvas.txt +22 -0
- package/templates/vendor/LICENSE-mermaid.txt +21 -0
- package/templates/vendor/html-docx.js +13216 -0
- package/templates/vendor/html2canvas.min.js +20 -0
- package/templates/vendor/mermaid.min.js +2314 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { bundles, files, baseName, readXml, arr, bool, str, byName, safeRead } from '../xml.js';
|
|
3
|
+
|
|
4
|
+
const camel = (kebab) => kebab.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
5
|
+
|
|
6
|
+
export function extractUi(roots) {
|
|
7
|
+
const lwc = bundles(roots, 'lwc').filter((b) => !b.name.startsWith('__')).map((b) => {
|
|
8
|
+
const meta = readXml(path.join(b.dir, `${b.name}.js-meta.xml`));
|
|
9
|
+
const js = files([b.dir], '.', '.js').map(safeRead).join('\n');
|
|
10
|
+
const html = files([b.dir], '.', '.html').map(safeRead).join('\n');
|
|
11
|
+
const targets = arr(meta.targets && meta.targets.target).map(String).sort();
|
|
12
|
+
return {
|
|
13
|
+
name: b.name,
|
|
14
|
+
label: str(meta.masterLabel),
|
|
15
|
+
exposed: bool(meta.isExposed),
|
|
16
|
+
targets,
|
|
17
|
+
apex: [...new Set([...js.matchAll(/@salesforce\/apex\/([A-Za-z0-9_]+)\.[A-Za-z0-9_]+/g)].map((m) => m[1]))].sort(),
|
|
18
|
+
children: [...new Set([...html.matchAll(/<c-([a-z0-9-]+)/g)].map((m) => camel(m[1])).concat([...js.matchAll(/from\s+['"]c\/([A-Za-z0-9_]+)['"]/g)].map((m) => m[1])))].sort(),
|
|
19
|
+
usesMessageChannel: /lightning\/messageService/.test(js),
|
|
20
|
+
usesNavigation: /lightning\/navigation/.test(js),
|
|
21
|
+
usesWire: /@wire\s*\(/.test(js),
|
|
22
|
+
service: !html.trim(),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const aura = bundles(roots, 'aura').map((b) => {
|
|
27
|
+
const markup = files([b.dir], '.', '').filter((f) => /\.(cmp|app|evt|intf|design)$/.test(f)).map(safeRead).join('\n');
|
|
28
|
+
return {
|
|
29
|
+
name: b.name,
|
|
30
|
+
type: files([b.dir], '.', '.app').length ? 'application' : files([b.dir], '.', '.evt').length ? 'event' : 'component',
|
|
31
|
+
controller: (/controller\s*=\s*"([A-Za-z0-9_.]+)"/i.exec(markup) || [])[1] || null,
|
|
32
|
+
children: [...new Set([...markup.matchAll(/<c:([A-Za-z0-9_]+)/g)].map((m) => m[1]))].sort(),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const vfPages = files(roots, 'pages', '.page').map((f) => {
|
|
37
|
+
const src = safeRead(f);
|
|
38
|
+
const tag = (/<apex:page[^>]*>/i.exec(src) || [''])[0];
|
|
39
|
+
return {
|
|
40
|
+
name: baseName(f, '.page'),
|
|
41
|
+
controller: (/\bcontroller\s*=\s*"([^"]+)"/i.exec(tag) || [])[1] || null,
|
|
42
|
+
extensions: ((/\bextensions\s*=\s*"([^"]+)"/i.exec(tag) || [])[1] || '').split(',').map((s) => s.trim()).filter(Boolean),
|
|
43
|
+
};
|
|
44
|
+
}).sort(byName);
|
|
45
|
+
|
|
46
|
+
const flexipages = files(roots, 'flexipages', '.flexipage-meta.xml').map((f) => {
|
|
47
|
+
const raw = safeRead(f);
|
|
48
|
+
const x = readXml(f);
|
|
49
|
+
const comps = [...raw.matchAll(/<componentName>([^<]+)<\/componentName>/g)].map((m) => m[1].trim());
|
|
50
|
+
return {
|
|
51
|
+
name: baseName(f, '.flexipage-meta.xml'),
|
|
52
|
+
label: str(x.masterLabel),
|
|
53
|
+
type: str(x.type),
|
|
54
|
+
object: str(x.sobjectType),
|
|
55
|
+
template: str(x.template && x.template.name),
|
|
56
|
+
customComponents: [...new Set(comps.filter((c) => !c.includes(':')))].sort(),
|
|
57
|
+
standardComponents: comps.filter((c) => c.includes(':')).length,
|
|
58
|
+
};
|
|
59
|
+
}).sort(byName);
|
|
60
|
+
|
|
61
|
+
const apps = files(roots, 'applications', '.app-meta.xml').map((f) => {
|
|
62
|
+
const x = readXml(f);
|
|
63
|
+
return {
|
|
64
|
+
name: baseName(f, '.app-meta.xml'),
|
|
65
|
+
label: str(x.label),
|
|
66
|
+
navType: str(x.navType),
|
|
67
|
+
uiType: str(x.uiType),
|
|
68
|
+
formFactors: arr(x.formFactors).map(String).sort(),
|
|
69
|
+
tabs: arr(x.tabs).map(String),
|
|
70
|
+
utilityBar: str(x.utilityBar),
|
|
71
|
+
console: str(x.navType) === 'Console',
|
|
72
|
+
};
|
|
73
|
+
}).sort(byName);
|
|
74
|
+
|
|
75
|
+
const tabs = files(roots, 'tabs', '.tab-meta.xml').map((f) => {
|
|
76
|
+
const x = readXml(f);
|
|
77
|
+
const kind = x.customObject ? 'object' : x.lwcComponent ? 'lwc' : x.flexiPage ? 'page' : x.url ? 'web' : x.page ? 'visualforce' : x.auraComponent ? 'aura' : 'other';
|
|
78
|
+
return { name: baseName(f, '.tab-meta.xml'), kind, lwc: str(x.lwcComponent), aura: str(x.auraComponent), flexiPage: str(x.flexiPage), label: str(x.label) };
|
|
79
|
+
}).sort(byName);
|
|
80
|
+
|
|
81
|
+
const quickActions = files(roots, 'quickActions', '.quickAction-meta.xml').map((f) => {
|
|
82
|
+
const x = readXml(f);
|
|
83
|
+
return { name: baseName(f, '.quickAction-meta.xml'), type: str(x.type), lwc: str(x.lightningWebComponent), aura: str(x.lightningComponent), flow: str(x.flowDefinition), object: str(x.targetObject) };
|
|
84
|
+
}).sort(byName);
|
|
85
|
+
|
|
86
|
+
const messageChannels = files(roots, 'messageChannels', '.messageChannel-meta.xml').map((f) => baseName(f, '.messageChannel-meta.xml')).sort();
|
|
87
|
+
return { lwc, aura, vfPages, flexipages, apps, tabs, quickActions, messageChannels, vfComponents: files(roots, 'components', '.component').length };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Where is each LWC / Aura component used? Scans pages, apps, tabs, actions, flows,
|
|
92
|
+
* OmniStudio and Experience bundles. Result: { name: [{ kind, name }] }.
|
|
93
|
+
*/
|
|
94
|
+
export function placements(roots, ui) {
|
|
95
|
+
const used = {};
|
|
96
|
+
const add = (comp, kind, name) => { if (!comp) return; (used[comp] ||= []).push({ kind, name }); };
|
|
97
|
+
for (const p of ui.flexipages) for (const c of p.customComponents) add(c, 'flexipage', p.name);
|
|
98
|
+
for (const l of ui.lwc) for (const c of l.children) add(c, 'lwc', l.name);
|
|
99
|
+
for (const a of ui.aura) for (const c of a.children) add(c, 'aura', a.name);
|
|
100
|
+
for (const tb of ui.tabs) { add(tb.lwc, 'tab', tb.name); add(tb.aura, 'tab', tb.name); }
|
|
101
|
+
for (const q of ui.quickActions) { add(q.lwc, 'quickAction', q.name); add(q.aura, 'quickAction', q.name); }
|
|
102
|
+
const scan = (folder, kind, re) => {
|
|
103
|
+
for (const f of files(roots, folder, '', { recursive: true })) {
|
|
104
|
+
const raw = safeRead(f);
|
|
105
|
+
for (const m of raw.matchAll(re)) add(m[1], kind, path.basename(f).split('.')[0]);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
scan('flows', 'flow', /<extensionName>c:([A-Za-z0-9_]+)<\/extensionName>/g);
|
|
109
|
+
const omniRe = /lwcName(?:"|")\s*:\s*(?:"|")([A-Za-z0-9_]+)/g;
|
|
110
|
+
scan('omniScripts', 'omniScript', omniRe);
|
|
111
|
+
scan('omniUiCard', 'flexCard', omniRe);
|
|
112
|
+
scan('experiences', 'experience', /"c:([A-Za-z0-9_]+)"/g);
|
|
113
|
+
scan('digitalExperiences', 'experience', /"c:([A-Za-z0-9_]+)"/g);
|
|
114
|
+
for (const k of Object.keys(used)) {
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
used[k] = used[k].filter((u) => { const key = u.kind + ':' + u.name; if (seen.has(key)) return false; seen.add(key); return true; })
|
|
117
|
+
.sort((a, b) => (a.kind + a.name).localeCompare(b.kind + b.name));
|
|
118
|
+
}
|
|
119
|
+
return used;
|
|
120
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Content fingerprints of the metadata, one per file: what `tadforce update` compares between two versions.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { walk, toPosix } from '../util/fsx.js';
|
|
6
|
+
|
|
7
|
+
/** Metadata folder → facts.json area it feeds. */
|
|
8
|
+
export const AREAS = {
|
|
9
|
+
classes: 'apex', triggers: 'apex',
|
|
10
|
+
objects: 'data', customMetadata: 'data.cmdtTypes', globalValueSets: 'data',
|
|
11
|
+
flows: 'automation.flows', workflows: 'automation',
|
|
12
|
+
omniScripts: 'automation.omnistudio', omniIntegrationProcedures: 'automation.omnistudio', omniDataTransforms: 'automation.omnistudio', omniUiCard: 'automation.omnistudio',
|
|
13
|
+
lwc: 'ui', aura: 'ui', pages: 'ui', components: 'ui', flexipages: 'ui', applications: 'ui', tabs: 'ui', quickActions: 'ui', messageChannels: 'ui', experiences: 'ui', digitalExperiences: 'ui', callCenters: 'integration',
|
|
14
|
+
permissionsets: 'security', permissionsetgroups: 'security', profiles: 'security', roles: 'security', queues: 'security', groups: 'security', sharingRules: 'security', customPermissions: 'security',
|
|
15
|
+
namedCredentials: 'integration', externalCredentials: 'integration', remoteSiteSettings: 'integration', connectedApps: 'integration', authproviders: 'integration', cspTrustedSites: 'integration', corsWhitelistOrigins: 'integration', externalServiceRegistrations: 'integration',
|
|
16
|
+
reports: 'misc', dashboards: 'misc', reportTypes: 'misc', staticresources: 'misc', labels: 'misc', email: 'misc',
|
|
17
|
+
genAiPlugins: 'modules', genAiFunctions: 'modules', genAiPromptTemplates: 'modules', bots: 'modules',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** One hash per metadata file, keyed by its path from the type folder (classes/Foo.cls). */
|
|
21
|
+
export function fingerprints(dirs) {
|
|
22
|
+
const out = {};
|
|
23
|
+
for (const dir of dirs) {
|
|
24
|
+
for (const f of walk(dir)) {
|
|
25
|
+
const parts = toPosix(path.relative(dir, f)).split('/');
|
|
26
|
+
const i = parts.findIndex((p) => AREAS[p]);
|
|
27
|
+
if (i < 0) continue;
|
|
28
|
+
out[parts.slice(i).join('/')] = crypto.createHash('sha1').update(fs.readFileSync(f)).digest('hex').slice(0, 16);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b)));
|
|
32
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Exclusions are decided by humans (knowledge/exclusions.yml). Heuristics only propose candidates.
|
|
2
|
+
|
|
3
|
+
/** Every component collection of facts.json, addressable by a short type name. */
|
|
4
|
+
export const COLLECTIONS = [
|
|
5
|
+
['apex', (f) => f.apex.classes],
|
|
6
|
+
['trigger', (f) => f.apex.triggers],
|
|
7
|
+
['lwc', (f) => f.ui.lwc],
|
|
8
|
+
['aura', (f) => f.ui.aura],
|
|
9
|
+
['vfPage', (f) => f.ui.vfPages],
|
|
10
|
+
['flexipage', (f) => f.ui.flexipages],
|
|
11
|
+
['app', (f) => f.ui.apps],
|
|
12
|
+
['tab', (f) => f.ui.tabs],
|
|
13
|
+
['quickAction', (f) => f.ui.quickActions],
|
|
14
|
+
['flow', (f) => f.automation.flows],
|
|
15
|
+
['omniScript', (f) => f.automation.omnistudio.omniScripts],
|
|
16
|
+
['integrationProcedure', (f) => f.automation.omnistudio.integrationProcedures],
|
|
17
|
+
['dataMapper', (f) => f.automation.omnistudio.dataMappers],
|
|
18
|
+
['flexCard', (f) => f.automation.omnistudio.flexCards],
|
|
19
|
+
['object', (f) => f.data.objects],
|
|
20
|
+
['cmdt', (f) => f.data.cmdtTypes, 'type'],
|
|
21
|
+
['profile', (f) => f.security.profiles],
|
|
22
|
+
['permissionSet', (f) => f.security.permissionSets],
|
|
23
|
+
['permissionSetGroup', (f) => f.security.permissionSetGroups],
|
|
24
|
+
['role', (f) => f.security.roles],
|
|
25
|
+
['queue', (f) => f.security.queues],
|
|
26
|
+
['namedCredential', (f) => f.integration.namedCredentials],
|
|
27
|
+
['externalCredential', (f) => f.integration.externalCredentials],
|
|
28
|
+
['remoteSite', (f) => f.integration.remoteSites],
|
|
29
|
+
['connectedApp', (f) => f.integration.connectedApps],
|
|
30
|
+
['authProvider', (f) => f.integration.authProviders],
|
|
31
|
+
['report', (f) => f.misc.reports.names, null],
|
|
32
|
+
['dashboard', (f) => f.misc.dashboards.names, null],
|
|
33
|
+
['emailTemplate', (f) => f.misc.emailTemplates, null],
|
|
34
|
+
['staticResource', (f) => f.misc.staticResources, null],
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const keyOf = (item, field) => (field === null ? item : item[field || 'name']);
|
|
38
|
+
|
|
39
|
+
function matcher(pattern) {
|
|
40
|
+
const re = new RegExp('^' + String(pattern).replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$', 'i');
|
|
41
|
+
return (s) => re.test(String(s));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Remove excluded components from facts (in place). Returns the list of applied exclusions. */
|
|
45
|
+
export function applyExclusions(facts, knowledge = {}) {
|
|
46
|
+
const rules = [...(knowledge.exclude || [])];
|
|
47
|
+
for (const feat of knowledge.features || []) {
|
|
48
|
+
for (const c of feat.components || []) rules.push({ name: typeof c === 'string' ? c : c.name, type: c.type, reason: `${feat.name}${feat.note ? ' — ' + feat.note : ''}`, feature: feat.name });
|
|
49
|
+
}
|
|
50
|
+
const applied = [];
|
|
51
|
+
for (const rule of rules) {
|
|
52
|
+
if (!rule || !rule.name) continue;
|
|
53
|
+
const match = matcher(rule.name);
|
|
54
|
+
for (const [type, get, field] of COLLECTIONS) {
|
|
55
|
+
if (rule.type && rule.type !== 'any' && rule.type !== type) continue;
|
|
56
|
+
const list = get(facts);
|
|
57
|
+
if (!Array.isArray(list)) continue;
|
|
58
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
59
|
+
const k = keyOf(list[i], field);
|
|
60
|
+
if (match(k)) {
|
|
61
|
+
applied.push({ type, name: k, reason: rule.reason || null, feature: rule.feature || null });
|
|
62
|
+
list.splice(i, 1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return applied.sort((a, b) => (a.type + a.name).localeCompare(b.type + b.name));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Org-wide components (profiles, roles, queues, report folders…) are shared by every project: never scoped out.
|
|
71
|
+
const SCOPED_TYPES = new Set(['apex', 'trigger', 'lwc', 'aura', 'vfPage', 'flexipage', 'app', 'tab', 'quickAction', 'flow', 'omniScript', 'integrationProcedure', 'dataMapper', 'flexCard', 'object', 'cmdt', 'permissionSet', 'permissionSetGroup', 'namedCredential', 'externalCredential', 'remoteSite', 'connectedApp', 'authProvider', 'staticResource']);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Keep only the part of the org a project owns (tadforce.config.json › scope.include, name patterns with `*`).
|
|
75
|
+
* Standard objects stay: a project extends them, it does not own them. Returns the components set aside.
|
|
76
|
+
*/
|
|
77
|
+
export function applyScope(facts, scope = {}) {
|
|
78
|
+
const include = (scope.include || []).filter(Boolean).map(matcher);
|
|
79
|
+
if (!include.length) return [];
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const [type, get, field] of COLLECTIONS) {
|
|
82
|
+
if (!SCOPED_TYPES.has(type)) continue;
|
|
83
|
+
const list = get(facts);
|
|
84
|
+
if (!Array.isArray(list)) continue;
|
|
85
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
86
|
+
const k = keyOf(list[i], field);
|
|
87
|
+
if (type === 'object' && !String(k).includes('__')) continue;
|
|
88
|
+
if (type === 'apex' && list[i].test && include.some((m) => m(String(k).replace(/_?Tests?$/i, '')))) continue;
|
|
89
|
+
if (!include.some((m) => m(k))) { out.push({ type, name: k }); list.splice(i, 1); }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return out.sort((a, b) => (a.type + a.name).localeCompare(b.type + b.name));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const SUSPICIOUS = new Set(['test', 'tests', 'poc', 'demo', 'demos', 'sample', 'samples', 'dryrun', 'repro', 'tmp', 'temp', 'old', 'deprecated', 'obsolete', 'backup', 'bkp', 'copy', 'draft', 'example', 'examples', 'playground', 'todelete', 'unused', 'legacy', 'dummy', 'fake', 'mock']);
|
|
96
|
+
|
|
97
|
+
export function nameWords(name) {
|
|
98
|
+
return String(name)
|
|
99
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
100
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
101
|
+
.split(/[^A-Za-z]+/)
|
|
102
|
+
.map((w) => w.toLowerCase())
|
|
103
|
+
.filter(Boolean);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Propose exclusion candidates (never applied automatically): suspicious names, components
|
|
108
|
+
* placed nowhere, inactive automation, Apex referenced only by tests.
|
|
109
|
+
*/
|
|
110
|
+
export function exclusionCandidates(facts, refs) {
|
|
111
|
+
const out = [];
|
|
112
|
+
const add = (type, name, reason) => out.push({ type, name, reason });
|
|
113
|
+
for (const [type, get, field] of COLLECTIONS) {
|
|
114
|
+
const list = get(facts) || [];
|
|
115
|
+
for (const item of list) {
|
|
116
|
+
const name = keyOf(item, field);
|
|
117
|
+
if (type === 'apex' && item.test) continue;
|
|
118
|
+
const bad = nameWords(name).filter((w) => SUSPICIOUS.has(w));
|
|
119
|
+
if (bad.length) add(type, name, `name suggests ${bad.join('/')}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const l of facts.ui.lwc) if (!refs.placements[l.name]) add('lwc', l.name, 'not placed on any page, app, tab, action, flow or OmniStudio component in the snapshot');
|
|
123
|
+
for (const a of facts.ui.aura) if (a.type === 'component' && !refs.placements[a.name]) add('aura', a.name, 'not referenced by any page, app, tab, action or component in the snapshot');
|
|
124
|
+
for (const fl of facts.automation.flows) if (!fl.active) add('flow', fl.name, `flow status is ${fl.status}`);
|
|
125
|
+
for (const k of ['omniScripts', 'integrationProcedures', 'dataMappers', 'flexCards']) {
|
|
126
|
+
for (const o of facts.automation.omnistudio[k]) if (!o.active) add(k.replace(/s$/, ''), o.name, 'inactive version');
|
|
127
|
+
}
|
|
128
|
+
const ENTRY = new Set(['rest', 'soap', 'schedulable', 'invocable', 'registrationHandler', 'emailHandler', 'global']);
|
|
129
|
+
for (const c of facts.apex.classes) {
|
|
130
|
+
if (c.test || c.kinds.some((k) => ENTRY.has(k))) continue;
|
|
131
|
+
if (!refs.apexReferenced.has(c.name)) add('apex', c.name, refs.apexReferencedByTests.has(c.name) ? 'referenced only by test classes' : 'not referenced by any other component in the snapshot');
|
|
132
|
+
}
|
|
133
|
+
for (const p of facts.ui.flexipages) {
|
|
134
|
+
if (!refs.flexipageAssigned.has(p.name)) add('flexipage', p.name, 'not assigned to any app, object override or tab in the snapshot');
|
|
135
|
+
}
|
|
136
|
+
// merge duplicates (one entry per component, reasons joined)
|
|
137
|
+
const merged = {};
|
|
138
|
+
for (const c of out) {
|
|
139
|
+
const k = c.type + ':' + c.name;
|
|
140
|
+
if (merged[k]) { if (!merged[k].reasons.includes(c.reason)) merged[k].reasons.push(c.reason); } else merged[k] = { type: c.type, name: c.name, reasons: [c.reason] };
|
|
141
|
+
}
|
|
142
|
+
return Object.values(merged).sort((a, b) => (a.type + a.name).localeCompare(b.type + b.name));
|
|
143
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Which Salesforce products this org uses, from evidence in the metadata only.
|
|
2
|
+
// Each detected cloud maps to a reference in skills/tadforce/references/modules/ and to
|
|
3
|
+
// conditional sections of the skeletons (<!-- tad:if modules.<flag> -->).
|
|
4
|
+
|
|
5
|
+
const has = (set, ...names) => names.some((n) => set.has(n));
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Evidence rules. `objects`: standard objects whose presence in the snapshot (with custom fields,
|
|
9
|
+
* record types or page usage) signals the product; `ns`: managed package namespaces; `test`: other signals.
|
|
10
|
+
*/
|
|
11
|
+
const CLOUDS = [
|
|
12
|
+
{ id: 'salesCloud', name: 'Sales Cloud', ref: 'sales-cloud', objects: ['Opportunity', 'Lead', 'Quote', 'Forecasting', 'Territory2', 'OpportunityLineItem', 'ActionCadence'], test: (f) => f.misc.folders.territory2Models > 0 || f.misc.folders.forecastingTypes > 0 },
|
|
13
|
+
{ id: 'serviceCloud', name: 'Service Cloud', ref: 'service-cloud', objects: ['Case', 'Entitlement', 'ServiceContract', 'MessagingSession', 'LiveChatTranscript', 'VoiceCall', 'CaseMilestone'], test: (f) => f.misc.signals.entitlementProcesses > 0 || f.misc.signals.serviceChannels > 0 || f.data.objects.some((o) => o.name.endsWith('__kav')) },
|
|
14
|
+
{ id: 'fieldService', name: 'Field Service', ref: 'field-service', ns: ['FSL'], objects: ['WorkOrder', 'ServiceAppointment', 'ServiceTerritory', 'ServiceResource', 'WorkOrderLineItem', 'MaintenancePlan'] },
|
|
15
|
+
{ id: 'experienceCloud', name: 'Experience Cloud', ref: 'experience-cloud', test: (f) => f.misc.signals.networks + f.misc.signals.experiences + f.misc.signals.sites > 0 },
|
|
16
|
+
{ id: 'marketingCloud', name: 'Marketing Cloud Connect', ref: 'marketing', ns: ['et4ae5'] },
|
|
17
|
+
{ id: 'accountEngagement', name: 'Marketing Cloud Account Engagement', ref: 'marketing', ns: ['pi'] },
|
|
18
|
+
{ id: 'commerce', name: 'Commerce Cloud (B2B / D2C)', ref: 'commerce', objects: ['WebStore', 'WebCart', 'ProductCatalog', 'BuyerGroup', 'BuyerAccount', 'CommerceEntitlementPolicy'] },
|
|
19
|
+
{ id: 'orderManagement', name: 'Order Management', ref: 'commerce', objects: ['OrderSummary', 'FulfillmentOrder', 'OrderDeliveryGroupSummary', 'ReturnOrder'] },
|
|
20
|
+
{ id: 'cpq', name: 'Salesforce CPQ', ref: 'revenue-cpq', ns: ['SBQQ'] },
|
|
21
|
+
{ id: 'billing', name: 'Salesforce Billing', ref: 'revenue-cpq', ns: ['blng'] },
|
|
22
|
+
{ id: 'revenueCloud', name: 'Revenue Cloud', ref: 'revenue-cpq', objects: ['ProductSellingModel', 'ProductSellingModelOption', 'BillingSchedule', 'Invoice', 'ProductClassification'] },
|
|
23
|
+
{ id: 'financialServices', name: 'Financial Services Cloud', ref: 'industries', ns: ['FinServ'], objects: ['FinancialAccount', 'FinancialAccountRole', 'FinancialGoal', 'PartyProfile', 'ResidentialLoanApplication'] },
|
|
24
|
+
{ id: 'insurance', name: 'Insurance (Financial Services Cloud)', ref: 'industries', ns: ['vlocity_ins'], objects: ['InsurancePolicy', 'InsurancePolicyCoverage', 'Claim', 'Producer', 'InsurancePolicyParticipant'] },
|
|
25
|
+
{ id: 'healthCloud', name: 'Health Cloud', ref: 'industries', ns: ['HealthCloudGA'], objects: ['CarePlan', 'CareProgram', 'ClinicalEncounter', 'HealthcareProvider', 'PatientMedicalProcedure'] },
|
|
26
|
+
{ id: 'lifeSciences', name: 'Life Sciences Cloud', ref: 'industries', objects: ['LifeSciMarketableProduct', 'ProviderVisit', 'ClinicalStudy'] },
|
|
27
|
+
{ id: 'manufacturing', name: 'Manufacturing Cloud', ref: 'industries', objects: ['SalesAgreement', 'AccountForecast', 'SalesAgreementProduct', 'WarrantyTerm'] },
|
|
28
|
+
{ id: 'automotive', name: 'Automotive Cloud', ref: 'industries', objects: ['Vehicle', 'VehicleDefinition', 'DriverQualification'] },
|
|
29
|
+
{ id: 'consumerGoods', name: 'Consumer Goods Cloud', ref: 'industries', ns: ['cgcloud'], objects: ['RetailStore', 'Visit', 'RetailVisitKpi', 'InStoreLocation'] },
|
|
30
|
+
{ id: 'communications', name: 'Communications / Media / Energy & Utilities Cloud', ref: 'industries', ns: ['vlocity_cmt'], objects: ['ServicePoint', 'BillingAccount'] },
|
|
31
|
+
{ id: 'publicSector', name: 'Public Sector Solutions', ref: 'industries', ns: ['vlocity_ps'], objects: ['IndividualApplication', 'BusinessLicense', 'RegulatoryCode', 'PublicComplaint', 'BenefitAssignment'] },
|
|
32
|
+
{ id: 'education', name: 'Education Cloud', ref: 'industries', ns: ['hed'], objects: ['LearnerProfile', 'AcademicTermEnrollment', 'LearningProgram', 'ProgramEnrollment'] },
|
|
33
|
+
{ id: 'nonprofit', name: 'Nonprofit (Nonprofit Cloud / NPSP)', ref: 'industries', ns: ['npsp', 'npe01', 'npo02', 'npe03', 'npe4', 'npe5'], objects: ['GiftTransaction', 'GiftCommitment', 'Program', 'ProgramEnrollment'] },
|
|
34
|
+
{ id: 'loyalty', name: 'Loyalty Management', ref: 'industries', objects: ['LoyaltyProgram', 'LoyaltyProgramMember', 'LoyaltyLedger', 'TransactionJournal'] },
|
|
35
|
+
{ id: 'netZero', name: 'Net Zero Cloud', ref: 'industries', objects: ['StnryAssetCrbnFtprnt', 'VehicleAssetCrbnFtprnt', 'Scope3CrbnFtprnt'] },
|
|
36
|
+
{ id: 'omnistudio', name: 'OmniStudio', ref: 'omnistudio', ns: ['omnistudio', 'vlocity_cmt', 'vlocity_ins', 'vlocity_ps'], test: (f) => Object.values(f.automation.omnistudio).some((l) => l.length > 0) },
|
|
37
|
+
{ id: 'dataCloud', name: 'Data Cloud', ref: 'data-cloud', test: (f) => f.misc.signals.dataStreams > 0 || f.data.objects.some((o) => /__(dlm|dll)$/.test(o.name)) || (f.misc.folders.dataStreamDefinitions || 0) + (f.misc.folders.dataSourceObjects || 0) > 0 },
|
|
38
|
+
{ id: 'agentforce', name: 'Agentforce / Einstein', ref: 'agentforce-ai', test: (f) => f.misc.signals.bots + f.misc.signals.genAi > 0 || (f.misc.folders.genAiPromptTemplates || 0) + (f.misc.folders.mlPredictionDefinitions || 0) > 0 },
|
|
39
|
+
{ id: 'crmAnalytics', name: 'CRM Analytics', ref: 'analytics', ns: ['wave'], test: (f) => ['wave', 'waveDashboards', 'waveDataflows', 'waveRecipes', 'waveApplications', 'waveDatasets'].some((k) => f.misc.folders[k] > 0) },
|
|
40
|
+
{ id: 'shield', name: 'Shield (encryption, event monitoring, audit)', ref: 'security-shield', test: (f) => ['transactionSecurityPolicies', 'eventSubscriptions'].some((k) => f.misc.folders[k] > 0) || f.misc.settings.some((s) => /PlatformEncryption|EventSettings|FieldHistoryRetention/.test(s)) },
|
|
41
|
+
{ id: 'slack', name: 'Slack integration', ref: 'integration-platforms', ns: ['slackv2', 'slack'], test: (f) => (f.misc.folders.slackApps || 0) > 0 },
|
|
42
|
+
{ id: 'events', name: 'Platform Events / Change Data Capture', ref: 'platform-events', test: (f) => f.data.objects.some((o) => o.kind === 'platformEvent') || f.data.cdc.length > 0 },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** Standard objects count as evidence when the org customised or uses them. */
|
|
46
|
+
function usedStandard(f) {
|
|
47
|
+
const used = new Set();
|
|
48
|
+
for (const o of f.data.objects) if (o.kind === 'standard' && (o.customFields > 0 || o.recordTypes.length || o.validationRules.length)) used.add(o.name);
|
|
49
|
+
for (const p of f.ui.flexipages) if (p.object) used.add(p.object);
|
|
50
|
+
for (const t of f.apex.triggers) if (t.object) used.add(t.object);
|
|
51
|
+
for (const fl of f.automation.flows) if (fl.object) used.add(fl.object);
|
|
52
|
+
for (const q of f.security.queues) q.objects.forEach((x) => used.add(x));
|
|
53
|
+
return used;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function detectModules(f) {
|
|
57
|
+
const ns = new Set(f.namespaces.map((n) => n.namespace));
|
|
58
|
+
const std = usedStandard(f);
|
|
59
|
+
const clouds = [];
|
|
60
|
+
const flags = {};
|
|
61
|
+
for (const c of CLOUDS) {
|
|
62
|
+
const evidence = [];
|
|
63
|
+
for (const n of c.ns || []) if (ns.has(n)) evidence.push(`namespace ${n}`);
|
|
64
|
+
for (const o of c.objects || []) if (std.has(o) || f.data.objects.some((x) => x.name === o && x.kind === 'standard' && x.customFields > 0)) evidence.push(`object ${o}`);
|
|
65
|
+
if (c.test && c.test(f)) evidence.push('metadata');
|
|
66
|
+
const on = evidence.length > 0;
|
|
67
|
+
flags[c.id] = on;
|
|
68
|
+
if (on) clouds.push({ id: c.id, name: c.name, reference: c.ref, evidence: [...new Set(evidence)].slice(0, 6) });
|
|
69
|
+
}
|
|
70
|
+
const industryIds = ['financialServices', 'insurance', 'healthCloud', 'lifeSciences', 'manufacturing', 'automotive', 'consumerGoods', 'communications', 'publicSector', 'education', 'nonprofit', 'loyalty', 'netZero'];
|
|
71
|
+
const products = [...new Set([...clouds.map((c) => c.name), ...f.namespaces.filter((n) => n.product).map((n) => n.product)])].sort();
|
|
72
|
+
return {
|
|
73
|
+
...flags,
|
|
74
|
+
industryCloud: clouds.filter((c) => industryIds.includes(c.id)).map((c) => c.name),
|
|
75
|
+
omniChannel: f.misc.signals.serviceChannels + f.misc.signals.routingConfigs > 0,
|
|
76
|
+
cti: f.integration.callCenters.length > 0,
|
|
77
|
+
visualforce: f.ui.vfPages.length > 0,
|
|
78
|
+
aura: f.ui.aura.length > 0,
|
|
79
|
+
legacyAutomation: f.automation.rules.workflowRules + f.automation.rules.processBuilders > 0,
|
|
80
|
+
mobile: f.ui.apps.some((a) => a.formFactors.includes('Small')),
|
|
81
|
+
clouds,
|
|
82
|
+
references: [...new Set(clouds.map((c) => c.reference))].sort(),
|
|
83
|
+
products,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Observed naming conventions: most frequent prefixes and suffixes per component family.
|
|
2
|
+
const SUFFIXES = ['TriggerHandler', 'Handler', 'Controller', 'Service', 'Selector', 'Domain', 'Batch', 'Queueable', 'Scheduler', 'Schedulable', 'Helper', 'Utils', 'Util', 'Manager', 'Engine', 'Factory', 'Wrapper', 'DTO', 'Mock', 'Test', 'Exception', 'Callout', 'Client', 'Resource', 'Api'];
|
|
3
|
+
|
|
4
|
+
function top(map, n = 6, min = 2) {
|
|
5
|
+
return Object.entries(map).filter(([, c]) => c >= min).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, n).map(([value, count]) => ({ value, count }));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function prefixOf(name) {
|
|
9
|
+
const m = /^([A-Za-z]+\d*_)/.exec(name) || /^([a-z]+\d+)/.exec(name) || /^([A-Z]{2,5})(?=[A-Z][a-z])/.exec(name);
|
|
10
|
+
return m ? m[1] : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function prefixes(names) {
|
|
14
|
+
const counts = {};
|
|
15
|
+
for (const n of names) { const p = prefixOf(n); if (p) counts[p] = (counts[p] || 0) + 1; }
|
|
16
|
+
return top(counts);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function detectNaming(f) {
|
|
20
|
+
const classes = f.apex.classes.map((c) => c.name);
|
|
21
|
+
const suffixCounts = {};
|
|
22
|
+
for (const n of classes) { const s = SUFFIXES.find((x) => n.endsWith(x)); if (s) suffixCounts[s] = (suffixCounts[s] || 0) + 1; }
|
|
23
|
+
return {
|
|
24
|
+
apex: { prefixes: prefixes(classes), suffixes: top(suffixCounts, 10, 1) },
|
|
25
|
+
triggers: { prefixes: prefixes(f.apex.triggers.map((t) => t.name)) },
|
|
26
|
+
lwc: { prefixes: prefixes(f.ui.lwc.map((l) => l.name)) },
|
|
27
|
+
flows: { prefixes: prefixes(f.automation.flows.map((x) => x.name)) },
|
|
28
|
+
objects: { prefixes: prefixes(f.data.objects.filter((o) => o.kind === 'custom' && !o.namespace).map((o) => o.name)) },
|
|
29
|
+
permissionSets: { prefixes: prefixes(f.security.permissionSets.map((p) => p.name)) },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Review signals for the Improvement Axes document. A signal is a measurable observation,
|
|
2
|
+
// not a verdict: the architect confirms, qualifies and prioritises it.
|
|
3
|
+
|
|
4
|
+
const list = (items, n = 12) => items.slice(0, n);
|
|
5
|
+
|
|
6
|
+
export function computeSignals(f, org = null, codeAnalyzer = null) {
|
|
7
|
+
const s = [];
|
|
8
|
+
const add = (id, pillar, priority, value, evidence = [], extra = {}) => { if (value) s.push({ id, pillar, priority, value, evidence: list(evidence), total: evidence.length, ...extra }); };
|
|
9
|
+
const app = f.apex.classes.filter((c) => !c.test);
|
|
10
|
+
const tests = f.apex.classes.filter((c) => c.test);
|
|
11
|
+
|
|
12
|
+
const versions = [...app, ...f.apex.triggers, ...f.automation.flows].map((x) => parseFloat(x.apiVersion)).filter((v) => !Number.isNaN(v));
|
|
13
|
+
const maxApi = versions.length ? Math.max(...versions) : null;
|
|
14
|
+
if (maxApi) {
|
|
15
|
+
const old = [...app, ...f.apex.triggers].filter((x) => parseFloat(x.apiVersion) <= maxApi - 15);
|
|
16
|
+
add('api-version-old', 'adaptable', 'p3', old.length, old.map((x) => `${x.name} (${x.apiVersion})`), { threshold: `≤ ${(maxApi - 15).toFixed(1)}` });
|
|
17
|
+
}
|
|
18
|
+
const without = app.filter((c) => c.sharing === 'without');
|
|
19
|
+
add('without-sharing', 'trusted', 'p2', without.length, without.map((c) => c.name));
|
|
20
|
+
const none = app.filter((c) => c.sharing === 'none' && !c.kinds.includes('interface') && !c.kinds.includes('enum'));
|
|
21
|
+
add('no-sharing-keyword', 'trusted', 'p3', none.length, none.map((c) => c.name));
|
|
22
|
+
const ids = app.filter((c) => c.hardcodedIds > 0);
|
|
23
|
+
add('hardcoded-ids', 'adaptable', 'p2', ids.length, ids.map((c) => c.name));
|
|
24
|
+
const loops = app.filter((c) => c.soqlInLoops > 0);
|
|
25
|
+
add('soql-dml-in-loops', 'easy', 'p2', loops.length, loops.map((c) => `${c.name} (${c.soqlInLoops})`));
|
|
26
|
+
const big = app.filter((c) => c.lines > 1000);
|
|
27
|
+
add('large-classes', 'adaptable', 'p3', big.length, big.map((c) => `${c.name} (${c.lines} lines)`));
|
|
28
|
+
|
|
29
|
+
const perObject = {};
|
|
30
|
+
for (const t of f.apex.triggers) (perObject[t.object] ||= []).push(t.name);
|
|
31
|
+
const multi = Object.entries(perObject).filter(([, v]) => v.length > 1);
|
|
32
|
+
add('multiple-triggers-per-object', 'adaptable', 'p2', multi.length, multi.map(([o, v]) => `${o}: ${v.join(', ')}`));
|
|
33
|
+
const fat = f.apex.triggers.filter((t) => t.lines > 30);
|
|
34
|
+
add('logic-in-triggers', 'adaptable', 'p3', fat.length, fat.map((t) => `${t.name} (${t.lines} lines)`));
|
|
35
|
+
|
|
36
|
+
if (app.length) {
|
|
37
|
+
const ratio = tests.length / app.length;
|
|
38
|
+
if (ratio < 0.5) add('low-test-ratio', 'trusted', 'p2', `${tests.length}/${app.length}`, [], { ratio: Math.round(ratio * 100) / 100 });
|
|
39
|
+
}
|
|
40
|
+
if (org && typeof org.coverage === 'number') {
|
|
41
|
+
if (org.coverage < 75) add('org-coverage', 'trusted', 'p1', `${org.coverage}%`);
|
|
42
|
+
else if (org.coverage < 85) add('org-coverage', 'trusted', 'p3', `${org.coverage}%`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
add('aura-residual', 'adaptable', 'p3', f.ui.aura.length, f.ui.aura.map((a) => a.name));
|
|
46
|
+
add('visualforce-residual', 'adaptable', 'p3', f.ui.vfPages.length, f.ui.vfPages.map((p) => p.name));
|
|
47
|
+
add('workflow-rules', 'adaptable', 'p2', f.automation.rules.workflowRules);
|
|
48
|
+
add('process-builders', 'adaptable', 'p2', f.automation.rules.processBuilders, f.automation.flows.filter((x) => x.processType === 'Workflow').map((x) => x.name));
|
|
49
|
+
const inactive = f.automation.flows.filter((x) => !x.active);
|
|
50
|
+
add('inactive-flows', 'easy', 'p3', inactive.length, inactive.map((x) => `${x.name} (${x.status})`));
|
|
51
|
+
const flowsPerObject = {};
|
|
52
|
+
for (const fl of f.automation.flows.filter((x) => x.active && x.object && /Record/.test(x.triggerType || ''))) (flowsPerObject[fl.object] ||= []).push(fl.name);
|
|
53
|
+
const crowded = Object.entries(flowsPerObject).filter(([o]) => perObject[o]);
|
|
54
|
+
add('flows-and-triggers-same-object', 'adaptable', 'p3', crowded.length, crowded.map(([o, v]) => `${o}: ${perObject[o].join(', ')} + ${v.join(', ')}`));
|
|
55
|
+
|
|
56
|
+
const powerful = [...f.security.profiles, ...f.security.permissionSets].filter((p) => p.powerful.some((x) => ['ModifyAllData', 'ViewAllData'].includes(x)));
|
|
57
|
+
add('powerful-permissions', 'trusted', 'p2', powerful.length, powerful.map((p) => `${p.name} (${p.powerful.filter((x) => ['ModifyAllData', 'ViewAllData'].includes(x)).join(', ')})`));
|
|
58
|
+
const publicRW = f.data.objects.filter((o) => o.kind === 'custom' && !o.namespace && o.sharingModel === 'ReadWrite');
|
|
59
|
+
add('public-read-write-objects', 'trusted', 'p3', publicRW.length, publicRW.map((o) => o.name));
|
|
60
|
+
const insecure = f.integration.remoteSites.filter((r) => r.insecure);
|
|
61
|
+
add('insecure-remote-sites', 'trusted', 'p1', insecure.length, insecure.map((r) => r.name));
|
|
62
|
+
const legacyNc = f.integration.namedCredentials.filter((n) => n.type === 'Legacy');
|
|
63
|
+
add('legacy-named-credentials', 'trusted', 'p3', legacyNc.length, legacyNc.map((n) => n.name));
|
|
64
|
+
const fullScope = f.integration.connectedApps.filter((a) => a.scopes.includes('Full'));
|
|
65
|
+
add('connected-app-full-scope', 'trusted', 'p2', fullScope.length, fullScope.map((a) => a.name));
|
|
66
|
+
const undocumented = f.data.objects.filter((o) => o.kind === 'custom' && !o.namespace && !o.description);
|
|
67
|
+
add('objects-without-description', 'easy', 'p3', undocumented.length, undocumented.map((o) => o.name));
|
|
68
|
+
add('exclusion-candidates', 'easy', 'p3', f.exclusions.candidates.length, f.exclusions.candidates.map((c) => `${c.type} ${c.name}`));
|
|
69
|
+
|
|
70
|
+
if (codeAnalyzer) {
|
|
71
|
+
for (const [sev, n] of Object.entries(codeAnalyzer.bySeverity || {})) {
|
|
72
|
+
if (n) add(`code-analyzer-sev${sev}`, 'trusted', Number(sev) <= 2 ? 'p1' : Number(sev) === 3 ? 'p2' : 'p3', n, (codeAnalyzer.topRules || []).map((r) => `${r.rule} (${r.count})`));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return s;
|
|
76
|
+
}
|