Setting the file. One moment.
Setup Verification · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page lib/ setup-verification.js
JavaScript · 180 lines · 11 KB
// setup re-runs, it is never upgraded in place.
14 const { SETUP_CHECK_METHODS , SETUP_CHECK_METHOD_SET } = require ( './setup-check-methods.js' );
15 // The target entities each named setup executor in execute-setup.js writes. A step with one of
16 // these ids must list them in `entityRefs`, so codegen binds the step as their producer; each is
17 // `setupProducible` in rp-target-wix, so the mapping preflight expects setup to make it too.
18 // A step that runs a generic registered writer (`intent.writerId`) declares its own refs instead.
19 const SETUP_EXECUTOR_ENTITY_REFS = Object. freeze ({ 'create-cms-collections' : Object. freeze ([ 'cms/collection' ]) });
20 const fs = require ( 'node:fs' );
21 const path = require ( 'node:path' );
22 let reviewedReferencePaths;
23 function knownReferencePath ( ref , targetPath , projectDir ) {
24 if ( ! reviewedReferencePaths) {
25 reviewedReferencePaths = new Map (); const root = path. join (__dirname, '../resources/rp-target-wix/domains' );
26 for ( const domain of fs. readdirSync (root, { withFileTypes: true }). filter ( entry => entry. isDirectory ())) {
27 const entities = path. join (root, domain.name, 'entities' ); if ( ! fs. existsSync (entities)) continue ;
28 for ( const file of fs. readdirSync (entities). filter ( file => file. endsWith ( '.json' ))) { const definition = JSON . parse (fs. readFileSync (path. join (entities, file))); for ( const [ dependencyRef , paths ] of Object. entries (definition.dependencyReferenceFields || {})) { if ( ! reviewedReferencePaths. has (dependencyRef)) reviewedReferencePaths. set (dependencyRef, new Set ()); for ( const value of paths) reviewedReferencePaths. get (dependencyRef). add (value); } }
29 }
30 }
31 if (reviewedReferencePaths. get (ref)?. has (targetPath) === true ) return true ;
32 const A = require ( './operation-assessments' );
33 const check = () => ( A . current ()?.assessments || []). some ( a => {
34 if ( ! a.contract?.dependencies?.[ref]) return false ;
35 const contract = require ( './dependency-proofs' ). operationContract (a.targetRef, a.operation)?.contract;
36 return (contract?.assessmentReferenceFields?.[ref] || []). includes (targetPath);
37 });
38 try { return projectDir ? A . withProject (projectDir, check) : check (); }
39 catch { return false ; }
40 }
41
42 // Collects each item's `id`, flagging (via callbacks, not thrown errors — callers decide how
43 // severe a given issue is) any item with a missing/non-string id or a duplicate of an id
44 // already seen. Duplicate ids are dropped from the returned list entirely, never silently
45 // treated as "one occurrence satisfies both".
46 function normalizeIds ( items , { onMissing , onDuplicate }) {
47 const ids = [];
48 const seen = new Set ();
49 for ( const item of items) {
50 const id = item && item.id;
51 if ( ! id || typeof id !== 'string' ) {
52 onMissing (item);
53 continue ;
54 }
55 if (seen. has (id)) {
56 onDuplicate (id);
57 continue ;
58 }
59 seen. add (id);
60 ids. push (id);
61 }
62 return ids;
63 }
64
65 // A plan with no genuinely usable `steps` array (missing entirely, not an array, or the
66 // plan itself isn't an object) can never be treated as "zero requirements" — that would let
67 // a malformed plan vacuously satisfy an empty receipt. Reject it outright instead.
68 function validReadBack ( declaration , projectDir ) {
69 if ( ! declaration.readBack) return true ;
70 // These references already have reviewed response-to-value bindings in the issuer.
71 // A local declaration supplements missing knowledge; it cannot replace those bindings.
72 if ([ 'ecom/shipping-option' , 'ecom/delivery-profile' , 'tax/tax-group' ]. includes (declaration.ref)) return false ;
73 const r = declaration.readBack, L = require ( './source-location' ), A = require ( './operation-assessments' );
74 if ( ! L . isExact (r.identityPath) || ! r.values || JSON . stringify (Object. keys (r.values). sort ()) !== JSON . stringify ([ ... declaration.paths]. sort ()) ||
75 Object. values (r.values). some ( p => ! L . isExact (p)) || ! Array. isArray (r.evidence) || ! r.evidence. some ( e => e.kind === 'authoritative-schema' && e.revision)) return false ;
76 try { for ( const e of r.evidence) A . artifact (projectDir || A . current ()?.projectDir,e); return true ; } catch { return false ; }
77 }
78 function validatePlanShape ( plan , projectDir ) {
79 if ( ! plan || typeof plan !== 'object' || ! Array. isArray (plan.steps)) {
80 return { ok: false , reasons: [ 'plan_missing_or_invalid' ], stepIds: [] };
81 }
82 const reasons = [];
83 if (plan.schemaVersion !== SCHEMA_VERSION ) reasons. push ( 'plan_schema_version_mismatch' );
84 const stepIds = normalizeIds (plan.steps, {
85 onMissing : () => reasons. push ( 'plan_step_missing_id' ),
86 onDuplicate : ( id ) => reasons. push ( `plan_duplicate_step_id:${ id }` ),
87 });
88 for ( const step of plan.steps) for ( const ref of Object. hasOwn ( SETUP_EXECUTOR_ENTITY_REFS , step?.id ?? '' ) ? SETUP_EXECUTOR_ENTITY_REFS [step.id] : []) if ( ! (step.entityRefs || []). includes (ref)) reasons. push ( `plan_step_entity_refs_missing:${ step . id }:${ ref }` );
89 const allKeys = new Set ();
90 for ( const step of plan.steps) for ( const key of step.referenceKeys || []) for ( const targetPath of key.paths || []) { const identity = key.ref + ':' + key.sourceStableKey + ':' + targetPath; if ( ! (step.entityRefs || []). includes (key.ref) || ! knownReferencePath (key.ref, targetPath, projectDir) || ! validReadBack (key, projectDir) || allKeys. has (identity)) reasons. push ( 'plan_reference_invalid:' + step.id); allKeys. add (identity); }
91 return { ok: reasons. length === 0 , reasons, stepIds };
92 }
93
94 // `rp-execute-setup/scripts/execute-setup.js` reads requirements at every
95 // consumer as `Array.isArray(x) ? x : x?.requirements`, so exactly two shapes are usable: a
96 // bare array, or a non-array object whose `requirements` is an array. Anything else (an
97 // object carrying only `items`, or neither key) is one the executor silently treats as "no
98 // requirements"; the router blocks it instead. `schemaVersion` is not read.
99 function validateSetupRequirementsShape ( value ) {
100 if (Array. isArray (value)) return { ok: true , errors: [] };
101 if (value && typeof value === 'object' && Array. isArray (value.requirements)) return { ok: true , errors: [] };
102 return { ok: false , errors: [ 'setup requirements must be an array or an object with a requirements array' ] };
103 }
104
105 function validateSetupVerification ({ verification , plan , planDigest , siteId , projectDir }) {
106 if ( ! verification || typeof verification !== 'object' ) {
107 return { ok: false , reasons: [ 'missing_or_invalid_verification' ] };
108 }
109
110 const reasons = [];
111 if (siteId && ! verification.siteId) reasons. push ( 'site_id_missing' );
112 else if (siteId && verification.siteId !== siteId) reasons. push ( 'site_id_mismatch' );
113
114 if (verification.schemaVersion !== SCHEMA_VERSION ) {
115 reasons. push ( 'schema_version_mismatch' );
116 }
117 if ( ! Array. isArray (verification.requirements)) {
118 return { ok: false , reasons: [ ... reasons, 'missing_requirements_array' ] };
119 }
120
121 const planShape = validatePlanShape (plan, projectDir);
122 if (planShape.reasons. includes ( 'plan_missing_or_invalid' )) {
123 // No usable plan to compare coverage against at all — never fall back to "nothing was
124 // required, so an empty receipt counts as full coverage".
125 return { ok: false , reasons: [ ... reasons, ... planShape.reasons] };
126 }
127 reasons. push ( ... planShape.reasons);
128
129 const nonPassed = verification.requirements. filter (( requirement ) => ! requirement || requirement.status !== 'passed' );
130 for ( const requirement of nonPassed) {
131 reasons. push ( `non_passed_requirement:${ ( requirement && requirement . id ) || 'unknown'}` );
132 }
133 for ( const requirement of verification.requirements) {
134 const check = requirement && requirement.status === 'passed' && requirement.verification;
135 if (requirement && requirement.status === 'passed' && ( ! check || ! SETUP_CHECK_METHOD_SET . has (check.method) || typeof check.checkedAt !== 'string' || Number. isNaN (Date. parse (check.checkedAt)) || ! check.observed || typeof check.observed !== 'object' || Array. isArray (check.observed))) reasons. push ( `setup_check_evidence_missing:${ requirement . id || 'unknown'}` );
136 }
137
138 const verifiedIds = normalizeIds (verification.requirements, {
139 onMissing : () => reasons. push ( 'verification_requirement_missing_id' ),
140 onDuplicate : ( id ) => reasons. push ( `duplicate_verification_requirement:${ id }` ),
141 });
142
143 const planIdSet = new Set (planShape.stepIds);
144 const verifiedIdSet = new Set (verifiedIds);
145 for ( const id of planIdSet) {
146 if ( ! verifiedIdSet. has (id)) reasons. push ( `missing_requirement:${ id }` );
147 }
148 for ( const id of verifiedIdSet) {
149 if ( ! planIdSet. has (id)) reasons. push ( `unexpected_requirement:${ id }` );
150 }
151
152 if ( ! planDigest) {
153 reasons. push ( 'plan_digest_unavailable' );
154 } else if (verification.planDigest !== planDigest) {
155 reasons. push ( 'plan_digest_mismatch' );
156 }
157
158 for ( const step of plan.steps) {
159 const requirement = verification.requirements. find ( row => row.id === step.id);
160 reasons. push ( ... validateReferenceBindings (step, requirement?.referenceBindings, projectDir));
161 }
162 return { ok: reasons. length === 0 , reasons };
163 }
164
165 function validateReferenceBindings ( step , bindings , projectDir ) {
166 const declarations = step.referenceKeys || [], errors = [], seen = new Set ();
167 if ( ! Array. isArray (declarations) || bindings !== undefined && ! Array. isArray (bindings)) return [ 'setup_reference_shape_invalid' ];
168 for ( const declaration of declarations) {
169 if ( ! (step.entityRefs || []). includes (declaration.ref) || ! declaration.sourceStableKey || ! Array. isArray (declaration.paths) || ! declaration.paths. length || new Set (declaration.paths).size !== declaration.paths. length ) errors. push ( 'setup_reference_declaration_invalid:' + step.id);
170 const matches = (bindings || []). filter ( row => row.ref === declaration.ref && row.sourceStableKey === declaration.sourceStableKey);
171 if (matches. length !== 1 ) { errors. push ( 'setup_reference_coverage:' + step.id); continue ; }
172 const row = matches[ 0 ];
173 if ( typeof row.targetId !== 'string' || ! row.targetId || ! row.values || JSON . stringify (Object. keys (row.values). sort ()) !== JSON . stringify ([ ... declaration.paths]. sort ()) || Object. values (row.values). some ( value => typeof value !== 'string' || ! value)) errors. push ( 'setup_reference_values_invalid:' + step.id);
174 for ( const targetPath of declaration.paths) { if ( ! knownReferencePath (declaration.ref, targetPath, projectDir) || ! validReadBack (declaration, projectDir)) errors. push ( 'setup_reference_path_unknown:' + step.id); const key = declaration.ref + ':' + declaration.sourceStableKey + ':' + targetPath; if (seen. has (key)) errors. push ( 'setup_reference_duplicate:' + step.id); seen. add (key); }
175 }
176 for ( const binding of bindings || []) if ( ! declarations. some ( row => row.ref === binding.ref && row.sourceStableKey === binding.sourceStableKey)) errors. push ( 'setup_reference_unexpected:' + step.id);
177 return errors;
178 }
179
180 module . exports = { validateReferenceBindings, SCHEMA_VERSION, SETUP_EXECUTOR_ENTITY_REFS, SETUP_CHECK_METHODS, SETUP_CHECK_METHOD_SET, validatePlanShape, validateSetupRequirementsShape, validateSetupVerification };