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