Setting the file. One moment.
Dependency Proofs · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page function discriminated
— line 76
This file
Number 45.61
Position 61 of 182
Type JavaScript
Size 47 KB
Lines 600 lib/ dependency-proofs.js
JavaScript · 600 lines · 47 KB
14
const
fs
=
require
(
'node:fs'
);
15 const path = require ( 'node:path' );
16 const crypto = require ( 'node:crypto' );
17 const assessments = require ( './operation-assessments' );
18
19 const REASONS = new Set ([ 'no-data' , 'within-write' , 'not-referenced' ]);
20 const ABSENCE_KINDS = new Set ([ 'zero-row' , 'zero-match' , 'capability-absent' ]);
21 const PREDICATE_OPS = new Set ([ 'equals' , 'in' , 'present' , 'startsWith' ]);
22 // Producers whose dependent writes nothing through a transitive ref, or whose own task binds it.
23 const INHERITABLE = new Set ([ 'import-task' , 'disposition' , 'no-producer' , 'secondary-output' , 'child-output' , 'inherited' ]);
24 const refPattern = / ^ [a-z0-9-] + \/ [a-z0-9-] +$ / ;
25 const DOMAINS = path. join (__dirname, '../resources/rp-target-wix/domains' );
26 const hash = bytes => crypto. createHash ( 'sha256' ). update (bytes). digest ( 'hex' );
27 const equal = ( a , b ) => JSON . stringify (a) === JSON . stringify (b);
28
29 function canonical ( value ) {
30 if (Array. isArray (value)) return '[' + value. map (canonical). join ( ',' ) + ']' ;
31 if (value && typeof value === 'object' ) return '{' + Object. keys (value). sort (). map ( key => JSON . stringify (key) + ':' + canonical (value[key])). join ( ',' ) + '}' ;
32 return JSON . stringify (value);
33 }
34 // The one loader for dependency knowledge, shared by the resolver and the runtime gate. Tests may
35 // point it at a fixture knowledge tree (the same idea as plugin-cms-fallback's targetKnowledgeRoot);
36 // the rules applied to whatever it loads never change.
37 let domainsRoot = DOMAINS ;
38 function useTargetKnowledgeRoot ( root ) {
39 const prior = domainsRoot;
40 domainsRoot = root ? path. resolve (root) : DOMAINS ;
41 return () => { domainsRoot = prior; };
42 }
43 const MISSING = Object. freeze ({ code: 'ENOENT' });
44 function bundledDomain ( ref ) {
45 if ( ! refPattern. test (ref)) throw new Error ( `entity_ref_unknown:${ ref }` );
46 // Parsed once per file version; callers get their own copy, as they did from a fresh parse.
47 try { const found = require ( './cached-json' ). readJson (path. join (domainsRoot, ref. split ( '/' )[ 0 ], 'entities' , ref. split ( '/' )[ 1 ] + '.json' ), MISSING ); if (found === MISSING ) throw MISSING ; return structuredClone (found); }
48 catch (error) { if (error.code?. startsWith ( 'mapping_assessment_' )) throw error; throw new Error ( `entity_ref_unknown:${ ref }` ); }
49 }
50 function domain ( ref ) { return assessments. effectiveDomain (ref, bundledDomain (ref)); }
51 // The reviewed contract for one write operation, by name or alias. Null when knowledge has none.
52 function operationContract ( entityRef , operation ) {
53 if ( ! operation) return null ;
54 const contracts = domain (entityRef).operationContracts || {};
55 for ( const [ name , contract ] of Object. entries (contracts)) if (name === operation || (contract.aliases || []). includes (operation)) return { name, contract };
56 return null ;
57 }
58 // Prose (evidence, notes) may change without invalidating a proof; every field a check reads does.
59 function contractDigest ( contract ) {
60 const { evidence : _evidence , notes : _notes , ... semantic } = contract || {};
61 return hash ( canonical (semantic));
62 }
63 // The refs a write operation actually depends on: an operation contract may declare an entity-level
64 // edge inapplicable (Import Order carries no Shipping Options or delivery-profile reference).
65 function applicableDependsOn ( entityRef , operation ) {
66 const deps = domain (entityRef).dependsOn || [];
67 const found = operationContract (entityRef, operation);
68 if ( ! found) return deps;
69 return [ ...new Set ([ ... deps, ... Object. keys (found.contract.dependencies || {})])]. filter ( ref => ! found.contract.dependencies?.[ref]?.inapplicable);
70 }
71 function inapplicableRefs ( entityRef , operation ) {
72 const found = operationContract (entityRef, operation);
73 return found ? Object. entries (found.contract.dependencies || {}). filter (([, dep ]) => dep.inapplicable). map (([ ref ]) => ref) : [];
74 }
75 function guardPath ( guard ) { return typeof guard === 'string' ? guard : guard?.path; }
76 function discriminated ( guard ) { return typeof guard === 'object' && guard !== null && guard.equals !== undefined ; }
77 function stripInput ( targetPath , writeInput ) {
78 if ( typeof targetPath !== 'string' ) return null ;
79 return writeInput && targetPath. startsWith (writeInput + '.' ) ? targetPath. slice (writeInput. length + 1 ) : targetPath;
80 }
81 // Path templates overlap when one is the other or an ancestor of it; `[]` and `[n]` compare equal.
82 function pathsOverlap ( a , b ) {
83 const norm = value => value. replace ( / \[ \d * \] / g , '[]' );
84 const x = norm (a), y = norm (b);
85 return x === y || y. startsWith (x + '.' ) || x. startsWith (y + '.' ) || y. startsWith (x + '[]' ) || x. startsWith (y + '[]' );
86 }
87
88 // --- deterministic payload and record reads ---------------------------------------------------
89 function valuesAt ( value , template ) {
90 const parts = template. split ( '.' ), out = [];
91 ( function visit ( node , index ) {
92 if (index === parts. length ) { out. push (node); return ; }
93 if (node === null || typeof node !== 'object' ) return ;
94 const part = parts[index], array = part. endsWith ( '[]' ), key = array ? part. slice ( 0 , - 2 ) : part;
95 if ( ! Object. hasOwn (node, key)) return ;
96 if (array) { if (Array. isArray (node[key])) for ( const entry of node[key]) visit (entry, index + 1 ); }
97 else visit (node[key], index + 1 );
98 })(value, 0 );
99 return out;
100 }
101 // A guarded path is present when a payload carries a value there (and, for a discriminated guard,
102 // that value equals the discriminator). Presence contradicts a proof that no reference is written.
103 function guardHits ( payload , guards ) {
104 const hits = [];
105 for ( const guard of guards || []) {
106 const template = guardPath (guard);
107 const values = valuesAt (payload, template). filter ( value => value !== undefined && value !== null && value !== '' );
108 if (values. some ( value => typeof guard === 'string' || guard.equals === undefined || value === guard.equals)) hits. push (template);
109 }
110 return hits;
111 }
112 function validatePredicate ( predicate ) {
113 if ( ! predicate || typeof predicate !== 'object' ) return false ;
114 if (Array. isArray (predicate.any)) return predicate.any. length > 0 && predicate.any. every (validatePredicate);
115 if ( typeof predicate.path !== 'string' || ! / ^ \w + (?: \[\] ) ? (?: \. \w + (?: \[\] ) ? ) *$ / . test (predicate.path) || ! PREDICATE_OPS . has (predicate.op)) return false ;
116 if (predicate.op === 'in' ) return Array. isArray (predicate.values) && predicate.values. length > 0 ;
117 if (predicate.op === 'equals' || predicate.op === 'startsWith' ) return predicate.value !== undefined && (predicate.op !== 'startsWith' || typeof predicate.value === 'string' );
118 return true ;
119 }
120 function predicateMatches ( record , predicate ) {
121 if (Array. isArray (predicate.any)) return predicate.any. some ( clause => predicateMatches (record, clause));
122 const values = valuesAt (record, predicate.path);
123 if (predicate.op === 'present' ) return values. some ( value => value !== undefined && value !== null && value !== '' );
124 if (predicate.op === 'equals' ) return values. some ( value => value === predicate.value);
125 if (predicate.op === 'in' ) return values. some ( value => predicate.values. includes (value));
126 return values. some ( value => typeof value === 'string' && value. startsWith (predicate.value));
127 }
128
129 // --- mapping-plan reads -----------------------------------------------------------------------
130 function rowOutputs ( row ) {
131 const outputs = [];
132 for ( const mapping of row.fieldMappings || []) for ( const output of mapping.outputs || []) if (output && typeof output === 'object' ) outputs. push (output);
133 return outputs;
134 }
135 // One exact write operation per dependent: the preferred writer, plus only the auxiliary operations
136 // its contract names. A single-create preference beside bulk-create outputs cannot share a proof.
137 function resolveRowOperation ( row ) {
138 const primary = row.preferredWrite?.writerId || null ;
139 const ops = [ ...new Set ( rowOutputs (row). filter ( output => ! output.targetRef || output.targetRef === row.targetRef). map ( output => output.writeOperation). filter (Boolean))]. sort ();
140 const found = primary ? operationContract (row.targetRef, primary) : (ops. length === 1 ? operationContract (row.targetRef, ops[ 0 ]) : null );
141 if ( ! found) return { operation: primary || (ops. length === 1 ? ops[ 0 ] : null ), contract: null , errors: [] };
142 const allowed = new Set ([found.name, ... (found.contract.aliases || []), ... (found.contract.auxiliaryOperations || [])]);
143 const extra = ops. filter ( op => ! allowed. has (op));
144 return { operation: found.name, contract: found.contract, errors: extra. length ? [ `dependency_operation_mixed:${ row . targetRef }:${ found . name }:${ extra . join ( ',' ) }` ] : [] };
145 }
146 // Held or excluded scope is never an executable producer or writer.
147 const INACTIVE = new Set ([ 'excluded' , 'rejected' , 'deferred' ]);
148 const activeRows = ( plan , readEntityMappings ) => readEntityMappings (plan). filter ( row => row && ! INACTIVE . has (row.status));
149 function rowsByTarget ( plan , readEntityMappings ) {
150 const rows = new Map ();
151 for ( const row of readEntityMappings (plan)) {
152 if ( ! row?.targetRef || ! refPattern. test (row.targetRef) || INACTIVE . has (row.status)) continue ;
153 if ( ! rows. has (row.targetRef)) rows. set (row.targetRef, []);
154 rows. get (row.targetRef). push (row);
155 }
156 return rows;
157 }
158 // Paths the dependent's write sends: its own rows' outputs, plus any active output that names its
159 // write operation, whatever target it is labelled with — the operation decides the request body.
160 function mappedPaths ( rows , targetRef , operation , contract , allRows = []) {
161 const ops = new Set ([operation, ... (contract?.aliases || [])]);
162 const paths = [];
163 const add = output => { const target = stripInput (output.targetPath, output.writeInput); if (target) paths. push (target); };
164 for ( const row of rows) for ( const output of rowOutputs (row)) {
165 if (output.targetRef && output.targetRef !== targetRef) continue ;
166 if (output.writeOperation && ! ops. has (output.writeOperation)) continue ;
167 add (output);
168 }
169 for ( const row of allRows) if ( ! rows. includes (row)) for ( const output of rowOutputs (row)) if (ops. has (output.writeOperation)) add (output);
170 return paths;
171 }
172 function mislabelled ( plan , readEntityMappings , dependentRef , found ) {
173 const ops = new Set ([found.name, ... (found.contract.aliases || [])]);
174 for ( const row of activeRows (plan, readEntityMappings)) for ( const output of rowOutputs (row)) {
175 const targetRef = output.targetRef || row?.targetRef;
176 if ( ! ops. has (output.writeOperation) || targetRef === dependentRef) continue ;
177 let owned = null ;
178 try { owned = refPattern. test (targetRef || '' ) ? operationContract (targetRef, output.writeOperation) : null ; } catch { owned = null ; }
179 if ( ! owned) return true ;
180 }
181 return false ;
182 }
183
184 // --- proof validation -------------------------------------------------------------------------
185 function proofKey ( entityRef , operation , ref ) { return `${ entityRef }|${ operation }|${ ref }` ; }
186 function insideProject ( projectDir , file ) {
187 if ( typeof file !== 'string' || ! file) throw new Error ( 'dependency_proof_evidence_path_invalid' );
188 const root = path. resolve (projectDir), absolute = path. resolve (root, file);
189 if ( ! absolute. startsWith (root + path.sep)) throw new Error ( 'dependency_proof_evidence_path_invalid' );
190 return absolute;
191 }
192 // zero-match: a deterministic predicate over a complete, populated extract. Returns the measured
193 // count and the extract digest it was measured on; throws when the population is not complete.
194 // Before approval the extract must also be the census-measured population; at runtime a source
195 // catch-up extract is re-measured on its own completeness (the census predates it).
196 // Runtime re-validates before every batch, so a scan is cached per extract file identity and
197 // predicate: one full scan per distinct extract version, never one per batch, edge or record.
198 const scanCache = new Map (), scanTotals = { scans: 0 , records: 0 };
199 function statKey ( file ) { const stat = fs. statSync (file); return `${ stat . ino }:${ stat . size }:${ stat . mtimeMs }:${ stat . ctimeMs }` ; }
200 function extractDigest ( file ) {
201 const key = 'digest:' + file, stamp = statKey (file), prior = scanCache. get (key);
202 if (prior?.stamp === stamp) return prior.value;
203 const value = hash (fs. readFileSync (file)); scanCache. set (key, { stamp, value }); return value;
204 }
205 function measureZeroMatch ( projectDir , absence , censusRow , work , stage ) {
206 if ( ! censusRow) throw new Error ( 'dependency_proof_absence_unmeasured' );
207 if ( ! Number. isInteger (censusRow.recordCount) || censusRow.recordCount <= 0 ) throw new Error ( 'dependency_proof_population_empty' );
208 if ( ! validatePredicate (absence.predicate)) throw new Error ( 'dependency_proof_predicate_invalid' );
209 const manifest = JSON . parse (fs. readFileSync ( insideProject (projectDir, absence.manifestPath), 'utf8' ));
210 const extractFile = insideProject (projectDir, absence.extractPath);
211 const key = 'scan:' + extractFile + ':' + canonical (absence.predicate), stamp = statKey (extractFile);
212 let scan = scanCache. get (key)?.stamp === stamp ? scanCache. get (key).value : null ;
213 if ( ! scan) {
214 const bytes = fs. readFileSync (extractFile), records = bytes. toString ( 'utf8' ). split ( / \r ? \n / ). filter (Boolean). map ( line => JSON . parse (line));
215 scan = { matches: records. filter ( record => predicateMatches (record, absence.predicate)). length , extractSha256: hash (bytes), population: records. length };
216 scanCache. set (key, { stamp, value: scan }); scanTotals.scans ++ ; scanTotals.records += records. length ;
217 if (work) { work.predicateScans ++ ; work.recordsScanned += records. length ; }
218 }
219 if ( ! Number. isInteger (manifest.rowsWritten) || manifest.rowsWritten !== manifest.snapshotTotal || scan.population !== manifest.rowsWritten || stage !== 'runtime' && scan.population !== censusRow.recordCount) throw new Error ( 'dependency_proof_population_incomplete' );
220 return scan;
221 }
222 // Validate one proposed proof against the reviewed contract, the census and the mapping. `stage`
223 // 'mapping' and 'codegen' require bound evidence to be current; 'runtime' re-measures a changed
224 // extract instead (source catch-up) and reports a contradiction rather than throwing.
225 function validateProof ( proof , { rows = new Map (), plan = null , readEntityMappings = () => [], projectDir = null , stage = 'mapping' , work = null } = {}) {
226 const errors = [], key = proof && typeof proof === 'object' ? `${ proof . dependentRef }:${ proof . ref }` : '<invalid>' ;
227 const fail = code => { errors. push ( `${ code }:${ key }` ); return { ok: false , errors, proof }; };
228 if ( ! proof || typeof proof !== 'object' || typeof proof.id !== 'string' || ! proof.id || ! refPattern. test (proof.dependentRef || '' ) || ! refPattern. test (proof.ref || '' ) || typeof proof.operation !== 'string' || ! REASONS . has (proof.reason)) return fail ( 'dependency_proof_invalid' );
229 if ( typeof proof.explanation !== 'string' || ! proof.explanation. trim () || ! Array. isArray (proof.evidenceRefs) || ! proof.evidenceRefs. length || proof.evidenceRefs. some ( ref => typeof ref !== 'string' || ! ref)) return fail ( 'dependency_proof_evidence_missing' );
230 let found;
231 try { found = operationContract (proof.dependentRef, proof.operation); if ( ! ( domain (proof.dependentRef).dependsOn || []). includes (proof.ref) && ! Object. hasOwn (found?.contract.dependencies || {},proof.ref)) return fail ( 'dependency_proof_not_a_dependency' ); }
232 catch (error) { return fail (error.message. split ( ':' )[ 0 ]); }
233 if ( ! found) return fail ( 'dependency_proof_contract_unknown' );
234 const dep = found.contract.dependencies?.[proof.ref];
235 if ( ! dep) return fail ( 'dependency_proof_contract_unknown' );
236 if (dep.inapplicable) return fail ( 'dependency_proof_inapplicable' );
237 if (proof.operation !== found.name) return fail ( 'dependency_proof_operation_alias' );
238 if (proof.operationContractDigest !== contractDigest (found.contract)) return fail ( 'dependency_proof_contract_stale' );
239 if ( ! Array. isArray (dep.guardedPaths) || ! dep.guardedPaths. length ) return fail ( 'dependency_proof_guard_unknown' );
240 if ( ! equal (proof.guardedPaths, dep.guardedPaths)) return fail ( 'dependency_proof_guard_mismatch' );
241 const targetRows = plan ? ( rowsByTarget (plan, readEntityMappings). get (proof.dependentRef) || []) : [];
242 if (plan) {
243 if ( ! targetRows. length ) return fail ( 'dependency_proof_dependent_unmapped' );
244 for ( const row of targetRows) { const resolved = resolveRowOperation (row); if (resolved.errors. length || resolved.operation !== found.name) return fail ( 'dependency_proof_operation_mismatch' ); }
245 }
246 // An output carrying the dependent's write operation under a target that does not own it would
247 // hide its paths from the guard check below.
248 if (plan && mislabelled (plan, readEntityMappings, proof.dependentRef, found)) return fail ( 'dependency_proof_operation_mislabelled' );
249 const mapped = plan ? mappedPaths (targetRows, proof.dependentRef, found.name, found.contract, activeRows (plan, readEntityMappings)) : [];
250 // A `{ path, equals }` guard matches one value at its path (the Gift Cards catalog appId). A plan
251 // records which paths it writes, not their values, so mapping only refuses plain guards; the
252 // runtime payload guards (guardHits) enforce the value on every record before it is sent.
253 const guardMapped = () => mapped. some ( target => dep.guardedPaths. some ( guard => ! discriminated (guard) && pathsOverlap (target, guardPath (guard))));
254 let status = 'valid' , measured = null ;
255 if (proof.reason === 'within-write' ) {
256 if ( ! dep.withinWrite || ! Array. isArray (dep.creatingPaths) || ! dep.creatingPaths. length ) return fail ( 'dependency_proof_within_write_unsupported' );
257 if ( ! equal (proof.creatingPaths, dep.creatingPaths)) return fail ( 'dependency_proof_creating_paths_mismatch' );
258 const keys = found.contract.recovery?.reconciliationKeys || [];
259 if ( ! keys. length ) return fail ( 'dependency_proof_recovery_undefined' );
260 if (plan) {
261 if ( guardMapped ()) return fail ( 'dependency_proof_guard_mapped' );
262 if ( ! keys. some ( entry => entry.clientAssigned || mapped. some ( target => target. replace ( / \[ \d * \] / g , '[]' ) === entry.path))) return fail ( 'dependency_proof_reconciliation_key_unmapped' );
263 }
264 } else {
265 if (dep.conditional !== true ) return fail ( 'dependency_proof_not_conditional' );
266 if (dep.nonReferencePrerequisite) return fail ( 'dependency_proof_prerequisite_remains' );
267 if (plan && guardMapped ()) return fail ( 'dependency_proof_guard_mapped' );
268 }
269 if (proof.reason === 'not-referenced' && plan && targetRows. some ( row => row.fieldMappingStatus !== undefined && row.fieldMappingStatus !== 'resolved' )) return fail ( 'dependency_proof_mapping_incomplete' );
270 if (proof.reason === 'no-data' ) {
271 const absence = proof.absence;
272 if ( ! absence || ! ABSENCE_KINDS . has (absence.kind)) return fail ( 'dependency_proof_absence_unmeasured' );
273 try {
274 if (absence.kind === 'zero-row' ) {
275 if ( ! Array. isArray (absence.censusIds) || ! absence.censusIds. length ) return fail ( 'dependency_proof_absence_unmeasured' );
276 for ( const id of absence.censusIds) {
277 const row = rows. get (id);
278 if ( ! row || ! Number. isInteger (row.recordCount)) return fail ( 'dependency_proof_absence_unmeasured' );
279 if (row.recordCount !== 0 || row.state !== 'no-data' ) { if (stage === 'runtime' ) { status = 'contradicted' ; break ; } return fail ( 'dependency_proof_absence_contradicted' ); }
280 }
281 } else if (absence.kind === 'zero-match' ) {
282 if ( ! projectDir) return fail ( 'dependency_proof_absence_unmeasured' );
283 const current = extractDigest ( insideProject (projectDir, absence.extractPath));
284 if (current !== absence.extractSha256 && stage !== 'runtime' ) return fail ( 'dependency_proof_evidence_stale' );
285 measured = measureZeroMatch (projectDir, absence, rows. get (absence.censusId), work, stage);
286 if (measured.matches > 0 ) { if (stage === 'runtime' ) status = 'contradicted' ; else return fail ( 'dependency_proof_absence_contradicted' ); }
287 } else {
288 if ( ! projectDir || typeof absence.capability !== 'string' || ! absence.capability) return fail ( 'dependency_proof_absence_unmeasured' );
289 const bytes = fs. readFileSync ( insideProject (projectDir, absence.evidencePath));
290 if ( hash (bytes) !== absence.evidenceSha256) return fail ( 'dependency_proof_evidence_stale' );
291 const evidence = JSON . parse (bytes. toString ( 'utf8' ));
292 // Only an explicit, scoped absence counts; a missing or failed discovery response does not.
293 if (evidence?.capability !== absence.capability || evidence.status !== 'absent' || typeof evidence.scope !== 'string' || ! evidence.scope || ! Number. isFinite (Date. parse (evidence.observedAt))) return fail ( 'dependency_proof_absence_unmeasured' );
294 }
295 } catch (error) {
296 if ( / ^ dependency_proof_/ . test (error.message)) return fail (error.message);
297 return fail (error.code === 'ENOENT' ? 'dependency_proof_evidence_missing' : 'dependency_proof_evidence_invalid' );
298 }
299 }
300 return { ok: true , errors, proof, status, measured, contractName: found.name };
301 }
302 function validateProofs ( proofs , context = {}) {
303 const valid = new Map (), errors = [];
304 if (proofs !== undefined && ! Array. isArray (proofs)) return { proofs: valid, errors: [ 'dependency_proofs_invalid' ] };
305 for ( const proof of proofs || []) {
306 const result = validateProof (proof, context);
307 if ( ! result.ok) { errors. push ( ... result.errors); continue ; }
308 const key = proofKey (proof.dependentRef, proof.operation, proof.ref);
309 if (valid. has (key)) { errors. push ( `dependency_proof_duplicate:${ proof . dependentRef }:${ proof . ref }` ); continue ; }
310 valid. set (key, { ... proof, status: result.status, measured: result.measured });
311 }
312 return { proofs: valid, errors };
313 }
314
315 // --- graph resolution -------------------------------------------------------------------------
316 // A setup step is never per-record: a ref that may come from either an import task or a setup step
317 // (a tax group) is per-record only when an import task produces it.
318 const SETUP_PRODUCERS = new Set ([ 'setup-step' , 'pending-setup' ]);
319 function edgeMode ( entityRefs , ref , producer ) {
320 if ( SETUP_PRODUCERS . has (producer?.kind)) return 'task-complete' ;
321 return entityRefs. every ( entity => domain (entity).dependencyModes?.[ref] === 'per-record' ) ? 'per-record' : 'task-complete' ;
322 }
323 // Every ref a dependent needs, operation-aware, with the parents each transitive ref was reached
324 // through. `operationFor(ref)` names the operation that produces an intermediate ref, if known.
325 // `adopted` names refs whose task only reads and verifies what already exists on the site (mapping
326 // `writeMode: 'adopt-only-no-writes'`): nothing is written through them, so their own write-time
327 // dependencies are not expanded, and the closure keeps the ref and stops there.
328 function dependencyClosure ( entityRefs , operations , operationFor , adopted = new Set ()) {
329 const direct = new Map (), via = new Map (), cycles = [];
330 for ( const entity of entityRefs) for ( const ref of applicableDependsOn (entity, operations[entity])) {
331 // A ref this task writes itself is satisfied inside the task (its writer orders the writes);
332 // binding it to the task would make the task wait on itself.
333 if (entityRefs. includes (ref)) continue ;
334 if ( ! direct. has (ref)) direct. set (ref, []);
335 direct. get (ref). push (entity);
336 }
337 const seen = new Set (direct. keys ()), queue = [ ... direct. keys ()];
338 while (queue. length ) {
339 const parent = queue. shift ();
340 if (adopted. has (parent)) continue ;
341 for ( const ref of applicableDependsOn (parent, operationFor (parent))) {
342 if (entityRefs. includes (ref)) { cycles. push (ref); continue ; }
343 if ( ! direct. has (ref)) { if ( ! via. has (ref)) via. set (ref, new Set ()); via. get (ref). add (parent); }
344 if ( ! seen. has (ref)) { seen. add (ref); queue. push (ref); }
345 }
346 }
347 return { direct, via, refs: [ ... seen]. sort (), cycles: [ ...new Set (cycles)]. sort () };
348 }
349 // Plan-side adopt-only evidence: a target is adopt-only only when every active mapping row for it says
350 // `writeMode: 'adopt-only-no-writes'`. A target with both kinds of row is `mixed` and refused, because
351 // the same task would both create and promise to write nothing.
352 const ADOPT_ONLY_WRITE_MODE = 'adopt-only-no-writes' ;
353 function adoptOnlyRefsOf ( plan , readEntityMappings ) {
354 const rows = rowsByTarget (plan, readEntityMappings), refs = [], mixed = [];
355 for ( const [ targetRef , targetRows ] of rows) {
356 const adopting = targetRows. filter ( row => row.writeMode === ADOPT_ONLY_WRITE_MODE ). length ;
357 if (adopting === targetRows. length ) refs. push (targetRef); else if (adopting) mixed. push (targetRef);
358 }
359 return { refs: refs. sort (), mixed: mixed. sort () };
360 }
361 // Resolve producers for every dependent. One function serves mapping preflight and codegen; the
362 // stage adapters only supply who can produce what. A dependent marked `adoptOnly` is honoured only
363 // when the plan marks every ref it owns adopt-only (`adoptOnlyRefs`); the reverse also holds: a
364 // dependent owning a plan-adopt-only ref must carry the mark, so a task that omits it is never
365 // treated as a creating task silently. Either mismatch is an error, and the creating-task rules still
366 // run for that dependent, so the mark can never be used to skip a dependency. Setup at mapping time is `pending-setup`: it can
367 // pass structural readiness but is never a producer of record.
368 function resolveDependencyGraph ({ dependents , imports , setupFor = () => null , dispositions = {}, proofs = new Map (), adoptOnlyRefs = [], adoptOnlyMixed = [] }) {
369 const result = {}, errors = [], usedProofs = new Set ();
370 const approvedAdopt = new Set (adoptOnlyRefs), adopted = new Set (), adoptedTasks = new Set (), adoptErrors = new Map ();
371 for ( const dependent of dependents) {
372 const owned = dependent.entityRefs. filter ( ref => approvedAdopt. has (ref)), mixed = dependent.entityRefs. filter ( ref => adoptOnlyMixed. includes (ref));
373 const problems = mixed. map ( ref => `dependency_adopt_only_mixed:${ dependent . id }:${ ref }` );
374 if (dependent.adoptOnly === true && owned. length === dependent.entityRefs. length && ! problems. length ) { adoptedTasks. add (dependent.id); dependent.entityRefs. forEach ( ref => adopted. add (ref)); }
375 else if (dependent.adoptOnly === true ) problems. push ( `dependency_adopt_only_unapproved:${ dependent . id }` );
376 else if (owned. length ) problems. push ( `dependency_adopt_only_unmarked:${ dependent . id }` );
377 if (problems. length ) adoptErrors. set (dependent.id, problems);
378 }
379 const producerOf = new Map ();
380 for ( const producer of imports) for ( const ref of producer.entityRefs) if ( ! producerOf. has (ref)) producerOf. set (ref, producer);
381 const operationFor = ref => producerOf. get (ref)?.operations?.[ref];
382 const secondaryFor = ( ref , selfId ) => imports. filter ( producer => producer.id !== selfId && producer.entityRefs. some ( entity => operationContract (entity, producer.operations?.[entity])?.contract.secondaryOutputs?.[ref])). map ( producer => ({ kind: 'secondary-output' , taskId: producer.id, parentRef: producer.entityRefs. find ( entity => operationContract (entity, producer.operations?.[entity])?.contract.secondaryOutputs?.[ref]) }));
383 // A child entity (a product's variants) is created inside its parent's write. Its identities come
384 // from the parent task whose reviewed operation declares them; with no such task nothing can name a
385 // child, so the ref is inherited from the parent and any write of it is refused.
386 const childOf = ref => { try { return domain (ref).childOf || null ; } catch { return null ; } };
387 const childFor = ( ref , selfId ) => imports. filter ( producer => producer.id !== selfId && producer.entityRefs. includes ( childOf (ref)) && operationContract ( childOf (ref), producer.operations?.[ childOf (ref)])?.contract.childOutputs?.[ref]). map ( producer => ({ kind: 'child-output' , taskId: producer.id, parentRef: childOf (ref) }));
388 // A child is never imported on its own: its identities exist only in the parent's verified receipt.
389 for ( const producer of imports) for ( const ref of producer.entityRefs) if ( childOf (ref)) errors. push ( `dependency_child_standalone:${ producer . id }:${ ref }` );
390 for ( const dependent of dependents) {
391 const taskErrors = [ ... (adoptErrors. get (dependent.id) || [])], edges = [], bound = new Map (), carried = new Set ();
392 // An adopt-only task writes nothing, so it needs no producer for any dependency of what it adopts.
393 if (adoptedTasks. has (dependent.id)) { result[dependent.id] = { refs: [], edges: [], dependsOn: [], errors: taskErrors }; continue ; }
394 let closure;
395 try { closure = dependencyClosure (dependent.entityRefs, dependent.operations || {}, operationFor, adopted); }
396 catch (error) { errors. push (error.message); result[dependent.id] = { refs: [], edges: [], dependsOn: [], errors: [error.message] }; continue ; }
397 for ( const ref of closure.cycles) taskErrors. push ( `dependency_cycle:${ dependent . id }:${ ref }` );
398 // Direct refs first, then transitive refs in an order where every parent is already bound.
399 // A child ref is bound after its parent, whose producer decides the child's fallback.
400 const order = [ ... closure.direct. keys ()]. sort (( a , b ) => Number ( Boolean ( childOf (a))) - Number ( Boolean ( childOf (b))) || a. localeCompare (b));
401 const pending = closure.refs. filter ( ref => ! closure.direct. has (ref));
402 while (pending. length ) {
403 const index = pending. findIndex ( ref => [ ... closure.via. get (ref)]. every ( parent => ! pending. includes (parent)));
404 order. push ( ... pending. splice (index < 0 ? 0 : index, 1 ));
405 }
406 for ( const ref of order) {
407 const isDirect = closure.direct. has (ref), candidates = [];
408 // Reached only through per-record parents: each dependent record waits for its own parent
409 // record, whose write was already gated on this ref, so no task-wide wait is added.
410 if ( ! isDirect && [ ... closure.via. get (ref)]. every ( parent => carried. has (parent))) {
411 const producer = { kind: 'inherited' , via: [ ... closure.via. get (ref)]. sort () };
412 bound. set (ref, producer); edges. push ({ ref, mode: 'task-complete' , producer }); carried. add (ref); continue ;
413 }
414 // A task is never its own producer: the readiness gate would wait for a pass of itself.
415 if ( ! childOf (ref)) for ( const producer of imports) if (producer.id !== dependent.id && producer.entityRefs. includes (ref)) candidates. push ({ kind: 'import-task' , taskId: producer.id });
416 const scoped = isDirect ? closure.direct. get (ref). map ( entity => ({ entity, proof: proofs. get ( proofKey (entity, dependent.operations?.[entity], ref)) })) : [];
417 // Mapping-time `pending-setup` with no planned step is only a placeholder: it yields to a proof.
418 const setup = setupFor (ref);
419 if (setup && ! (setup.kind === 'pending-setup' && ! setup.stepId && scoped. some ( row => row.proof))) candidates. push (setup);
420 if (dispositions[ref]) candidates. push ({ kind: 'disposition' , censusIds: dispositions[ref].censusIds, reconciliationSha256: dispositions[ref].reconciliationSha256 });
421 if (isDirect) {
422 if (scoped. some ( row => row.proof)) {
423 if ( ! scoped. every ( row => row.proof)) { taskErrors. push ( `dependency_proof_scope_incomplete:${ dependent . id }:${ ref }` ); continue ; }
424 for ( const row of scoped) usedProofs. add ( proofKey (row.entity, dependent.operations[row.entity], ref));
425 candidates. push ({ kind: 'no-producer' , proofs: scoped. map (({ entity , proof }) => ({ entityRef: entity, operation: proof.operation, reason: proof.reason, proofId: proof.id, operationContractDigest: proof.operationContractDigest })) });
426 }
427 // A member task's verified contact identities can satisfy a contact reference only when no
428 // other producer exists, so two producers never compete for the same reference.
429 if ( ! candidates. length ) candidates. push ( ... secondaryFor (ref, dependent.id));
430 if ( ! candidates. length && childOf (ref)) candidates. push ( ... childFor (ref, dependent.id));
431 if ( ! candidates. length && childOf (ref) && bound. has ( childOf (ref))) candidates. push ({ kind: 'inherited' , via: [ childOf (ref)] });
432 } else if ( ! candidates. length ) {
433 const parents = [ ... closure.via. get (ref)]. sort ();
434 if (parents. every ( parent => INHERITABLE . has (bound. get (parent)?.kind))) candidates. push ({ kind: 'inherited' , via: parents });
435 }
436 // A proof that this operation writes no reference needs no producer, so a setup step (pending
437 // at mapping, real at codegen) that makes the ref for other dependents never competes with it:
438 // mapping and codegen then bind the same proof whether or not the plan has that step (an
439 // add-on's optional tax group). An import task or a disposition still conflicts.
440 if (candidates. some ( candidate => candidate.kind === 'no-producer' )) for ( let i = candidates. length - 1 ; i >= 0 ; i -- ) if ([ 'setup-step' , 'pending-setup' ]. includes (candidates[i].kind)) candidates. splice (i, 1 );
441 if (candidates. length > 1 && candidates. some ( candidate => candidate.kind === 'no-producer' )) { taskErrors. push ( `dependency_proof_conflict:${ dependent . id }:${ ref }` ); continue ; }
442 if (candidates. length !== 1 ) { taskErrors. push ( `dependency_${ candidates . length ? 'multiply_bound' : 'unbound'}:${ ref }` ); continue ; }
443 const producer = candidates[ 0 ], mode = producer.kind === 'inherited' ? 'task-complete' : edgeMode (dependent.entityRefs, ref, producer);
444 if (mode === 'per-record' && [ 'setup-step' , 'pending-setup' ]. includes (producer.kind) || [ 'secondary-output' , 'child-output' ]. includes (producer.kind) && mode !== 'per-record' ) taskErrors. push ( `dependency_mode_invalid:${ dependent . id }:${ ref }` );
445 bound. set (ref, producer); edges. push ({ ref, mode, producer });
446 // A proof that this operation writes no such reference carries nothing through it either:
447 // the ref's own dependencies (a gift card product's media) add no task-wide wait.
448 if (mode === 'per-record' || producer.kind === 'no-producer' ) carried. add (ref);
449 }
450 const dependsOn = [ ...new Set (edges. filter ( edge => [ 'import-task' , 'secondary-output' , 'child-output' ]. includes (edge.producer.kind)). map ( edge => edge.producer.taskId))]. sort ();
451 result[dependent.id] = { refs: closure.refs, edges, dependsOn, errors: taskErrors };
452 errors. push ( ... taskErrors);
453 }
454 for ( const key of proofs. keys ()) if ( ! usedProofs. has (key)) errors. push ( `dependency_proof_unused:${ key . split ( '|' ). join ( ':' ) }` );
455 return { ok: errors. length === 0 , errors, tasks: result };
456 }
457
458 // --- mapping preflight (before approval) ------------------------------------------------------
459 const NEXT_STEPS = {
460 dependency_unbound: 'Import the dependency, record an approved exclusion, or propose a supported noProducer proof for the exact dependent operation.' ,
461 dependency_multiply_bound: 'Keep exactly one producer for this dependency.' ,
462 dependency_proof_conflict: 'A dependency with a real producer cannot also be explained as needing none; remove one.' ,
463 dependency_operation_mixed: 'Resolve one write operation for this dependent; a single-create preference and bulk-create outputs cannot share a proof.' ,
464 dependency_proof_guard_mapped: 'The approved mapping writes a guarded reference path; bind a real producer or stop writing the path.' ,
465 dependency_proof_operation_mislabelled: 'A mapping output uses this write operation under a different target; label it with the entity that write creates.' ,
466 dependency_proof_contract_stale: 'Target knowledge changed; recompute operationContractDigest and re-review the proof.' ,
467 dependency_proof_evidence_stale: 'Source evidence changed; re-measure the proof on the current extract.' ,
468 dependency_proof_absence_contradicted: 'The measured data contains the dependency; it is not absent.' ,
469 setup_step_claims_import_entity: 'Drop the ref from the setup step; the import task that writes it owns it. A read-only prerequisite check lists no import-owned refs.' ,
470 dependency_adopt_only_unapproved: 'The task is marked adopt-only but the approved mapping does not mark every ref it owns adopt-only; fix the mark or the mapping row.' ,
471 dependency_adopt_only_unmarked: 'The approved mapping marks this ref adopt-only but the task is not marked adoptOnly: true; emit the mark (rp-import-codegen) or change the mapping row to create.' ,
472 dependency_adopt_only_mixed: 'The mapping has adopt-only and creating rows for one target; split them or make every row the same write mode.' ,
473 dependency_child_standalone: 'Map these source records into their parent entity; the parent write creates them and its verified receipt names them.' ,
474 dependency_proof_expected: 'The mapping writes none of this conditional reference, so nothing has to produce it: add the suggestedProof to mapping-plan.json dependencyProofs. Setup only produces it when a mapped path references it.' ,
475 };
476 const UNPRODUCIBLE_NEXT = 'Nothing can produce this dependency: rp-target-wix has no writer for it and no setup step makes it, so it cannot be imported. Record an approved exclusion (census disposition) for it, or propose a supported noProducer proof for the exact dependent operation.' ;
477 // Whether the target has any way to be written: setup, a parent write, a registered writer, or
478 // guarded raw requests for a native entity with no writer.
479 // Unknown knowledge keeps the ordinary message.
480 function producible ( ref ) {
481 let entity;
482 try { entity = domain (ref); } catch { return true ; }
483 if (entity.setupProducible || entity.childOf) return true ;
484 const writers = require ( '../resources/rp-target-wix/lib/wix-writers.js' );
485 return [entity.preferredWrite?.writerId, ... Object. keys (entity.operationContracts || {})]. some ( name => typeof writers[name] === 'function' ) || Boolean (writers. WRITE_BUILDERS_BY_ENTITY [ref]?. length ) || require ( './target-import-support' ). rawRequestEntity (ref);
486 }
487 function diagnostic ( code , dependent = null ) {
488 const [ name , ... rest ] = code. split ( ':' ), detail = rest. join ( ':' );
489 const next = name === 'dependency_unbound' && ! producible (detail) ? UNPRODUCIBLE_NEXT : NEXT_STEPS [name];
490 return { code: name, ... (dependent ? { dependent } : {}), detail, next: next || 'Correct the named dependency evidence, then re-run the preflight.' };
491 }
492 // The not-referenced proof a dependent's mapping already satisfies for `ref`, or null.
493 function notReferencedProof ( dependent , ref , context ) {
494 const operation = dependent?.operations?.[dependent.id], found = operationContract (dependent?.id, operation), dep = found?.contract.dependencies?.[ref];
495 if ( ! dep || dep.conditional !== true || ! Array. isArray (dep.guardedPaths) || ! dep.guardedPaths. length ) return null ;
496 const proof = { id: `${ dependent . id . split ( '/' )[ 1 ] }-${ ref . split ( '/' )[ 1 ] }` , dependentRef: dependent.id, operation: found.name, ref, reason: 'not-referenced' ,
497 explanation: `The approved ${ dependent . id } mapping writes none of ${ dep . guardedPaths . map ( guardPath ). join ( ', ' ) }.` ,
498 evidenceRefs: [ `mapping/mapping-plan.json#${ dependent . id }` ], operationContractDigest: contractDigest (found.contract), guardedPaths: dep.guardedPaths };
499 return validateProof (proof, { ... context, stage: 'mapping' }).ok ? proof : null ;
500 }
501 function preflightMapping ({ plan , reconciliation , dispositions = {}, readEntityMappings , projectDir = null }) {
502 const started = process.hrtime. bigint (), work = { predicateScans: 0 , recordsScanned: 0 , dependents: 0 , edges: 0 };
503 const rowsMap = new Map ((reconciliation?.rows || []). map ( row => [row.censusId, row]));
504 const byTarget = rowsByTarget (plan, readEntityMappings), errors = [], dependents = [], imports = [];
505 // The same plan-side evidence codegen reads: a target whose every row adopts what a quick pass made
506 // is an adopt-only dependent, so mapper and codegen draw the same edges for it.
507 const adoption = adoptOnlyRefsOf (plan, readEntityMappings);
508 for ( const [ targetRef , rows ] of byTarget) {
509 let setupProducible = false ;
510 try { setupProducible = Boolean ( domain (targetRef).setupProducible); } catch (error) { errors. push (error.message); continue ; }
511 const resolved = rows. map (resolveRowOperation), ops = [ ...new Set (resolved. map ( row => row.operation))];
512 for ( const row of resolved) errors. push ( ... row.errors);
513 if (ops. length > 1 ) errors. push ( `dependency_operation_mixed:${ targetRef }:${ ops . join ( ',' ) }` );
514 const producer = { id: targetRef, entityRefs: [targetRef], operations: ops. length === 1 && ops[ 0 ] ? { [targetRef]: ops[ 0 ] } : {}, ... (adoption.refs. includes (targetRef) ? { adoptOnly: true } : {}) };
515 // Whether setup or an import writes a mapped setup-producible target is decided at codegen, so
516 // here it stays pending setup as a producer; its own dependencies are still checked.
517 if ( ! setupProducible) imports. push (producer);
518 dependents. push (producer);
519 }
520 const setupFor = ref => {
521 const planned = require ( './setup-producers' ). plannedProducer (plan, ref, projectDir);
522 if (planned) return planned;
523 try { return domain (ref).setupProducible ? { kind: 'pending-setup' } : null ; } catch { return null ; }
524 };
525 const validated = validateProofs (plan?.dependencyProofs, { rows: rowsMap, plan, readEntityMappings, projectDir, stage: 'mapping' , work });
526 errors. push ( ... validated.errors);
527 const graph = resolveDependencyGraph ({ dependents, imports, setupFor, dispositions, proofs: validated.proofs, adoptOnlyRefs: adoption.refs, adoptOnlyMixed: adoption.mixed });
528 const taskErrors = new Set (Object. values (graph.tasks). flatMap ( task => task.errors));
529 errors. push ( ... graph.errors. filter ( error => ! taskErrors. has (error)));
530 work.dependents = dependents. length ; work.edges = Object. values (graph.tasks). reduce (( sum , task ) => sum + task.edges. length , 0 );
531 const diagnostics = [ ...new Set (errors)]. map ( code => diagnostic (code));
532 for ( const [ id , task ] of Object. entries (graph.tasks)) for ( const code of new Set (task.errors)) diagnostics. push ( diagnostic (code, id));
533 // A setup-producible ref with no planned setup step and no mapping row of its own stays
534 // `pending-setup` here, yet nothing will ever make it. When the dependent's reviewed contract makes
535 // the ref conditional and the mapping writes none of its guarded paths, codegen would find no
536 // producer (`dependency_unbound`): ask for the not-referenced proof now, ready to add.
537 for ( const [ id , task ] of Object. entries (graph.tasks)) for ( const edge of task.edges) {
538 if (edge.producer.kind !== 'pending-setup' || edge.producer.stepId || byTarget. has (edge.ref)) continue ;
539 const suggestedProof = notReferencedProof (dependents. find ( row => row.id === id), edge.ref, { plan, readEntityMappings, rows: rowsMap, projectDir });
540 if (suggestedProof) diagnostics. push ({ ... diagnostic ( `dependency_proof_expected:${ edge . ref }` , id), suggestedProof });
541 }
542 // The same proof for a conditional ref nothing can produce here (a site's share image with no
543 // media row): the blocking dependency_unbound row carries it, ready to add, so a mapping that
544 // writes none of the guarded paths is fixed by the proof rather than by planning a producer.
545 for ( const row of diagnostics) if (row.code === 'dependency_unbound' && row.dependent) {
546 let suggestedProof = null ;
547 try { suggestedProof = notReferencedProof (dependents. find ( dependent => dependent.id === row.dependent), row.detail, { plan, readEntityMappings, rows: rowsMap, projectDir }); } catch { suggestedProof = null ; }
548 if (suggestedProof) Object. assign (row, { suggestedProof, next: `The mapping writes none of this conditional reference: add the suggestedProof to mapping-plan.json dependencyProofs, or map the producer of ${ row . detail }.` });
549 }
550 const pendingSetup = [ ...new Set (Object. values (graph.tasks). flatMap ( task => task.edges. filter ( edge => edge.producer.kind === 'pending-setup' ). map ( edge => edge.ref)))]. sort ();
551 // A producer whose mapping rows still have an uninspected field inventory cannot run yet. When
552 // every row of that target is pending it is not a producer at all (`deferred-mapping`); when only
553 // some are, a dependent waits for the pending rows its retained writes name in
554 // requiredSourceEntityRefs (the same prerequisites execution scope uses). Held dependents are
555 // annotated too. Listed for the review, not a structural error, so an independent subset can
556 // still be approved provisionally.
557 const identities = row => [row.sourceEntityRef, row.sourceEntity, row.entityKey]. filter (Boolean);
558 const canonicalId = row => row.sourceEntityRef || row.sourceEntity || row.entityKey;
559 const pendingRows = new Map ();
560 for ( const [ targetRef , rows ] of byTarget) {
561 const pending = rows. filter ( row => row.fieldMappingStatus === 'pending' );
562 if (pending. length ) pendingRows. set (targetRef, { all: pending. length === rows. length , rows: pending });
563 }
564 const waiting = [];
565 for ( const [ id , task ] of Object. entries (graph.tasks)) {
566 const required = new Set ((byTarget. get (id) || []). flatMap ( row => row.requiredSourceEntityRefs || []));
567 for ( const edge of task.edges) {
568 const held = edge.producer.kind === 'import-task' && pendingRows. get (edge.producer.taskId);
569 if ( ! held) continue ;
570 const rows = held.all ? held.rows : held.rows. filter ( row => identities (row). some ( name => required. has (name)));
571 if ( ! rows. length ) continue ;
572 const sourceEntityRefs = [ ...new Set (rows. map (canonicalId))]. sort ();
573 if (held.all) edge.producer = { kind: 'deferred-mapping' , taskId: edge.producer.taskId, sourceEntityRefs };
574 else edge.pendingSourceEntityRefs = sourceEntityRefs;
575 waiting. push ({ dependent: id, ref: edge.ref, mode: edge.mode, sourceEntityRefs, producerPartlyPending: ! held.all,
576 next: 'Finish the field mapping of the named source entities (for a plugin entity, its full read comes first: resources/rp-source-wordpress/scripts/plugin-full-read.js), or record an approved exclusion. Until then this dependent waits.' });
577 }
578 }
579 return {
580 schemaVersion: 1 , stage: 'mapping' , structuralReady: diagnostics. length === 0 , executionReady: false ,
581 errors: diagnostics, pendingSetup, waiting,
582 tasks: Object. fromEntries (Object. entries (graph.tasks). map (([ id , task ]) => [id, { operation: dependents. find ( row => row.id === id)?.operations?.[id] || null , edges: task.edges }])),
583 proofs: [ ... validated.proofs. values ()]. map ( proof => ({ id: proof.id, dependentRef: proof.dependentRef, operation: proof.operation, ref: proof.ref, reason: proof.reason, status: proof.status })),
584 work: { ... work, elapsedMs: Number ((process.hrtime. bigint () - started) / 1000000 n ) },
585 };
586 }
587
588 module . exports = {
589 REASONS, canonical, domain, bundledDomain, useTargetKnowledgeRoot, contractDigest, operationContract, applicableDependsOn, inapplicableRefs, guardHits, guardPath, valuesAt,
590 validatePredicate, predicateMatches, resolveRowOperation, validateProof, validateProofs, proofKey, dependencyClosure, adoptOnlyRefsOf,
591 resolveDependencyGraph, preflightMapping, pathsOverlap, scanTotals : () => ({ ... scanTotals }),
592 };
593 for ( const name of [ 'preflightMapping' , 'resolveDependencyGraph' ]) {
594 const run = module . exports [name];
595 module . exports [name] = options => options.plan ? assessments. withPlan (options.plan, options.projectDir, () => run (options)) : run (options);
596 }
597 for ( const name of [ 'validateProof' , 'validateProofs' ]) {
598 const run = module . exports [name];
599 module . exports [name] = ( proof , options = {}) => options.plan ? assessments. withPlan (options.plan, options.projectDir, () => run (proof, options)) : run (proof, options);
600 }