Setting the file. One moment.
Mapping Recovery · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page function verifiedIds
— line 140
This file
Number 45.100
Position 100 of 182
Type JavaScript
Size 22 KB
Lines 272 lib/ mapping-recovery.js
JavaScript · 272 lines · 22 KB
(
'./operation-assessments'
);
8 const LEDGER = 'mapping/review/mapping-gaps.json' ;
9 const validatedSources = new WeakMap ();
10 const fail = code => { throw Object. assign ( new Error (code), { code }); };
11 const read = ( root , file , fallback ) => {
12 try { return require ( './cached-json' ). readJson (path. join (root, file), fallback); }
13 catch (e) { if (e.code === 'ENOENT' ) return fallback; throw e; }
14 };
15 function shape ( row , d ) {
16 const r = d?.recovery, errors = [];
17 if (d?.disposition !== 'deferred-field' && ! (d?.disposition === 'unresolved' && r)) return errors;
18 const held = d.disposition === 'unresolved' && r?.scope === 'record' ;
19 if ( ! r || (r.scope !== 'field' && ! held) || ! r.source || ! r.membership || ! Array. isArray (r.evidence) || ! r.evidence. length ) return [ 'deferral needs recoverable source, membership and evidence' ];
20 if ( ! held) {
21 if ( ! r.createWithoutField || ! r.correction) return [ 'deferred field needs eligibility and correction evidence' ];
22 if (r.createWithoutField.optional !== true || r.createWithoutField.noMaterialBehaviorChange !== true || ! r.createWithoutField.reason) errors. push ( 'deferral requires optionality and unchanged material behavior' );
23 const create = r.createWithoutField;
24 if ( ! create.operation || ! create.targetRef || ! create.writeInput || ! Array. isArray (create.paths) || ! create.paths. length || create.paths. some ( p => ! L . isExact (p))) errors. push ( 'deferral requires exact withheld create fields' );
25 if (r.correction.mutable !== true || ! r.correction.operation || ! r.correction.targetRef || ! r.correction.writeInput ||
26 ! Array. isArray (r.correction.paths) || ! r.correction.paths. length || r.correction.paths. some ( p => ! L . isExact (p))) errors. push ( 'deferral requires a supported exact correction path' );
27 }
28 if ( ! row.discoveryCoverage?.sourceSnapshotDigest || r.sourceSnapshotDigest !== row.discoveryCoverage.sourceSnapshotDigest ||
29 r.inventoryDigest !== row.discoveryCoverage.inventoryDigest) errors. push ( 'deferral source scope is stale' );
30 if ( ! d.affectedScope?.sourceIdsComplete || d.affectedScope.countStatus !== 'known' || ! Array. isArray (d.affectedScope.sourceIds)) errors. push ( 'deferral needs complete measured affected membership' );
31 if ( ! d.nextAction?.cta || d.nextAction.requiresImportOperator !== true || ! d.nextAction.prompt) errors. push ( 'deferral requires an actionable Import Operator handoff' );
32 if (d.sourcePaths?. some ( p => /password | credential | secret | access_token | refresh_token/ i . test (p))) errors. push ( 'restricted source data cannot be retained for deferred repair' );
33 if (row.discoveryCoverage?.paths?. some ( p => p?.retentionClass === 'credential' && d.sourcePaths. some ( s => L . within (s,p.path) || L . within (p.path,s)))) errors. push ( 'restricted source data cannot be retained for deferred repair' );
34 return errors;
35 }
36 function validate ( projectDir , row , d ) {
37 const errors = shape (row, d);
38 if (errors. length ) fail ( 'mapping_recovery_invalid' );
39 if ( ! d.recovery) return ;
40 const r = d.recovery;
41 const bytes = A . artifact (projectDir, r.source), memberBytes = A . artifact (projectDir, r.membership);
42 for ( const e of r.evidence) A . artifact (projectDir, e);
43 const cacheKey = A . digest ([r,d.affectedScope,d.sourcePaths]);
44 if (validatedSources. get (bytes)?. has (cacheKey)) return validatedSources. get (bytes). get (cacheKey);
45 const records = bytes. toString ( 'utf8' ). split ( / \r ? \n / ). filter (Boolean). map ( JSON .parse);
46 const membership = JSON . parse (memberBytes);
47 const ids = d.affectedScope.sourceIds. map (String). sort ();
48 if ( ! Array. isArray (membership) || membership. some ( m => typeof m.sourceId !== 'string' || typeof m.sourceStableKey !== 'string' || ! m.sourceStableKey) ||
49 A . digest (membership. map ( m => m.sourceId). sort ()) !== A . digest (ids) || new Set (membership. map ( m => m.sourceStableKey)).size !== ids. length ||
50 ids. length !== d.affectedScope.count || new Set (ids).size !== ids. length ) fail ( 'mapping_recovery_membership_invalid' );
51 if ( ! r.sourceIdPath || ! L . isExact (r.sourceIdPath)) fail ( 'mapping_recovery_identity_missing' );
52 const byId = new Map ();
53 for ( const record of records) {
54 const values = L . valuesAt (record, r.sourceIdPath);
55 // Source identity is deliberately scalar; never infer identity from array position.
56 if (values. length !== 1 || ! [ 'string' , 'number' ]. includes ( typeof values[ 0 ])) fail ( 'mapping_recovery_identity_ambiguous' );
57 const id = values[ 0 ];
58 if (id !== undefined ) {
59 if (byId. has ( String (id))) fail ( 'mapping_recovery_identity_ambiguous' );
60 byId. set ( String (id), record);
61 }
62 }
63 if (ids. some ( id => ! byId. has (id))) fail ( 'mapping_recovery_source_missing' );
64 if (ids. some ( id => d.sourcePaths. some ( p => ! L . valuesAt (byId. get (id), p). length ))) fail ( 'mapping_recovery_source_field_missing' );
65 for ( const e of r.evidence) A . artifact (projectDir, e);
66 if (r.scope === 'field' && ! r.evidence. some ( e => e.kind === 'authoritative-schema' && e.revision)) fail ( 'mapping_recovery_schema_missing' );
67 const result = { ids, byId, membership };
68 if ( ! validatedSources. has (bytes)) validatedSources. set (bytes, new Map ());
69 validatedSources. get (bytes). set (cacheKey, result);
70 return result;
71 }
72 function validatePlan ( projectDir , plan , sourceEntityRefs = null ) {
73 const fields = require ( './mapping-field-contract' );
74 for ( const row of fields. entityRows (plan). filter ( r =>! sourceEntityRefs || sourceEntityRefs. includes (fields. entityRef (r)))) for ( const d of row.fieldMappings || []) if (d.recovery) validate (projectDir, row, d);
75 }
76 function ledger ( projectDir ) {
77 const value = read (projectDir, LEDGER , []);
78 const gaps = Array. isArray (value) ? value : value?.gaps;
79 if ( ! Array. isArray (gaps)) fail ( 'mapping_recovery_ledger_invalid' );
80 return { value, gaps };
81 }
82 function assertPersisted ( projectDir , plan , sourceEntityRefs = null ) {
83 validatePlan (projectDir, plan, sourceEntityRefs);
84 const fields = require ( './mapping-field-contract' );
85 const { gaps } = ledger (projectDir);
86 for ( const ref of (plan.recoveryGapRefs || []). filter ( r =>! sourceEntityRefs || sourceEntityRefs. includes (r.sourceEntity))) {
87 const g = gaps. find ( g => g.id === ref.id);
88 if ( ! g || A . digest ([g.recovery,g.affectedScope]) !== ref.recoveryDigest) fail ( 'mapping_recovery_gap_not_persisted' );
89 }
90 for ( const row of fields. entityRows (plan). filter ( r =>! sourceEntityRefs || sourceEntityRefs. includes (fields. entityRef (r)))) for ( const expected of fields. gapRows (row). filter ( g => g.recovery)) {
91 const stored = gaps. find ( g => g.id === expected.id);
92 if ( ! stored || A . digest (stored.recovery) !== A . digest (expected.recovery) || A . digest (stored.affectedScope) !== A . digest (expected.affectedScope)) fail ( 'mapping_recovery_gap_not_persisted' );
93 }
94 }
95 function assertPayload ( projectDir , plan , targetRefs , payload , { writeInput = null , sourceStableKey } = {}) {
96 const fields = require ( './mapping-field-contract' );
97 const relevant = fields. entityRows (plan). filter ( r => targetRefs. includes (r.targetRef) || r.fieldMappings?. some ( d => targetRefs. includes (d.recovery?.createWithoutField?.targetRef))). map (fields.entityRef);
98 assertPersisted (projectDir, plan, relevant);
99 for ( const row of require ( './mapping-field-contract' ). entityRows (plan)) for ( const d of row.fieldMappings || []) {
100 if (d.disposition === 'unresolved' && d.recovery?.scope === 'record' && targetRefs. includes (row.targetRef)) {
101 const members = JSON . parse ( A . artifact (projectDir,d.recovery.membership));
102 if ( ! sourceStableKey || members. some ( m => m.sourceStableKey === sourceStableKey)) fail ( 'mapping_recovery_record_held' );
103 }
104 if (d.disposition !== 'deferred-field' || ! targetRefs. includes (d.recovery.createWithoutField.targetRef)) continue ;
105 const members = JSON . parse ( A . artifact (projectDir, d.recovery.membership));
106 if ( ! sourceStableKey) fail ( 'mapping_recovery_identity_required' );
107 if ( ! members. some ( m => m.sourceStableKey === sourceStableKey)) continue ;
108 const input = d.recovery.createWithoutField.writeInput;
109 const body = writeInput ? payload?.[writeInput] : payload && Object. hasOwn (payload,input) ? payload[input] : payload;
110 for ( const p of d.recovery.createWithoutField.paths) {
111 const relative = p. startsWith (input + '.' ) ? p. slice (input. length + 1 ) : p;
112 if ( L . valuesAt (body, relative). some ( v => v !== undefined )) fail ( 'mapping_recovery_deferred_field_written' );
113 }
114 }
115 }
116 function permitsDisposition ( projectDir , plan , row , disposition ) {
117 if (disposition.status !== 'deferred' || ! disposition.decisionRefs?. length ) return false ;
118 try {
119 assertPersisted (projectDir, plan, [ require ( './mapping-field-contract' ). entityRef (row)]);
120 const expected = require ( './mapping-field-contract' ). projectEntity (row).fieldDispositions. find ( d => d.kind === disposition.kind && d.path === disposition.path && d.writeOperation === disposition.writeOperation);
121 return expected?.status === 'deferred' && A . digest (expected) === A . digest (disposition);
122 } catch { return false ; }
123 }
124 function pendingSummary ( projectDir ) {
125 const { gaps } = ledger (projectDir);
126 return gaps. filter ( g => g.recovery). map ( g => ({
127 gapId: g.id, entity: g.sourceEntity, decisionId: g.decisionId, affectedScope: g.affectedScope,
128 source: g.recovery.source, correction: g.recovery.correction, scope:g.recovery.scope,
129 status: 'pending' , nextAction: g.nextAction, membership: g.recovery.membership,
130 resolvedSourceIds: verifiedIds (projectDir, g),
131 })). filter ( g => g.affectedScope.sourceIds. some ( id => ! g.resolvedSourceIds. includes ( String (id))));
132 }
133 // The ledger is a projection. Only append-only, source-bound repair evidence closes membership.
134 function decisionDigest ( row , decision ) {
135 const { id , sourcePaths , selectors , disposition , transform , valuePolicy , referencePolicy , outputs , affectedScope , affectedScopes } = decision;
136 return A . digest ({ targetRef: row.targetRef, sourceSnapshotDigest: row.discoveryCoverage?.sourceSnapshotDigest,
137 inventoryDigest: row.discoveryCoverage?.inventoryDigest,
138 id, sourcePaths, selectors, disposition, transform, valuePolicy, referencePolicy, outputs, affectedScope, affectedScopes });
139 }
140 function verifiedIds ( projectDir , gap ) {
141 try { A . artifact (projectDir,gap.recovery.source); A . artifact (projectDir,gap.recovery.membership); for ( const e of gap.recovery.evidence) A . artifact (projectDir,e); }
142 catch { return []; }
143 const journal = require ( './local-state' ). attemptJournalPath (projectDir);
144 let lines;
145 try { lines = fs. readFileSync (journal, 'utf8' ); } catch (e) { if (e.code === 'ENOENT' ) return []; throw e; }
146 const fields = require ( './mapping-field-contract' ), plan = read (projectDir, 'mapping/mapping-plan.json' ,{});
147 const row = fields. entityRows (plan). find ( r => fields. entityRef (r) === gap.sourceEntity);
148 const decision = row?.fieldMappings?. find ( d => d.id === gap.decisionId || d.supersedes?. includes (gap.decisionId));
149 const required = gap.recovery.scope === 'field' ? gap.recovery.correction.paths : decision?.outputs?. map ( o => o.targetPath);
150 if ( ! required?. length || ! decision?.outputs?. length ) return [];
151 let journalRows; try {journalRows = lines. split ( / \r ? \n / ). filter (Boolean). map ( JSON .parse);} catch { return [];}
152 const verified = journalRows. filter ( r => r.status === 'imported' &&
153 r.mappingRepair?.gapId === gap.id && r.mappingRepair.sourceDigest === gap.recovery.source.sha256 &&
154 r.mappingRepair.membershipDigest === gap.recovery.membership.sha256 && r.mappingRepair.observedDigest &&
155 (r.mappingRepair.decisionDigest ? r.mappingRepair.decisionDigest === decisionDigest (row, decision) :
156 r.mappingRepair.mappingPlanDigest === fields. digest (plan)) && r.targetId);
157 return [ ...new Set (verified. map ( r => r.mappingRepair.sourceId))]. filter ( id => required. every ( p => verified. some ( r => r.mappingRepair.sourceId === id && r.mappingRepair.targetPath === p)));
158 }
159 async function joinReport ( projectDir , report ) {
160 const plan = read (projectDir, 'mapping/mapping-plan.json' ,{}), saved = ledger (projectDir);
161 const missing = (plan.recoveryGapRefs || []). filter ( ref =>! saved.gaps. some ( g => g.id === ref.id && A . digest ([g.recovery,g.affectedScope]) === ref.recoveryDigest));
162 if (missing. length ) return { ... report,status:report.status === 'aborted' ? 'aborted' : 'blocked' ,mappingRecovery:{unknownCounts: true ,missingGapIds:missing. map ( g => g.id)},
163 operatorActions:[ ... (report.operatorActions || []),{code: 'mapping-recovery-ledger-missing' ,severity: 'high' ,action: 'Requires Import Operator: restore the pinned mapping gap ledger from the original migration artifacts before continuing or declaring completion.' ,deadlineReason: 'Before continuing import or launch.' ,affectedCount: null ,artifactRefs:[ LEDGER ]}]};
164 const pending = pendingSummary (projectDir);
165 if ( ! pending. length ) {
166 if ( ! plan.recoveryGapRefs?. length ) return report;
167 const { pendingFields : _pending , mappingRecovery : _recovery , ... rest } = report;
168 return { ... rest,operatorActions:(report.operatorActions || []). filter ( a => a.code !== 'mapping-field-pending' )};
169 }
170 const actions = (report.operatorActions || []). filter ( a => a.code !== 'mapping-field-pending' );
171 for ( const g of pending) actions. push ({ code: 'mapping-field-pending' , entity: g.entity, severity: 'high' ,
172 reason: g.scope === 'record' ? 'Records remain pending a mapping decision; retained source data has not been accepted as loss.' : 'Imported fields do not complete this record: retained source fields still need a mapping decision and verified repair.' ,
173 action: `Requires Import Operator: ${ g . nextAction ?. prompt || 'Resolve the pending mapping and repair existing target identities.'}` ,
174 affectedCount: g.affectedScope.sourceIds. length - g.resolvedSourceIds. length , deadlineReason: 'Before launch or relying on the pending field.' ,
175 actionId: g.gapId, role: 'import-operator' , importOperatorPrompt: g.nextAction.prompt,
176 affectedIds: g.affectedScope.sourceIds. filter ( id => ! g.resolvedSourceIds. includes ( String (id))), artifactRefs: [ LEDGER , g.source.artifact],
177 prerequisites: [ 'Current retained source' , 'Approved revised mapping' , 'Verified target identity and current target state' ],
178 resolutionEvidence: 'Read-back of the repaired field bound to source, target and mapping decision.' });
179 const local = require ( './local-state' );
180 let unknownCounts = false ;
181 const crosswalk = await local. loadCrosswalk (projectDir). catch (() => {unknownCounts = true ; return {bySource:{}};});
182 const journal = await local. loadAttemptJournal (projectDir). catch (() => {unknownCounts = true ; return {rows:[]};});
183 const attemptsByKey = new Map ();
184 for ( const r of journal.rows) { if ( ! attemptsByKey. has (r.sourceStableKey))attemptsByKey. set (r.sourceStableKey,[]);attemptsByKey. get (r.sourceStableKey). push (r);}
185 const records = new Map ();
186 for ( const g of pending) {
187 let members;
188 try { A . artifact (projectDir,g.source);members = JSON . parse ( A . artifact (projectDir,g.membership));}
189 catch {unknownCounts = true ;members = g.affectedScope.sourceIds. map ( sourceId => ({sourceId,sourceStableKey: null }));}
190 for ( const m of members) {
191 if (g.resolvedSourceIds. includes (m.sourceId)) continue ;
192 const cw = crosswalk.bySource[m.sourceStableKey];
193 const attempts = attemptsByKey. get (m.sourceStableKey) || [];
194 const status = ! m.sourceStableKey || unknownCounts ? 'needs-reconciliation' : cw?.targetId ? 'landed-with-pending-fields' : attempts. some ( r => ! [ 'deferred' , 'skipped_safe_mode_blocked' , 'skipped_out_of_scope' , 'skipped_unexpected' ]. includes (r.status)) ? 'needs-reconciliation' : 'not-yet-imported' ;
195 records. set (m.sourceStableKey || `${ g . entity }:${ m . sourceId }` , { sourceStableKey: m.sourceStableKey, status, targetId: cw?.targetId || null });
196 }
197 }
198 const counts = Object. fromEntries ([ 'landed-with-pending-fields' , 'not-yet-imported' , 'needs-reconciliation' ]. map ( s => [s, [ ... records. values ()]. filter ( r => r.status === s). length ]));
199 // Crosswalk identity proves a parent landed; it does not prove full fidelity. Use the
200 // existing completeness totals as the base and keep a null when that base is unavailable.
201 const landed = report.entityCompleteness?. reduce (( n , r ) => n + (r.imported || 0 ) + (r.alreadyPresentByCrosswalk || 0 ), 0 );
202 const fullyImported = Number. isSafeInteger (landed) && landed >= counts[ 'landed-with-pending-fields' ] && ! unknownCounts && ! counts[ 'needs-reconciliation' ] ? landed - counts[ 'landed-with-pending-fields' ] : null ;
203 return { ... report, status: report.status === 'aborted' ? 'aborted' : 'blocked' , pendingFields: pending,
204 mappingRecovery: { counts:{ ... counts,fullyImported}, records: [ ... records. values ()], unknownCounts }, operatorActions: actions };
205 }
206 // Repair is ordinary generated execution: the mapper supplies the revised exact binding and
207 // caller uses the existing guarded writer. No callback chooses a mapping or a target identity.
208 async function repair ({ projectDir , gapId , sourceId , targetId , mappingPlanDigest , outputId , expected ,
209 readTarget , writeTarget , verify , runId , taskId , siteId }) {
210 const fields = require ( './mapping-field-contract' );
211 const plan = read (projectDir, 'mapping/mapping-plan.json' );
212 if ( ! plan || fields. digest (plan) !== mappingPlanDigest) fail ( 'mapping_repair_mapping_stale' );
213 const { gaps } = ledger (projectDir), gap = gaps. find ( g => g.id === gapId);
214 if ( ! gap?.recovery || ! gap.affectedScope?.sourceIds?. map (String). includes ( String (sourceId))) fail ( 'mapping_repair_scope_invalid' );
215 const row = fields. entityRows (plan). find ( r => fields. entityRef (r) === gap.sourceEntity);
216 fields. assertExecutionScope (plan, gap.sourceEntity, mappingPlanDigest, projectDir);
217 const approval = read (projectDir, 'orchestration/approvals.json' ,{}).mapping;
218 if (approval?.status !== 'approved' || approval.decidedBy !== 'user' || !await require ( './census-reconciliation' ). mappingApprovalBound (projectDir,approval)) fail ( 'mapping_repair_approval_required' );
219 const d = row.fieldMappings. find ( d => d.id === gap.decisionId || d.supersedes?. includes (gap.decisionId));
220 const output = d?.outputs?. find ( o => o.id === outputId);
221 const fieldRepair = gap.recovery.scope === 'field' ;
222 if ( ! output || [ 'unresolved' , 'deferred-field' , 'excluded' ]. includes (d.disposition) || fieldRepair && (output.writeOperation !== gap.recovery.correction.operation ||
223 output.targetRef !== gap.recovery.correction.targetRef || ! gap.recovery.correction.paths. includes (output.targetPath))) fail ( 'mapping_repair_requires_mapping' );
224 // Actual target identity must come from the journal/crosswalk adapter, not a caller-made ID.
225 if ( ! targetId || ! runId || ! taskId || ! siteId || typeof verify !== 'function' ) fail ( 'mapping_repair_identity_required' );
226 const local = require ( './local-state' );
227 const member = JSON . parse ( A . artifact (projectDir, gap.recovery.membership)). find ( m => m.sourceId === String (sourceId));
228 const crosswalk = await local. loadCrosswalk (projectDir);
229 const identity = member && crosswalk.bySource[member.sourceStableKey];
230 if ( ! identity || identity.targetId !== targetId || identity.targetEntityType !== output.targetRef || identity.siteId !== siteId) fail ( 'mapping_repair_identity_conflict' );
231 if (row.discoveryCoverage.sourceSnapshotDigest !== gap.recovery.sourceSnapshotDigest || row.discoveryCoverage.inventoryDigest !== gap.recovery.inventoryDigest) fail ( 'mapping_repair_source_stale' );
232 const attemptId = crypto. randomUUID ();
233 const mappingRepair = { gapId, sourceId: String (sourceId), mappingPlanDigest, decisionDigest: decisionDigest (row,d), outputId, targetPath:output.targetPath,
234 sourceDigest: gap.recovery.source.sha256, membershipDigest: gap.recovery.membership.sha256 };
235 const recordAttempt = ( status , extra = {}) => local. appendAttempt (projectDir, {schemaVersion: 1 , attemptId,
236 sourceStableKey:member.sourceStableKey, writeSpecId:taskId, operation: 'update' , targetEntityType:output.targetRef,
237 targetId, siteId, runId, phase: 'import' , ts: new Date (). toISOString (), status, mappingRepair:{ ... mappingRepair, ... extra}});
238 const retained = validate (projectDir,row,{disposition:fieldRepair ? 'deferred-field' : 'unresolved' ,sourcePaths:gap.sourcePaths,
239 recovery:gap.recovery,affectedScope:gap.affectedScope,nextAction:gap.nextAction});
240 const sourceRecord = retained.byId. get ( String (sourceId));
241 if ( ! sourceRecord) fail ( 'mapping_repair_source_missing' );
242 if (d.affectedScope?.sourceIdsComplete && ! d.affectedScope.sourceIds?. map (String). includes ( String (sourceId))) fail ( 'mapping_repair_scope_invalid' );
243 if (output.appliesWhen && ! require ( './output-applicability' ). evaluate (output.appliesWhen,sourceRecord)) fail ( 'mapping_repair_scope_invalid' );
244 const actual = await readTarget ({ targetId, output });
245 if ( ! fieldRepair && A . digest (actual) !== A . digest (expected)) fail ( 'mapping_repair_created_record_unverified' );
246 const journal = await local. loadAttemptJournal (projectDir);
247 const prior = journal.rows. filter ( r => r.status === 'imported' && r.targetId === targetId && r.siteId === siteId &&
248 r.targetEntityType === output.targetRef && r.sourceStableKey === member.sourceStableKey &&
249 r.mappingRepair?.gapId === gapId && r.mappingRepair.sourceId === String (sourceId) &&
250 r.mappingRepair.sourceDigest === gap.recovery.source.sha256 && r.mappingRepair.membershipDigest === gap.recovery.membership.sha256 &&
251 r.mappingRepair.targetPath === output.targetPath && r.mappingRepair.observedDigest). at ( - 1 );
252 // The initial supported state is absence: this field was deliberately withheld. Later
253 // baselines come only from verified journal evidence, never a caller's claimed value.
254 const previousDigest = prior?.mappingRepair.observedDigest || A . digest ( undefined );
255 if ( A . digest (actual) !== previousDigest && A . digest (actual) !== A . digest (expected)) fail ( 'mapping_repair_target_conflict' );
256 if ( A . digest (actual) !== A . digest (expected)) {
257 await recordAttempt ( 'started' );
258 await writeTarget ({ targetId, output, expected });
259 }
260 const observed = await readTarget ({ targetId, output });
261 if ( ! ( await verify ({ expected, observed, output }))) fail ( 'mapping_repair_unverified' );
262 await recordAttempt ( 'imported' , {observedDigest: A . digest (observed)});
263 let telemetryPending = false ;
264 try {
265 const recorder = require ( '../resources/rp-telemetry/lib/telemetry-recorder' );
266 await recorder. record (projectDir, require ( '../resources/rp-telemetry/lib/knowledge-observation' ). event ({findingClass: 'missing-knowledge' ,
267 targetRef:output.targetRef,operation:output.writeOperation,toolkitVersion:recorder. resolveSkillsVersion () || 'unknown' ,
268 outcome: 'verified-repair' ,verificationLevel: 'read-back' ,affectedCount: 1 }));
269 } catch { telemetryPending = true ; }
270 return { verified: true , targetId, gapId, sourceId: String (sourceId), mappingPlanDigest, outputId, observedDigest: A . digest (observed), telemetryPending };
271 }
272 module . exports = { shape, validate, validatePlan, assertPersisted, assertPayload, permitsDisposition, pendingSummary, joinReport, repair, verifiedIds };