Setting the file. One moment.
Catalog Baseline · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page function classify
— line 111
This file
Number 45.53
Position 53 of 182
Type JavaScript
Size 24 KB
Lines 378 lib/ catalog-baseline.js
JavaScript · 378 lines · 24 KB
require
(
'node:fs'
);
14 const path = require ( 'node:path' );
15 const crypto = require ( 'node:crypto' );
16 const wixAuth = require ( './wix-auth.js' );
17
18 const BASELINE_PATH = 'setup/destination-catalog-baseline.json' ;
19 const COLLISIONS_PATH = 'execution/target-baseline-collisions.json' ;
20 const STORES_APP_DEF_ID = '215238eb-22a5-4c36-9e7b-e7c08025e04e' ;
21 const COLLISION_CODE = 'TARGET_BASELINE_COLLISION' ;
22 const NOT_DELETED_CODE = 'STORES_INSTALL_SEEDED_NOT_DELETED' ;
23 const PRE_EXISTING_CODE = 'STORES_PRE_EXISTING_CATALOG' ;
24 const ENTITY = { products: 'stores/product' , categories: 'stores/category' };
25
26 // Wix may add the samples a moment after the install call returns: read until two consecutive
27 // reads hold the same ids, at least SETTLE_MIN_READS times.
28 const SETTLE_MIN_READS = 3 ;
29 const SETTLE_MAX_READS = 10 ;
30 const SETTLE_DELAY_MS = 3000 ;
31
32 const pause = ( ms ) => new Promise (( resolve ) => setTimeout (resolve, ms));
33 const ids = ( items ) => items. map (( item ) => item.id). sort ();
34 const sameIds = ( a , b ) => JSON . stringify ( ids (a.products)) === JSON . stringify ( ids (b.products)) && JSON . stringify ( ids (a.categories)) === JSON . stringify ( ids (b.categories));
35
36 async function readCatalog ( wix , writers ) {
37 const products = await writers. queryAllStoresProducts (wix);
38 const categories = await writers. queryAllStoresCategories (wix);
39 const slim = ( item ) => ({ id: item.id, slug: item.slug ?? null });
40 return { products: products. filter (( item ) => item && item.id). map (slim), categories: categories. filter (( item ) => item && item.id). map (slim) };
41 }
42
43 async function settledCatalog ( wix , writers , { sleep = pause, delayMs = SETTLE_DELAY_MS } = {}) {
44 let previous = null ;
45 for ( let read = 1 ; read <= SETTLE_MAX_READS ; read += 1 ) {
46 if (read > 1 ) await sleep (delayMs);
47 const current = await readCatalog (wix, writers);
48 if (previous && read >= SETTLE_MIN_READS && sameIds (previous, current)) return { catalog: current, reads: read, settled: true };
49 previous = current;
50 }
51 return { catalog: previous, reads: SETTLE_MAX_READS , settled: false };
52 }
53
54 function rows ( items , kind , reason = null ) {
55 return items. map (( item ) => ({ id: item.id, slug: item.slug, kind, status: 'present' , ... (reason ? { reason } : {}) }));
56 }
57
58 // Deletes exactly the seeded rows, products first (a category delete leaves its products), then
59 // reads the catalog again: an id still listed is `delete_failed`, whatever the delete call said.
60 // The built-in All Products category appears with the store, so the diff calls it seeded; it is
61 // Wix's own category, never a sample.
62 async function markBuiltInCategory ( baseline , wix , writers ) {
63 let allProductsId = null ;
64 try { allProductsId = await writers. getAllProductsCategoryId (wix); } catch (error) { if (wixAuth. isRunInterruption (error)) throw error; /* fall back to the canonical slug below */ }
65 for ( const row of baseline.categories) {
66 if (row.kind === 'install_seeded' && (row.id === allProductsId || row.slug === 'all-products' )) Object. assign (row, { kind: 'pre_existing' , reason: 'built_in_all_products_category' });
67 }
68 }
69
70 async function clearSeeded ( baseline , wix , writers ) {
71 for ( const [ collection , build ] of [[ 'products' , writers.buildDeleteInstallSeededProductRequest], [ 'categories' , writers.buildDeleteInstallSeededCategoryRequest]]) {
72 for ( const row of baseline[collection]. filter (( item ) => item.kind === 'install_seeded' && item.status !== 'deleted' )) {
73 delete row.deleteError;
74 try { await wix. send ( build (row.id)); } catch (error) { if (wixAuth. isRunInterruption (error)) throw error; row.deleteError = String (error.message || error). slice ( 0 , 200 ); }
75 }
76 }
77 const after = await readCatalog (wix, writers);
78 const appeared = { products: [], categories: [] };
79 for ( const collection of [ 'products' , 'categories' ]) {
80 const present = new Set (after[collection]. map (( item ) => item.id));
81 for ( const row of baseline[collection]. filter (( item ) => item.kind === 'install_seeded' )) row.status = present. has (row.id) ? 'delete_failed' : 'deleted' ;
82 const known = new Set (baseline[collection]. map (( row ) => row.id));
83 appeared[collection] = after[collection]. filter (( item ) => ! known. has (item.id));
84 }
85 baseline.verifiedAt = new Date (). toISOString ();
86 return appeared;
87 }
88
89 // Each collection is read on its own, so a failed category read never discards a product read
90 // that succeeded. A collection whose read failed is `null`: unknown, never empty.
91 async function readBefore ( wix , writers ) {
92 const before = { products: null , categories: null };
93 for ( const [ collection , read ] of [[ 'products' , writers.queryAllStoresProducts], [ 'categories' , writers.queryAllStoresCategories]]) {
94 try { before[collection] = ( await read (wix)). filter (( item ) => item && item.id). map (( item ) => ({ id: item.id, slug: item.slug ?? null })); } catch (error) { if (wixAuth. isRunInterruption (error)) throw error; /* unknown */ }
95 }
96 return before;
97 }
98
99 function notSettled ( baseline ) {
100 return Object. assign ( new Error ( 'The Stores catalog kept changing after the install, so setup cannot tell which items the install added' ), {
101 blockerCode: 'CATALOG_NOT_SETTLED' ,
102 recommendedAction: 'Re-run setup in a few minutes. It reads the catalog again and finishes the record; import cannot start until it does.' ,
103 baseline,
104 });
105 }
106
107 // Classify a settled post-install catalog against the pre-install ids. `preInstall` is already
108 // resolved (see captureAroundInstall): a collection that is `null` there is unknown, so every item
109 // of it is `pre_existing`. Known rows -- the pre-install items and anything an earlier attempt
110 // recorded -- keep their record, listed or not.
111 function classify ( catalog , preInstall , known = { products: [], categories: [] }) {
112 const out = {};
113 for ( const collection of [ 'products' , 'categories' ]) {
114 const before = preInstall[collection];
115 const kept = new Map (known[collection]. map (( row ) => [row.id, row]));
116 out[collection] = catalog[collection]. map (( item ) => {
117 const row = kept. get (item.id);
118 if (row) return row.status === 'deleted' ? { ... row, status: 'present' } : row;
119 if (before === null ) return { id: item.id, slug: item.slug, kind: 'pre_existing' , status: 'present' , reason: 'pre_install_catalog_unreadable' };
120 return { id: item.id, slug: item.slug, kind: before. some (( old ) => old.id === item.id) ? 'pre_existing' : 'install_seeded' , status: 'present' };
121 });
122 for ( const row of known[collection]) if ( ! out[collection]. some (( item ) => item.id === row.id)) out[collection]. push (row);
123 }
124 return out;
125 }
126
127 // Deletes happen only on a site this run created, on the first install of Stores there. On any
128 // other site, or when Stores is installed again after an earlier setup, Stores may bring back an
129 // earlier catalog -- the owner's or this run's own import -- and that looks exactly like samples.
130 const mayDelete = ( baseline ) => baseline.siteOrigin === 'created' && ! baseline.reinstall;
131
132 // Deletes, when allowed, then persists. The record is written BEFORE any delete call, so a crash
133 // or a failed verification read never loses which items the install added.
134 // `detectNew` only while the install is still being recorded (import cannot have run yet): on a later
135 // re-run, new ids are this run's own import and must never be read as samples.
136 async function finish ( projectDir , baseline , wix , writers , { detectNew = true } = {}) {
137 if (baseline.categories. some (( row ) => row.kind === 'install_seeded' )) await markBuiltInCategory (baseline, wix, writers);
138 const seeded = baseline.products. concat (baseline.categories). filter (( row ) => row.kind === 'install_seeded' && row.status !== 'deleted' );
139 if ( ! mayDelete (baseline)) for ( const row of seeded) row.reason = baseline.reinstall ? 'reinstall_not_deleted' : 'existing_site_not_deleted' ;
140 writeBaseline (projectDir, baseline);
141 if ( ! seeded. length || ! mayDelete (baseline)) return ;
142 const appeared = await clearSeeded (baseline, wix, writers);
143 let changed = false ;
144 if (detectNew) {
145 // Something new showed up while deleting: record it as seeded and stop; the re-run finishes.
146 // A collection whose pre-install read is unknown never yields seeded items.
147 for ( const collection of [ 'products' , 'categories' ]) for ( const item of appeared[collection]) {
148 const unknown = baseline.preInstall?.[collection] == null ;
149 baseline[collection]. push ({ id: item.id, slug: item.slug, kind: unknown ? 'pre_existing' : 'install_seeded' , status: 'present' , reason: unknown ? 'pre_install_catalog_unreadable' : 'appeared_during_cleanup' });
150 if ( ! unknown) changed = true ;
151 }
152 }
153 if (changed) {
154 baseline.settled = false ;
155 writeBaseline (projectDir, baseline);
156 throw notSettled (baseline);
157 }
158 writeBaseline (projectDir, baseline);
159 }
160
161 // The Stores install step. Returns the baseline it wrote. `installed` says whether Stores was
162 // listed before this step; `install` performs and verifies the install.
163 async function captureAroundInstall ({ projectDir , wix , writers , installed , install , siteOrigin , sleep , delayMs }) {
164 const prior = loadBaseline (projectDir);
165 if (installed && prior) {
166 // A re-run of setup keeps the first run's record: anything added since may be this run's own
167 // import. An unsettled record (the install ran but was not fully recorded) is read again and
168 // finished; seeded items an earlier delete missed are tried again.
169 if (prior.settled === false ) {
170 const { catalog , reads , settled } = await settledCatalog (wix, writers, { sleep, delayMs });
171 const next = { ... prior, phase: 'installed' , ... classify (catalog, prior.preInstall || { products: null , categories: null }, prior), postInstallReads: reads, settled };
172 if ( ! settled) { writeBaseline (projectDir, next); throw notSettled (next); }
173 await finish (projectDir, next, wix, writers);
174 return { baseline: next, unchanged: false };
175 }
176 if ( ! mayDelete (prior) || ! prior.products. concat (prior.categories). some (( row ) => row.kind === 'install_seeded' && row.status !== 'deleted' )) return { baseline: prior, unchanged: true };
177 await finish (projectDir, prior, wix, writers, { detectNew: false });
178 return { baseline: prior, unchanged: false };
179 }
180 const header = { schemaVersion: 1 , appDefId: STORES_APP_DEF_ID , siteOrigin, storesInstalledBeforeRun: installed, startedAt: new Date (). toISOString () };
181 if (installed) {
182 const catalog = await readCatalog (wix, writers);
183 const baseline = { ... header, products: rows (catalog.products, 'pre_existing' , 'stores_installed_before_run' ), categories: rows (catalog.categories, 'pre_existing' , 'stores_installed_before_run' ), settled: true };
184 writeBaseline (projectDir, baseline);
185 return { baseline, unchanged: false };
186 }
187 // A prior record whose install completed means Stores was removed and is being installed again.
188 const reinstall = Boolean (prior && (prior.reinstall || prior.phase !== 'pre_install' ));
189 const read = await readBefore (wix, writers);
190 // An unreadable collection counts as empty only on the first setup of a site this run created
191 // (no store there yet, so nothing to lose); otherwise it stays unknown. A retry of an install that
192 // never completed reuses the first attempt's pre-install read.
193 const preInstall = {};
194 for ( const collection of [ 'products' , 'categories' ]) {
195 const retried = reinstall ? undefined : prior?.preInstall?.[collection];
196 preInstall[collection] = read[collection] ?? retried ?? (siteOrigin === 'created' && ! prior ? [] : null );
197 }
198 const known = { products: [ ... (prior?.products || [])], categories: [ ... (prior?.categories || [])] };
199 for ( const collection of [ 'products' , 'categories' ]) {
200 for ( const row of rows (preInstall[collection] || [], 'pre_existing' , 'pre_install' )) if ( ! known[collection]. some (( old ) => old.id === row.id)) known[collection]. push (row);
201 }
202 // Recorded before the install call: if a read after it fails, the retry still knows the
203 // pre-install ids and classifies against them instead of calling the samples pre-existing.
204 const baseline = { ... header, ... (reinstall ? { reinstall: true } : {}), preInstall, ... known, phase: 'pre_install' , settled: false };
205 writeBaseline (projectDir, baseline);
206 await install ();
207 baseline.phase = 'installed' ;
208 writeBaseline (projectDir, baseline);
209 const { catalog , reads , settled } = await settledCatalog (wix, writers, { sleep, delayMs });
210 Object. assign (baseline, classify (catalog, preInstall, known), { postInstallReads: reads, settled });
211 // Still changing after every read: record what is known and stop before deleting anything.
212 if ( ! settled) { writeBaseline (projectDir, baseline); throw notSettled (baseline); }
213 await finish (projectDir, baseline, wix, writers);
214 return { baseline, unchanged: false };
215 }
216
217 function writeBaseline ( projectDir , baseline ) {
218 const file = path. join (projectDir, BASELINE_PATH );
219 fs. mkdirSync (path. dirname (file), { recursive: true });
220 const tmp = `${ file }.${ crypto . randomUUID () }.tmp` ;
221 fs. writeFileSync (tmp, `${ JSON . stringify ( baseline , null , 2 ) } \n ` );
222 fs. renameSync (tmp, file);
223 }
224
225 // Missing means "no baseline recorded" (a run that never installed Stores through setup). A file
226 // that exists but cannot be read fails: treating it as absent would unprotect every item in it.
227 function loadBaseline ( projectDir ) {
228 let bytes;
229 try { bytes = fs. readFileSync (path. join (projectDir, BASELINE_PATH ), 'utf8' ); } catch (error) { if (error.code === 'ENOENT' ) return null ; throw error; }
230 const baseline = JSON . parse (bytes);
231 if (baseline?.schemaVersion !== 1 || ! Array. isArray (baseline.products) || ! Array. isArray (baseline.categories)) throw new Error ( `${ BASELINE_PATH } is not a valid catalog baseline` );
232 return baseline;
233 }
234
235 function digest ( projectDir ) {
236 try { return crypto. createHash ( 'sha256' ). update (fs. readFileSync (path. join (projectDir, BASELINE_PATH ))). digest ( 'hex' ); } catch (error) { if (error.code === 'ENOENT' ) return null ; throw error; }
237 }
238
239 const stillPresent = ( row ) => row.status !== 'deleted' ;
240
241 // Ids no importer may take over: every baseline item still on the site.
242 function protectedIds ( projectDir ) {
243 const baseline = loadBaseline (projectDir);
244 const pick = ( collection ) => new Set ((baseline?.[collection] || []). filter (stillPresent). map (( row ) => row.id));
245 return { products: pick ( 'products' ), categories: pick ( 'categories' ) };
246 }
247
248 // Seeded ids only: the "is the site in use" probe ignores these, so a seeded item a delete missed
249 // never reads as merchant data.
250 function installSeededIds ( projectDir ) {
251 const baseline = loadBaseline (projectDir);
252 return new Set ((baseline?.products || []). filter (( row ) => row.kind === 'install_seeded' ). map (( row ) => row.id));
253 }
254
255 function summarize ( baseline ) {
256 if ( ! baseline) return null ;
257 const count = ( collection , test ) => baseline[collection]. filter (test). length ;
258 const block = ( collection ) => ({
259 installSeeded: count (collection, ( row ) => row.kind === 'install_seeded' ),
260 deleted: count (collection, ( row ) => row.kind === 'install_seeded' && row.status === 'deleted' ),
261 deleteFailed: count (collection, ( row ) => row.kind === 'install_seeded' && row.status === 'delete_failed' ),
262 preExisting: count (collection, ( row ) => row.kind === 'pre_existing' ),
263 protected: count (collection, stillPresent),
264 });
265 return { products: block ( 'products' ), categories: block ( 'categories' ) };
266 }
267
268 // Split a catalog read into the items an importer may match and the protected ones.
269 function partition ( items , protectedSet ) {
270 const open = [], held = [];
271 for ( const item of items || []) (item && protectedSet. has (item.id) ? held : open). push (item);
272 return { open, held };
273 }
274
275 function collisionError ( entity , targetId ) {
276 return Object. assign ( new Error ( `${ COLLISION_CODE }: ${ entity } ${ targetId } was on the destination before this import; it is neither taken over nor duplicated` ), { code: COLLISION_CODE , targetId });
277 }
278
279 // Durable record of every held collision, read by the completion report. One row per entity and
280 // exact source key; a re-run updates it in place. Each writer clears only the keys it records
281 // (the read-back gate uses its own `read-back:<entity>:<id>` keys).
282 const sameRecord = ( row , entity , sourceKey ) => row.entity === entity && row.sourceKey === String (sourceKey);
283
284 function recordCollision ( projectDir , { entity , sourceKey , targetId , slug = null }) {
285 const file = path. join (projectDir, COLLISIONS_PATH );
286 let doc = { schemaVersion: 1 , collisions: [] };
287 try { doc = JSON . parse (fs. readFileSync (file, 'utf8' )); } catch (error) { if (error.code !== 'ENOENT' ) throw error; }
288 doc.collisions = (doc.collisions || []). filter (( row ) => ! sameRecord (row, entity, sourceKey));
289 doc.collisions. push ({ entity, sourceKey: String (sourceKey), targetId, slug, recordedAt: new Date (). toISOString () });
290 fs. mkdirSync (path. dirname (file), { recursive: true });
291 const tmp = `${ file }.${ crypto . randomUUID () }.tmp` ;
292 fs. writeFileSync (tmp, `${ JSON . stringify ( doc , null , 2 ) } \n ` );
293 fs. renameSync (tmp, file);
294 }
295
296 // A held record that later imports and verifies is no longer a collision.
297 function clearCollision ( projectDir , entity , sourceKey ) {
298 const file = path. join (projectDir, COLLISIONS_PATH );
299 let doc;
300 try { doc = JSON . parse (fs. readFileSync (file, 'utf8' )); } catch (error) { if (error.code === 'ENOENT' ) return ; throw error; }
301 const kept = (doc.collisions || []). filter (( row ) => ! sameRecord (row, entity, sourceKey));
302 if (kept. length === (doc.collisions || []). length ) return ;
303 doc.collisions = kept;
304 const tmp = `${ file }.${ crypto . randomUUID () }.tmp` ;
305 fs. writeFileSync (tmp, `${ JSON . stringify ( doc , null , 2 ) } \n ` );
306 fs. renameSync (tmp, file);
307 }
308
309 function loadCollisions ( projectDir ) {
310 try { return JSON . parse (fs. readFileSync (path. join (projectDir, COLLISIONS_PATH ), 'utf8' )).collisions || []; } catch (error) { if (error.code === 'ENOENT' ) return []; throw error; }
311 }
312
313 // Completion-report rows: the cutover items this feature owns, plus the `destinationBaseline` block.
314 function reportRows ( projectDir ) {
315 const baseline = loadBaseline (projectDir);
316 // Only collisions whose blocking item is still on the site are open; once setup deleted it, the
317 // record imports on the next run and the import's own accounting covers it.
318 const held = protectedIds (projectDir);
319 const collisions = loadCollisions (projectDir). filter (( row ) => (row.entity === ENTITY .categories ? held.categories : held.products). has (row.targetId));
320 const operatorActions = [];
321 if (baseline) {
322 const failed = [ ... baseline.products, ... baseline.categories]. filter (( row ) => row.kind === 'install_seeded' && row.status !== 'deleted' );
323 if (failed. length ) {
324 const why = failed. some (( row ) => row.status === 'delete_failed' ) ? 'that setup could not delete' : 'that setup left in place because this run did not create the site or Stores was installed again (check each is a sample, not an earlier product of the owner or of this import)' ;
325 operatorActions. push ({
326 actionId: 'catalog-baseline:seeded-not-deleted' , code: NOT_DELETED_CODE , severity: 'high' , role: 'cutover-operator' , entity: 'stores/product' ,
327 action: `Cutover Operator: in Business Manager > Store Products (and Categories), delete the ${ failed . length } item(s) that appeared when Wix Stores was installed and ${ why } (ids in affectedIds).` ,
328 deadlineReason: 'before switching traffic; customers would see sample products' , affectedCount: failed. length , affectedIds: failed. map (( row ) => row.id),
329 artifactRefs: [ BASELINE_PATH ], resolutionEvidence: 'A catalog read no longer lists these ids.' ,
330 });
331 }
332 const pre = baseline.products. filter (( row ) => row.kind === 'pre_existing' );
333 if (pre. length ) {
334 operatorActions. push ({
335 actionId: 'catalog-baseline:pre-existing' , code: PRE_EXISTING_CODE , severity: 'low' , role: 'site-owner' , entity: 'stores/product' ,
336 action: `Site owner: ${ pre . length } product(s) were on the store before this migration. RePlatform did not change or delete them; keep or remove them as you choose.` ,
337 deadlineReason: 'information only; review before go-live' , affectedCount: pre. length , affectedIds: pre. map (( row ) => row.id), artifactRefs: [ BASELINE_PATH ],
338 resolutionEvidence: 'The owner has reviewed the listed products.' ,
339 });
340 }
341 }
342 // Keyed by the blocking Wix item: the importer and the read-back gate name the same source record
343 // with different key formats, so counting source keys would count one record twice.
344 for ( const entity of [ ...new Set (collisions. map (( row ) => row.entity))]. sort ()) {
345 const targets = [ ...new Set (collisions. filter (( row ) => row.entity === entity). map (( row ) => row.targetId))];
346 operatorActions. push ({
347 actionId: `catalog-baseline:collision:${ entity }` , code: COLLISION_CODE , severity: 'high' , role: 'import-operator' , entity,
348 action: `Requires Import Operator: ${ targets . length } ${ entity } item(s) that were on the store before this import match source records, so those records were not written (target ids in affectedIds).` ,
349 importOperatorPrompt: `Resolve ${ COLLISION_CODE } for ${ entity }: for each row in ${ COLLISIONS_PATH }, remove or rename the listed Wix item (target id), then resume the import for that source record.` ,
350 deadlineReason: 'before switching traffic; these records are missing from the new store' , affectedCount: targets. length , affectedIds: targets,
351 artifactRefs: [ COLLISIONS_PATH , ... (baseline ? [ BASELINE_PATH ] : [])], resolutionEvidence: 'Each listed source record has a verified read-back.' ,
352 });
353 }
354 const summary = summarize (baseline);
355 return { operatorActions, destinationBaseline: summary ? { path: BASELINE_PATH , ... summary, collisions: new Set (collisions. map (( row ) => `${ row . entity } \u0000 ${ row . targetId }` )).size } : null };
356 }
357
358 // Quick-mode summaries are their own completion reports and never pass through
359 // completion-report.js, so the same rows are attached here: structured operator actions (merged by
360 // actionId) and the destinationBaseline block.
361 function quickReport ( projectDir , summary ) {
362 const { operatorActions , destinationBaseline } = reportRows (projectDir);
363 if (destinationBaseline) summary.destinationBaseline = destinationBaseline;
364 const others = (summary.operatorActions || []). filter (( row ) => ! String (row.actionId || '' ). startsWith ( 'catalog-baseline:' ));
365 if (operatorActions. length || summary.operatorActions) summary.operatorActions = [ ... others, ... operatorActions];
366 // Same rule as completion-report.js: an open high-severity catalog item is never a clean `complete`.
367 // A summary with no status of its own (WooCommerce) gets one: held records make it `partial`.
368 const high = operatorActions. some (( row ) => row.severity === 'high' );
369 if ( ! summary.status && operatorActions. some (( row ) => row.code === COLLISION_CODE )) summary.status = 'partial' ;
370 else if (( ! summary.status || summary.status === 'complete' ) && high) summary.status = 'complete_with_warnings' ;
371 return summary;
372 }
373
374 module . exports = {
375 BASELINE_PATH, COLLISIONS_PATH, COLLISION_CODE, NOT_DELETED_CODE, PRE_EXISTING_CODE, STORES_APP_DEF_ID, ENTITY,
376 readCatalog, settledCatalog, captureAroundInstall, loadBaseline, writeBaseline, digest, protectedIds, installSeededIds,
377 summarize, partition, collisionError, recordCollision, clearCollision, loadCollisions, reportRows, quickReport,
378 };