Setting the file. One moment.
Plugin Disposition · Rp Source Wordpress · wix/skills · Skills Docs
ContentsBack to the top of the page lib/ plugin-disposition.js
JavaScript · 400 lines · 18 KB
14
label:
'Requires development'
,
15 order: 1 ,
16 blurb: 'Wix has no surface for this; someone has to build it first. A human-signed verdict — automation never concludes this on its own.' ,
17 },
18 pending: {
19 label: 'Pending' ,
20 order: 2 ,
21 blurb: 'We do not know how to migrate this yet. Our open item, decided at the mapping review — never a limitation of the source or of Wix.' ,
22 },
23 // For manual-mapping: a complete, decided mapping, not a pending one — Wix can do this, but reaching
24 // it is a runbook the merchant clicks through, not an API call our code makes. Ranked below
25 // migration-planned only because merchant time is still owed, never because the decision is
26 // less final; ranked above pending because, unlike pending, nothing here is undecided.
27 'manual-mapping' : {
28 label: 'Manual mapping available' ,
29 order: 3 ,
30 blurb: 'Wix can do this. The mapping is decided — these are the exact steps you take yourself, no further review needed.' ,
31 },
32 'migration-planned' : {
33 label: 'Migration planned' ,
34 order: 4 ,
35 blurb: 'This comes across — via API into a native Wix entity, or via CMS as data keeping its original IDs.' ,
36 },
37 'no-need-to-migrate' : {
38 label: 'No need to migrate' ,
39 order: 5 ,
40 blurb: 'Nothing to move: Wix already does it, it was never data, or it is a setting you reconfigure once in Wix.' ,
41 },
42 };
43
44 // Worst-first: the status a plugin shows is the most demanding one across its capability
45 // rows. A plugin that is half planned and half pending is a pending conversation.
46 const STATUS_PRECEDENCE = [ 'requires-development' , 'pending' , 'manual-mapping' , 'migration-planned' , 'no-need-to-migrate' ];
47
48 function worstStatus ( statuses ) {
49 for ( const status of STATUS_PRECEDENCE ) {
50 if (statuses. includes (status)) return status;
51 }
52 return null ;
53 }
54
55 function hintIndex ( hintsFile ) {
56 const index = new Map ();
57 for ( const hint of (hintsFile && hintsFile.hints) || []) {
58 if (hint && hint.slug) index. set (hint.slug, hint);
59 }
60 return index;
61 }
62
63 function slugOf ( pluginId ) {
64 return String (pluginId || '' ). split ( '/' )[ 0 ];
65 }
66
67 /**
68 * Build one row per installed plugin from the coverage rows. When the plugin list was
69 * unavailable (no admin credential) there is no installed set to iterate, so rows come from
70 * detections only — and that limitation is reported rather than hidden.
71 */
72 function buildDispositionRows ({
73 detection = null ,
74 coverage = [],
75 profiles = [],
76 hints = null ,
77 } = {}) {
78 const hintsBySlug = hintIndex (hints);
79 const profilesBySlug = new Map (profiles. map (( profile ) => [profile.plugin, profile]));
80 const rows = [];
81
82 // A capability can carry two rows — the recognized profile row and a derived proposal
83 // (VERIFIED LIVE 2026-08-10: content.events held the TEC profile row plus a proposed row
84 // from unrelated event-ish CPTs). A recognized plugin's view must reflect its own row, so
85 // the recognized row wins the key; insertion order must not decide.
86 const coverageByCapability = new Map ();
87 for ( const row of coverage) {
88 const existing = coverageByCapability. get (row.capability);
89 if ( ! existing || (row.recognized && ! existing.recognized)) coverageByCapability. set (row.capability, row);
90 }
91 const coverageByPlugin = new Map ();
92 for ( const row of coverage) {
93 for ( const plugin of row.plugins || []) {
94 const slug = slugOf (plugin) || plugin;
95 if ( ! coverageByPlugin. has (slug)) coverageByPlugin. set (slug, []);
96 coverageByPlugin. get (slug). push (row);
97 }
98 }
99
100 // 1. Recognized plugins — the richest rows.
101 for ( const detected of detection?.detected || []) {
102 const profile = profilesBySlug. get (detected.plugin);
103 const capRows = detected.capabilities
104 . map (( capability ) => coverageByCapability. get (capability))
105 . filter (Boolean);
106 const status = worstStatus (capRows. map (( row ) => row.status)) || 'pending' ;
107 const plannedRows = capRows. filter (( row ) => row.status === 'migration-planned' );
108 const manualMappingRows = capRows. filter (( row ) => row.status === 'manual-mapping' );
109
110 rows. push ({
111 plugin: detected.plugin,
112 displayName: detected.displayName || detected.plugin,
113 version: detected.version,
114 active: detected.active,
115 does: profile?.does || describeFromCapabilities (detected.capabilities),
116 status,
117 via: plannedRows. length > 0
118 ? Array. from ( new Set (plannedRows. map (( row ) => row.via))). sort (). join ( '+' )
119 : null ,
120 mappingConfidence: plannedRows. length > 0
121 ? (plannedRows. every (( row ) => row.confidence === 'confirmed' ) ? 'confirmed' : 'proposed' )
122 : null ,
123 consequence: capRows. map (( row ) => row.userImpact). filter (Boolean). join ( ' ' )
124 || profile?.replacedBy
125 || '' ,
126 // The runbook to render inline when status is manual-mapping.
127 // One plugin could in principle carry more than one manual-mapping capability; only the
128 // first is rendered today, same simplification `via`/`mappingConfidence` above already
129 // make for multi-capability plugins.
130 manualSteps: manualMappingRows[ 0 ]?.manualSteps || null ,
131 // --- debugging basis ---
132 recognized: true ,
133 inventoryBasis: detection.pluginListAvailable && detected.signals. includes ( 'wp.v2.plugins' )
134 ? 'verified'
135 : 'fingerprinted' ,
136 statusBasis: capRows. length > 0
137 ? Array. from ( new Set (capRows. map (( row ) => row.basis))). sort (). join ( '+' )
138 : 'profile' ,
139 confidence: detected.confidence,
140 signals: detected.signals,
141 capabilities: detected.capabilities,
142 channels: Array. from ( new Set (detected.entities. map (( entity ) => entity.channel))). sort (),
143 channelStatuses: Array. from ( new Set (detected.entities. map (( entity ) => entity.channelStatus))). sort (),
144 targetRefs: Array. from ( new Set (capRows. flatMap (( row ) => row.targetRefs || []))). sort (),
145 profileVersion: detected.profileVersion || null ,
146 blocked: capRows. flatMap (( row ) => row.blocked || []),
147 blockers: capRows. flatMap (( row ) => (row.pitfalls || []). filter (( pitfall ) => pitfall.severity === 'blocker' ). map (( pitfall ) => pitfall.summary)),
148 });
149 }
150
151 // 2. Installed but unrecognized — projected from the rows classifyCoverage already made
152 // (attributed namespace/CPT rows, no-migration-needed list rows, or pending/cannot-tell).
153 for ( const installed of detection?.installedButUnprofiled || []) {
154 const slug = slugOf (installed.plugin);
155 const hint = hintsBySlug. get (slug);
156 const capRows = coverageByPlugin. get (slug) || [];
157 const status = worstStatus (capRows. map (( row ) => row.status)) || 'pending' ;
158 const plannedRows = capRows. filter (( row ) => row.status === 'migration-planned' );
159 const noNeedRow = capRows. find (( row ) => row.status === 'no-need-to-migrate' );
160
161 rows. push ({
162 plugin: installed.plugin,
163 displayName: installed.name || slug,
164 version: installed.version,
165 active: installed.active,
166 does: hint?.does || '' ,
167 status,
168 via: plannedRows. length > 0
169 ? Array. from ( new Set (plannedRows. map (( row ) => row.via))). sort (). join ( '+' )
170 : null ,
171 mappingConfidence: plannedRows. length > 0 ? 'proposed' : null ,
172 consequence: noNeedRow?.rationale
173 || capRows. map (( row ) => row.userImpact). filter (Boolean). join ( ' ' )
174 || 'We could not identify migratable data for this plugin. If it holds data you need, tell us.' ,
175 manualSteps: capRows. find (( row ) => row.status === 'manual-mapping' )?.manualSteps || null ,
176 recognized: false ,
177 inventoryBasis: 'verified' ,
178 statusBasis: capRows. length > 0
179 ? Array. from ( new Set (capRows. map (( row ) => row.basis))). sort (). join ( '+' )
180 : 'unresolved' ,
181 confidence: null ,
182 signals: [],
183 capabilities: [],
184 channels: [],
185 channelStatuses: [],
186 targetRefs: Array. from ( new Set (capRows. flatMap (( row ) => row.targetRefs || []))). sort (),
187 profileVersion: null ,
188 blocked: capRows. flatMap (( row ) => row.blocked || []),
189 blockers: [],
190 });
191 }
192
193 // 3. Fingerprinted — public evidence only, typically an unauthenticated run. Named
194 // from the alias map, projected from the coverage rows classifyCoverage already produced.
195 for ( const print of detection?.fingerprinted || []) {
196 // The fingerprinted-capability lookup and the plugin-slug lookup can surface the same
197 // coverage row twice (a row attributed to this plugin also keyed under its own
198 // fingerprint token) — dedupe by capability id or userImpact/blocked entries double up.
199 const capRowsBySlug = new Map (
200 [
201 ... (coverageByCapability. has ( `fingerprinted:${ print . token }` ) ? [coverageByCapability. get ( `fingerprinted:${ print . token }` )] : []),
202 ... (coverageByPlugin. get (print.slug) || []),
203 ]. map (( row ) => [row.capability, row]),
204 );
205 const capRows = Array. from (capRowsBySlug. values ());
206 if (capRows. length === 0 ) continue ;
207 const status = worstStatus (capRows. map (( row ) => row.status)) || 'pending' ;
208 const noNeedRow = capRows. find (( row ) => row.status === 'no-need-to-migrate' );
209 rows. push ({
210 plugin: print.slug,
211 displayName: print.displayName || print.token,
212 version: null ,
213 active: null ,
214 does: '' ,
215 status,
216 via: null ,
217 mappingConfidence: null ,
218 consequence: noNeedRow?.rationale
219 || capRows. map (( row ) => row.userImpact). filter (Boolean). join ( ' ' ),
220 manualSteps: capRows. find (( row ) => row.status === 'manual-mapping' )?.manualSteps || null ,
221 recognized: false ,
222 inventoryBasis: 'fingerprinted' ,
223 statusBasis: Array. from ( new Set (capRows. map (( row ) => row.basis))). sort (). join ( '+' ),
224 confidence: null ,
225 signals: print.evidence,
226 capabilities: [],
227 channels: [],
228 channelStatuses: [],
229 targetRefs: [],
230 profileVersion: null ,
231 blocked: [],
232 blockers: [],
233 });
234 }
235
236 for ( const row of rows) {
237 row.prerequisite = row.status === 'migration-planned' && (row.via || '' ). includes ( 'cms' )
238 ? CMS_PREREQUISITE
239 : null ;
240 }
241
242 return rows. sort (( a , b ) => {
243 const da = STATUS_META [a.status]?.order ?? 99 ;
244 const db = STATUS_META [b.status]?.order ?? 99 ;
245 return da - db || a.displayName. localeCompare (b.displayName);
246 });
247 }
248
249 // A CMS destination is not free: Wix Data must be installed first (otherwise item writes fail
250 // with WDE0110), and the collection itself has no verified writer yet, so creating it is manual
251 // setup. A row that says data comes across without saying that overstates how automatic it is.
252 const CMS_PREREQUISITE = 'Requires the Wix Data app installed, and the collection created as a setup step — collection creation is not automated yet.' ;
253
254 function describeFromCapabilities ( capabilities ) {
255 if ( ! capabilities || capabilities. length === 0 ) return '' ;
256 return `Provides ${ capabilities . join ( ', ' ) }.` ;
257 }
258
259 function summarizeDispositions ( rows ) {
260 const byStatus = {};
261 for ( const row of rows) byStatus[row.status] = (byStatus[row.status] || 0 ) + 1 ;
262 return {
263 plugins: rows. length ,
264 active: rows. filter (( row ) => row.active). length ,
265 byStatus,
266 needsMigrationWork: rows. filter (( row ) => [ 'migration-planned' , 'pending' , 'requires-development' ]. includes (row.status)). length ,
267 // manual-mapping is neither: no pipeline/codegen work is owed (unlike needsMigrationWork
268 // above), but the merchant does owe some action (unlike noWorkNeeded below).
269 needsMerchantAction: rows. filter (( row ) => row.status === 'manual-mapping' ). length ,
270 noWorkNeeded: rows. filter (( row ) => row.status === 'no-need-to-migrate' ). length ,
271 unresolved: rows. filter (( row ) => row.statusBasis === 'unresolved' || row.status === 'pending' ). length ,
272 // The batched blocked-but-recoverable ask: rows that need something from the user.
273 blocked: rows. filter (( row ) => (row.blocked || []). length > 0 ). length ,
274 // Rows that cannot land without a setup step first; the execution plan needs the count.
275 needsWixData: rows. filter (( row ) => row.prerequisite). length ,
276 };
277 }
278
279 function escapeCell ( value ) {
280 return String (value === null || value === undefined ? '' : value). replace ( / \| / g , ' \\ |' ). replace ( / \n + / g , ' ' );
281 }
282
283 function renderDispositionMarkdown ( rows , { host = null , generatedAt = null , pluginListAvailable = true } = {}) {
284 const summary = summarizeDispositions (rows);
285 const lines = [];
286
287 lines. push ( '# Plugin migration map' );
288 lines. push ( '' );
289 lines. push ( `One row per installed plugin, and where each one lands in Wix.${ host ? ` Source: \` ${ host } \` .` : ''}` );
290 lines. push ( '' );
291 lines. push ( `- Plugins installed: **${ summary . plugins }** (${ summary . active } active)` );
292 lines. push ( `- Need migration work: **${ summary . needsMigrationWork }**` );
293 lines. push ( `- Need no migration at all: **${ summary . noWorkNeeded }**` );
294 if (summary.needsMerchantAction > 0 ) {
295 lines. push ( `- Steps you can do right now: **${ summary . needsMerchantAction }**` );
296 }
297 if (summary.blocked > 0 ) {
298 lines. push ( `- Blocked on something only you can provide: **${ summary . blocked }**` );
299 }
300 if (generatedAt) lines. push ( `- Generated: \` ${ generatedAt } \` ` );
301 lines. push ( '' );
302
303 if ( ! pluginListAvailable) {
304 lines. push ( '> **The installed plugin list was unavailable** (`GET /wp/v2/plugins` needs an administrator credential).' );
305 lines. push ( '> Only plugins detectable from public signals appear below, so this list is incomplete.' );
306 lines. push ( '' );
307 }
308
309 const blockedRows = rows. filter (( row ) => (row.blocked || []). length > 0 );
310 if (blockedRows. length > 0 ) {
311 lines. push ( '## Needs something from you — asked once, each item skippable' );
312 lines. push ( '' );
313 lines. push ( 'Fixing any of these and re-running includes the data; skipping one never changes the mapping decision.' );
314 lines. push ( '' );
315 for ( const row of blockedRows) {
316 for ( const blocker of row.blocked) {
317 lines. push ( `- **${ row . displayName }** — ${ blocker . resolution }${ blocker . declined ? ' *(declined)*' : ''}` );
318 }
319 }
320 lines. push ( '' );
321 }
322
323 const blockers = rows. filter (( row ) => row.blockers. length > 0 );
324 if (blockers. length > 0 ) {
325 lines. push ( '## Decide these before committing to a date' );
326 lines. push ( '' );
327 for ( const row of blockers) {
328 for ( const blocker of row.blockers) lines. push ( `- **${ row . displayName }** — ${ blocker }` );
329 }
330 lines. push ( '' );
331 }
332
333 const groups = Object. entries ( STATUS_META )
334 . sort (([, a ], [, b ]) => a.order - b.order)
335 . map (([ key , meta ]) => [key, meta, rows. filter (( row ) => row.status === key)])
336 . filter (([, , group ]) => group. length > 0 );
337
338 for ( const [ key , meta , group ] of groups) {
339 lines. push ( `## ${ meta . label } — ${ group . length }` );
340 lines. push ( '' );
341 lines. push ( `${ meta . blurb }` );
342 lines. push ( '' );
343 if (key === 'manual-mapping' ) {
344 // Unlike every other group, the point here is the runbook itself, not a one-line
345 // consequence — so render each plugin's full manualSteps.steps[] inline instead of collapsing it into a table cell.
346 for ( const row of group) {
347 lines. push ( `### ${ row . displayName } ( \` ${ row . plugin } \` )` );
348 lines. push ( '' );
349 if (row.does) lines. push ( `${ row . does }` );
350 const steps = row.manualSteps;
351 if (steps?.prerequisite) lines. push ( `**Before you start:** ${ steps . prerequisite }` );
352 if (steps?.steps?. length ) {
353 lines. push ( '' );
354 steps.steps. forEach (( step , index ) => {
355 const outsideWix = step.actor === 'external' ? ' *(outside Wix)*' : '' ;
356 lines. push ( `${ index + 1 }. ${ step . text }${ outsideWix }` );
357 });
358 }
359 if (steps?.mechanism) {
360 lines. push ( '' );
361 lines. push ( `_How it behaves once connected:_ ${ steps . mechanism }` );
362 }
363 lines. push ( '' );
364 }
365 } else {
366 lines. push ( '| Plugin | What it does | What that means for this store | Status |' );
367 lines. push ( '| --- | --- | --- | --- |' );
368 for ( const row of group) {
369 const consequence = row.prerequisite
370 ? `${ row . consequence } ${ row . prerequisite }` . trim ()
371 : row.consequence;
372 lines. push ( `| **${ escapeCell ( row . displayName ) }**<br> \` ${ escapeCell ( row . plugin ) } \` | ${ escapeCell ( row . does ) } | ${ escapeCell ( consequence ) } | ${ row . active ? 'active' : 'inactive'} · v${ escapeCell ( row . version ) } |` );
373 }
374 lines. push ( '' );
375 }
376 }
377
378 lines. push ( '## Basis for each row (for debugging this run)' );
379 lines. push ( '' );
380 lines. push ( '`verified` inventory facts come from the site API. A `proposed` mapping is our **assessment** until the mapping review confirms it — never present a proposed row to a customer as fact.' );
381 lines. push ( '' );
382 lines. push ( '| Plugin | Status | Via | Mapping | Basis | Confidence | Channels | Targets | Profile |' );
383 lines. push ( '| --- | --- | --- | --- | --- | --- | --- | --- | --- |' );
384 for ( const row of rows) {
385 lines. push ( `| \` ${ escapeCell ( row . plugin ) } \` | ${ escapeCell ( row . status ) } | ${ escapeCell ( row . via || '-' ) } | ${ escapeCell ( row . mappingConfidence || '-' ) } | inv:${ row . inventoryBasis } / status:${ row . statusBasis } | ${ escapeCell ( row . confidence || '-' ) } | ${ escapeCell ( row . channels . join ( ', ' ) || '-' ) } | ${ escapeCell ( row . targetRefs . join ( ', ' ) || '-' ) } | ${ escapeCell ( row . profileVersion || '-' ) } |` );
386 }
387 lines. push ( '' );
388
389 return `${ lines . join ( ' \n ' ) } \n ` ;
390 }
391
392 module . exports = {
393 STATUS_META,
394 STATUS_PRECEDENCE,
395 worstStatus,
396 buildDispositionRows,
397 summarizeDispositions,
398 renderDispositionMarkdown,
399 hintIndex,
400 };