Setting the file. One moment.
Link Plan · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page lib/ link-plan.js
JavaScript · 148 lines · 9 KB
=
require
(
'./app-page-routing.js'
);
7
8 function stableId ( prefix , values ) {
9 return `${ prefix }-${ createHash ( 'sha256' ). update ( values . join ( ' \0 ' )). digest ( 'hex' ). slice ( 0 , 16 ) }` ;
10 }
11
12 function lookupKey ( path ) {
13 const hash = path. indexOf ( '#' );
14 return hash < 0 ? path : path. slice ( 0 , hash);
15 }
16
17 function pathParts ( value ) {
18 const url = new URL (value, 'https://link-audit.invalid' );
19 return { pathname: url.pathname, query: url.search, fragment: url.hash };
20 }
21
22 // Target rows come from a confirmed source identity -> target identity crosswalk and route
23 // read-back. Two records claiming the same old path are never silently collapsed.
24 function buildLinkPlan ({ census , targets , samples = [], routes = [], ownershipDecisions = {}, activeWriters = [], previous = null , runId , sourceSnapshot , routeRevision , deploymentId = null , budgetDecision = null }) {
25 if ( ! runId || ! sourceSnapshot || ! routeRevision) throw new Error ( 'runId, sourceSnapshot and routeRevision required' );
26 const index = new Map ();
27 for ( const target of targets) {
28 if ( ! target.sourcePath || ! target.targetRecordId || ! target.pageFamily) throw new Error ( 'target identity and sourcePath required' );
29 if (target.expectedPath) {
30 const derived = target.readBackPattern && target.readBackSlug
31 ? pathFromPattern (target.readBackPattern, target.readBackSlug)
32 : target.fixedPageReadBackPath;
33 if ( ! derived || derived !== target.expectedPath) throw new Error ( `expected path for ${ target . targetRecordId } lacks matching route and slug read-back` );
34 }
35 const key = lookupKey (target.sourcePath);
36 if ( ! index. has (key)) index. set (key, []);
37 index. get (key). push (target);
38 }
39 const pendingByTarget = new Map ();
40 const unresolved = [];
41 const outcomes = [];
42 const writerIntegrations = new Map ();
43 const recordWriter = ( occurrence ) => {
44 const field = `${ occurrence . recordType || 'unknown'}:${ occurrence . field }` ;
45 const guard = writerGuard (field, activeWriters, occurrence.recordType);
46 if ( ! writerIntegrations. has (field)) writerIntegrations. set (field, { field, status: guard.ok ? 'available' : 'pending' , reason: guard.ok ? null : guard.reason, occurrenceIds: [] });
47 writerIntegrations. get (field).occurrenceIds. push (occurrence.id);
48 };
49 for ( const occurrence of census.occurrences) {
50 if (occurrence.fragmentOnly) {
51 unresolved. push ({ id: stableId ( 'unresolved' , [occurrence.id]), occurrenceIds: [occurrence.id], reason: 'unsupported-family' , detail: 'fragment-anchor-unverified' , originalPath: occurrence.originalPath, candidateTargetIds: [] });
52 continue ;
53 }
54 if (occurrence.classification !== 'internal-navigation' ) {
55 unresolved. push ({ id: stableId ( 'unresolved' , [occurrence.id]), occurrenceIds: [occurrence.id], reason: occurrence.classification === 'stateful-navigation' ? 'unsupported-family' : 'ambiguous-target' , detail: occurrence.classification, originalPath: occurrence.originalPath || occurrence.original || '' , candidateTargetIds: [] });
56 recordWriter (occurrence);
57 continue ;
58 }
59 const parts = pathParts (occurrence.originalPath);
60 const exact = index. get ( lookupKey (occurrence.originalPath)) || [];
61 // A query permalink is resolved by its complete source identity. Falling back
62 // to the path requires explicit per-target evidence for each query key.
63 const queryKeys = [ ...new URLSearchParams (parts.query). keys ()];
64 const matches = exact. length ? exact : (index. get (parts.pathname) || []). filter (( target ) => ! queryKeys. length || queryKeys. every (( key ) => (target.supportedQueryKeys || []). includes (key)));
65 let unique = [ ...new Map (matches. map (( target ) => [ `${ target . targetRecordId }:${ target . expectedPath || ''}` , target])). values ()];
66 const decision = ownershipDecisions[ lookupKey (occurrence.originalPath)];
67 if (unique. length > 1 && decision && decision.targetRecordId && decision.decisionRef) {
68 const chosen = unique. filter (( target ) => target.targetRecordId === decision.targetRecordId);
69 if (chosen. length === 1 ) unique = chosen;
70 }
71 let reason;
72 if (queryKeys. length && ! exact. length && ! matches. length && index. has (parts.pathname)) reason = 'unsupported-family' ;
73 else if (occurrence.sourceBroken === true ) reason = 'source-broken' ;
74 else if (occurrence.ambiguousTarget === true ) reason = 'ambiguous-target' ;
75 else if ( ! unique. length ) reason = 'target-not-imported' ;
76 else if (unique. length > 1 ) reason = 'collision' ;
77 else if ( ! unique[ 0 ].expectedPath) reason = 'unsupported-family' ;
78 if (reason) {
79 unresolved. push ({ id: stableId ( 'unresolved' , [occurrence.id]), occurrenceIds: [occurrence.id], reason, originalPath: occurrence.originalPath, candidateTargetIds: unique. map (( target ) => target.targetRecordId) });
80 recordWriter (occurrence);
81 continue ;
82 }
83 const target = unique[ 0 ];
84 const key = stableId ( 'destination' , [target.targetRecordId, target.expectedPath, target.pageFamily]);
85 if ( ! pendingByTarget. has (key)) pendingByTarget. set (key, {
86 id: key, targetRecordId: target.targetRecordId, expectedPath: target.expectedPath,
87 pageFamily: target.pageFamily, occurrenceIds: [], status: 'pending' ,
88 ... (decision && unique. length === 1 ? { ownershipDecisionRef: decision.decisionRef } : {}),
89 });
90 pendingByTarget. get (key).occurrenceIds. push (occurrence.id);
91 recordWriter (occurrence);
92 }
93 const audit = {
94 schemaVersion: 1 , censusVersion: 4 , runId, sourceSnapshot, routeRevision, deploymentId, stage: 'pending-frontend' ,
95 manifest: census.manifest, counts: census.counts, occurrences: census.occurrences,
96 pendingDestinations: [ ... pendingByTarget. values ()], unresolved, outcomes,
97 writerIntegrations: [ ... writerIntegrations. values ()], samples, routes, postDnsStatus: previous && previous.postDnsStatus || 'pending' ,
98 verification: { deploymentId: null , checkedAt: null },
99 };
100 if (previous && previous.runId === runId) {
101 if (previous.sourceSnapshot === sourceSnapshot && previous.routeRevision === routeRevision) {
102 audit.priorWriteIntents = { ... (previous.priorWriteIntents || {}) };
103 for ( const outcome of previous.outcomes || []) if (outcome.writeIntentKey) audit.priorWriteIntents[outcome.occurrenceId] = outcome.writeIntentKey;
104 }
105 audit.liveElapsedMs = previous.liveElapsedMs || 0 ;
106 const sameVerificationInputs = previous.sourceSnapshot === sourceSnapshot && previous.routeRevision === routeRevision && previous.deploymentId === deploymentId;
107 if (sameVerificationInputs && previous.calibration) {
108 const currentDestinationIds = new Set (audit.pendingDestinations. map (( row ) => row.id));
109 audit.calibration = {
110 ... previous.calibration,
111 results: Object. fromEntries (Object. entries (previous.calibration.results || {}). filter (([ id ]) => currentDestinationIds. has (id))),
112 };
113 }
114 if (previous.estimate) audit.estimate = previous.estimate;
115 if (sameVerificationInputs && previous.sampleResults) audit.sampleResults = previous.sampleResults;
116 if (previous.budgetDecision) audit.budgetDecision = previous.budgetDecision;
117 if (previous.stage === 'budget-hold' && ! budgetDecision) audit.stage = 'budget-hold' ;
118 }
119 if (budgetDecision) {
120 if ( ! previous || previous.runId !== runId || previous.stage !== 'budget-hold' || ! budgetDecision.decisionRef || ! Number. isFinite (budgetDecision.maxLiveMs) || budgetDecision.maxLiveMs < 30 * 60 * 1000 || ! Number. isFinite (budgetDecision.maxExceptionTokens) || budgetDecision.maxExceptionTokens < 100000 ) {
121 throw new Error ( 'budget resume requires a recorded decision and explicit approved ceilings for the same held run' );
122 }
123 audit.budgetDecision = budgetDecision;
124 audit.stage = 'pending-frontend' ;
125 }
126 // Preserve final outcomes only when every provenance dimension still matches. The
127 // destination queues are reconstructed from current source and route evidence.
128 if (deploymentId && previous && previous.sourceSnapshot === sourceSnapshot && previous.routeRevision === routeRevision && previous.deploymentId === deploymentId) {
129 const finished = new Map ((previous.outcomes || []). map (( row ) => [row.occurrenceId, row]));
130 for ( const pending of audit.pendingDestinations) {
131 pending.occurrenceIds = pending.occurrenceIds. filter (( id ) => {
132 const done = finished. get (id);
133 const target = targets. find (( row ) => row.targetRecordId === pending.targetRecordId && row.expectedPath === pending.expectedPath);
134 const occurrence = audit.occurrences. find (( row ) => row.id === id);
135 if ( ! done || ! target || ! occurrence?.targetContentRevision || done.contentRevision !== occurrence.targetContentRevision || done.containingRecordId !== occurrence.targetRecordId || done.targetRecordId !== pending.targetRecordId || done.expectedPath !== pending.expectedPath) return true ;
136 outcomes. push (done); return false ;
137 });
138 }
139 audit.pendingDestinations = audit.pendingDestinations. filter (( row ) => row.occurrenceIds. length );
140 if (previous.stage === 'verified' && ! audit.pendingDestinations. length && ! audit.unresolved. length && audit.writerIntegrations. every (( row ) => row.occurrenceIds. every (( id ) => outcomes. some (( outcome ) => outcome.occurrenceId === id)))) {
141 audit.stage = 'verified' ;
142 audit.writerIntegrations. forEach (( row ) => { row.status = 'complete' ; });
143 }
144 }
145 return validateLinkAudit (audit);
146 }
147
148 module . exports = { buildLinkPlan };