Setting the file. One moment.
Mapping Omission Approval · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page — line 66
This file
Number 45.99
Position 99 of 182
Type JavaScript
Size 8 KB
Lines 90 lib/ mapping-omission-approval.js
JavaScript · 90 lines · 8 KB
GUID
}
=
require
(
'./operator-identity'
);
7 const read = ( dir , file ) => { try { return JSON . parse (fs. readFileSync (path. join (dir,file), 'utf8' ));} catch { throw new Error ( 'omission approval artifact unreadable' );}};
8 const mode = decisions => require ( './orchestration-decisions' ). getDecisionValue (decisions, 'automationMode' ) === 'one_click' ? 'one_click' : 'interactive' ;
9 function omissions ( row ) {
10 const excluded =new Set ((row.fieldMappings || []). filter ( d => d.disposition === 'excluded' && fields. pendingDecision (row,d)). map ( d => d.id));
11 if ( ! excluded.size) return [];
12 return fields. projectEntity (row).fieldDispositions. filter ( d => d.status === 'notMigrated' && d.decisionRefs?. some ( id => excluded. has (id))). map ( d => ({
13 kind:d.kind,path:d.path,writeOperation:d.writeOperation || null ,decisionRefs:[ ... d.decisionRefs]. sort (),
14 reason:d.reason,affectedScope:d.affectedScope
15 })). sort (( a , b ) => fields. canonical (a). localeCompare (fields. canonical (b)));
16 }
17 function candidates ( plan ) {
18 return fields. entityRows (plan). filter ( row => row.omissionReviewRequested && plan.executionScope?.resolvedEntityRefs?. includes (fields. entityRef (row)) && omissions (row). length );
19 }
20 // One digest per entity plus one per canonical (selector-qualified) location, so an approval
21 // names exactly which location/scope it covers and a change to any one location is detected.
22 function dispositionDigests ( plan ) {
23 return candidates (plan). map ( row => {
24 const rows = omissions (row);
25 return {entity:fields. entityRef (row),digest:fields. digest (rows),
26 locations:rows. map ( o => ({kind:o.kind,path:o.path,writeOperation:o.writeOperation,digest:fields. digest (o)}))};
27 }). sort (( a , b ) => a.entity. localeCompare (b.entity));
28 }
29 function changedLocations ( approved , expected ) {
30 const key = l => fields. canonical ([l.kind,l.path,l.writeOperation]);
31 const before =new Map ((approved || []). flatMap ( e => (e.locations || []). map ( l => [ `${ e . entity }|${ key ( l ) }` ,l.digest])));
32 const after =new Map ((expected || []). flatMap ( e => (e.locations || []). map ( l => [ `${ e . entity }|${ key ( l ) }` ,l.digest])));
33 return [ ...new Set ([ ... before. keys (), ... after. keys ()])]. filter ( k => before. get (k) !== after. get (k)). sort ();
34 }
35 function bindings ( dir , row ) {
36 const entity = row.sourceEntity || row.entity;
37 const C = require ( './read-back-contract' );
38 const paths = Object. hasOwn ( C . DOMAINS ,entity) ? C . bindingPaths (entity) :
39 [ 'mapping/mapping-plan.json' , `mapping/entity-decisions/${ fields . entityFileName ( row ) }` , 'mapping/mapping-resolution.json' ];
40 return Object. fromEntries (paths. map ( file => { const hash = hashArtifact (dir,file); if ( ! hash) throw new Error ( `omission approval binding missing: ${ file }` ); return [file,hash];}));
41 }
42 function requestMatches ( request , current ) {
43 return Object. entries (current). every (([ file , hash ]) => request.artifactRefs?. some ( ref => ref?.path === file && String (ref.sha256). replace ( / ^ sha256:/ , '' ) === hash));
44 }
45 function context ( dir ) {
46 const siteId = require ( './config-env' ). parseEnvText (fs. readFileSync (path. join (dir, 'config/wix.env' ), 'utf8' )). WIX_SITE_ID ;
47 const projectId = read (dir, 'orchestration/run.json' ).projectId;
48 if ( ! siteId || ! projectId) throw new Error ( 'omission approval requires project and destination identity' );
49 return {automationMode: 'interactive' ,projectId,siteId};
50 }
51 function validateEvidence ( dir , row ) {
52 const C = require ( './read-back-contract' ), entity = row.sourceEntity || row.entity;
53 const inv = Object. hasOwn ( C . DOMAINS ,entity) ? read (dir, `execution/read-back-inventory/${ entity }.json` ) : null ;
54 if (inv && (inv.schemaVersion !== C . INVENTORY_SCHEMA_VERSION || inv.locationEncoding !== require ( './source-location' ). ENCODING )) throw new Error ( 'omission inventory stale (location encoding); rerun inventory' );
55 const inventoryIds = inv ? new Set (inv.records. map ( r => r.sourceId)) : null ;
56 for ( const omission of omissions (row)) {
57 const scope = omission.affectedScope;
58 if ( ! scope.sourceSnapshotDigest || ! scope.inventoryDigest || scope.sourceSnapshotDigest !== row.discoveryCoverage?.sourceSnapshotDigest || scope.inventoryDigest !== row.discoveryCoverage?.inventoryDigest) throw new Error ( 'omission source evidence changed' );
59 if (scope.sourceIdsComplete && ( ! Array. isArray (scope.sourceIds) || fields. digest (scope.sourceIds) !== scope.sourceIdsDigest)) throw new Error ( 'omission complete source-ID digest changed' );
60 if (inv && scope.countStatus === 'known' && omission.kind === 'source' && scope.count !== (inv.sourcePaths[omission.path] || 0 )) throw new Error ( 'omission count differs from populated inventory' );
61 if (inv && scope.sourceIds?. some ( id =>! inventoryIds. has (id))) throw new Error ( 'omission source IDs are outside inventory' );
62 // A complete per-location membership must be the inventory's measured record set there.
63 if (inv && scope.countStatus === 'known' && scope.sourceIdsComplete && omission.kind === 'source' && scope.sourceIdsDigest !== (inv.sourceIdsDigests?.[omission.path] || fields. digest ([]))) throw new Error ( `omission membership differs from populated inventory at ${ omission . path }` );
64 }
65 }
66 function assertAuthorized ( dir , plan , rows = candidates (plan)) {
67 if ( ! rows. length ) return ;
68 if ( ! dir) throw new Error ( 'omission authorization requires the original project context' );
69 const approval = read (dir, 'orchestration/approvals.json' ).mapping;
70 if ( ! / ^ uar_ [a-zA-Z0-9-] +$ / . test (approval?.requestId || '' )) throw new Error ( 'omission approval request missing' );
71 const request = approval?.requestId && read (dir, `orchestration/user-actions/${ approval . requestId }.json` );
72 if ( mode ( read (dir, 'orchestration/decisions.json' )) !== 'interactive' || approval?.omissionApprovalContext?.automationMode !== 'interactive' ||
73 request?.omissionApprovalContext?.automationMode !== 'interactive' || approval?.status !== 'approved' || approval.decidedBy !== 'user' ||
74 ! GUID . test (approval.approvedBy?.userId || '' ) || ! approval.approvedBy?.displayName?. trim () || request?.status !== 'answered' || request.requestId !== approval.requestId || request.kind !== 'approval' || request.response?.mode !== 'choice' ||
75 request.effect?.kind !== 'approval' || request.effect.approvalType !== 'mapping' || ! [ 'approved' , 'approve' ]. includes (request.answer?.choice)) throw new Error ( 'identified interactive omission approval required' );
76 if (fields. digest (plan) !== fields. digest ( read (dir, 'mapping/mapping-plan.json' ))) throw new Error ( 'omission authority changed' );
77 if (fields. canonical (approval.omissionApprovalContext) !== fields. canonical ( context (dir)) || fields. canonical (request.omissionApprovalContext) !== fields. canonical ( context (dir))) throw new Error ( 'omission project or destination changed' );
78 const expected = dispositionDigests (plan);
79 if (fields. canonical (approval.fieldDispositionDigests) !== fields. canonical (expected) || fields. canonical (request.fieldDispositionDigests) !== fields. canonical (expected)) {
80 const changed = changedLocations (approval.fieldDispositionDigests,expected);
81 throw new Error ( `omission scope digest changed${ changed . length ? `: ${ changed . slice ( 0 , 5 ). join ( ', ' ) }${ changed . length > 5 ? ' …' : ''}` : ''}` );
82 }
83 if ( ! fields. compareMappingProjections (dir,plan).ok) throw new Error ( 'omission mapping projections changed' );
84 for ( const row of rows) {
85 validateEvidence (dir,row);
86 const current = bindings (dir,row);
87 if ( ! requestMatches (request,current) || Object. entries (current). some (([ file , hash ]) => approval.bindingDigests?.[file] !== hash)) throw new Error ( 'omission artifact binding changed' );
88 }
89 }
90 module . exports = {changedLocations,validateEvidence,context,mode,omissions,candidates,dispositionDigests,bindings,requestMatches,assertAuthorized};