Setting the file. One moment.
Discovered Set Status · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page function actionFor
— line 92
This file
Number 45.71
Position 71 of 182
Type JavaScript
Size 15 KB
Lines 271 lib/ discovered-set-status.js
JavaScript · 271 lines · 15 KB
9 const STATUS_FILE = 'execution/discovered-set-status.json' ;
10 const STATUS_MARKDOWN = 'execution/review/discovered-set-status.md' ;
11 const RESOLUTIONS_FILE = 'execution/discovered-set-resolutions.ndjson' ;
12 const RECONCILIATION_FILE = 'mapping/review/census-reconciliation.json' ;
13 const REPORT_FILE = 'execution/completion-report.json' ;
14 const OUTCOME_ORDER = [ 'failed' , 'unresolved' , 'deferred' , 'partial' , 'no-outcome-recorded' , 'not-decided' , 'unsupported' , 'excluded' , 'manual-open' , 'manual-done' , 'imported' , 'reference-only' , 'no-data' ];
15 const CLOSED = new Set ([ 'imported' , 'manual-done' , 'reference-only' , 'no-data' ]);
16 const COVERAGE_OPEN = {
17 recovered: false ,
18 'gated-exclusion' : false ,
19 'ungated-exclusion' : true ,
20 'accepted-loss' : true ,
21 unresolved: true
22 };
23 function digest ( value ) {
24 return crypto. createHash ( 'sha256' ). update (value). digest ( 'hex' );
25 }
26 function sorted ( value ) {
27 return Array. isArray (value) ? value. map (sorted) : value && typeof value === 'object' ? Object. fromEntries (Object. keys (value). sort (). map ( key => [key, sorted (value[key])])) : value;
28 }
29 async function raw ( projectDir , file ) {
30 try {
31 return await fs. readFile (path. join (projectDir, file), 'utf8' );
32 } catch (e) {
33 if (e.code === 'ENOENT' ) return null ;
34 throw e;
35 }
36 }
37 async function json ( projectDir , file ) {
38 const value = await raw (projectDir, file);
39 return value === null ? null : JSON . parse (value);
40 }
41 function refusal ( code , detail = {}) {
42 return Object. assign ( new Error (code), {
43 code,
44 ... detail
45 });
46 }
47 async function resolutionInputs ( projectDir ) {
48 const text = await raw (projectDir, RESOLUTIONS_FILE );
49 const lines = text === null ? [] : text. split ( ' \n ' ). filter (Boolean);
50 // The journal has one canonical newline per entry; reject malformed or partial entries.
51 for ( const line of lines) JSON . parse (line);
52 return {
53 text,
54 lines,
55 sha256: text === null ? null : digest (text)
56 };
57 }
58 async function inputDigests ( projectDir ) {
59 const [ report , reconciliation , approvals , resolutions ] = await Promise . all ([ raw (projectDir, REPORT_FILE ), raw (projectDir, RECONCILIATION_FILE ), json (projectDir, 'orchestration/approvals.json' ), resolutionInputs (projectDir)]);
60 return {
61 completionReport: report === null ? null : digest (report),
62 reconciliation: reconciliation === null ? null : digest (reconciliation),
63 approvalsMapping: digest ( JSON . stringify ( sorted (approvals?.mapping ?? null ))),
64 resolutions: {
65 sha256: resolutions.sha256,
66 lines: resolutions.lines. length
67 }
68 };
69 }
70 async function discoveredSetStatusFreshness ( projectDir , status ) {
71 const current = await inputDigests (projectDir);
72 const staleInputs = Object. keys (current). filter ( key => JSON . stringify (current[key]) !== JSON . stringify (status?.inputs?.[key]));
73 const reconciliation = await json (projectDir, RECONCILIATION_FILE );
74 const {
75 censusReconciliationFreshness
76 } = require ( './census-reconciliation.js' );
77 if ( ! reconciliation || ! ( await censusReconciliationFreshness (projectDir, reconciliation)).ok) {
78 if ( ! staleInputs. includes ( 'reconciliation' )) staleInputs. push ( 'reconciliation' );
79 }
80 return {
81 ok: staleInputs. length === 0 ,
82 staleInputs
83 };
84 }
85 const nonEmpty = value => typeof value === 'string' && value. trim (). length > 0 ;
86 function deriveFinalStatus ( backendStatus , statusReport ) {
87 return backendStatus === 'complete' && statusReport?.openCount > 0 ? 'complete_with_warnings' : backendStatus;
88 }
89 function labelFor ( row ) {
90 return row.label || (row.entity ? `${ row . entity }${ row . subtype && row . subtype !== 'all' ? `/${ row . subtype }` : ''}` : row.capability || row.censusId);
91 }
92 function actionFor ( projectDir , row , outcome , count ) {
93 if ( CLOSED . has (outcome) || (outcome === 'excluded' || outcome === 'unsupported' ) && row.ownerApproved) return null ;
94 if (outcome === 'manual-open' && row.cutoverTask && nonEmpty (row.cutoverTask.task)) {
95 // A named no-data cutover task (cutover-task-only.json): the signed task, its
96 // steps and its done-when evidence, in the same action fields.
97 const steps = (row.manualSteps && Array. isArray (row.manualSteps.steps) ? row.manualSteps.steps : [])
98 . map ( step => step && step.text). filter (nonEmpty);
99 return {
100 owner: 'Cutover Operator' ,
101 action: [ `${ row . cutoverTask . task }.` , ... steps. map (( text , index ) => `${ index + 1 }. ${ text }` )]. join ( ' ' ),
102 timing: 'before-dns' ,
103 consequence: `${ labelFor ( row ) } needs this done on Wix before launch` ,
104 affectedCount: count,
105 resolvedWhen: nonEmpty (row.cutoverTask.doneWhen) ? row.cutoverTask.doneWhen : 'A merchant or Cutover Operator records completion evidence' ,
106 importOperatorPrompt: null
107 };
108 }
109 if (outcome === 'manual-open' ) return {
110 owner: 'Cutover Operator' ,
111 action: 'Complete the recorded manual steps, then record resolution evidence' ,
112 timing: 'before-dns' ,
113 consequence: 'The discovered capability still needs manual setup' ,
114 affectedCount: count,
115 resolvedWhen: 'A merchant or Cutover Operator records completion evidence' ,
116 importOperatorPrompt: null
117 };
118 const label = labelFor (row);
119 let instruction;
120 if (outcome === 'not-decided' && row.reasonCode === 'import-after-finalization' ) instruction = 'was discovered after this migration finalized, and the owner wants it imported. Importing into the finalized site needs a separate human decision. Do not import it. Raise that decision with the migration owner and record the answer.' ; else if (outcome === 'not-decided' ) instruction = `is pending: ${ row . question || 'Ask the owner how to handle this discovered scope'}. Ask the owner, record the answer as a closed state with rp-mapper, rebuild the census reconciliation, record the mapping approval, then rebuild the discovered-set status report and the post-execution handoff.` ; else if (outcome === 'excluded' || outcome === 'unsupported' ) instruction = `is ${ outcome } without the owner's approval. Present the mapping review to the owner for approval, or reverse the exclusion with rp-mapper, then rebuild the status report and handoff.` ; else instruction = `has outcome ${ outcome }. Recover it through the existing import recovery path, then rebuild the completion report, the discovered-set status report and the post-execution handoff.` ;
121 return {
122 owner: 'Import Operator' ,
123 action: outcome === 'not-decided' ? 'Ask the migration owner and record the decision' : [ 'excluded' , 'unsupported' ]. includes (outcome) ? 'Obtain owner approval or reverse the exclusion' : 'Recover through the import recovery path' ,
124 timing: 'before-dns' ,
125 consequence: `Discovered scope ${ row . censusId } is ${ outcome }` ,
126 affectedCount: count,
127 resolvedWhen: outcome === 'not-decided' ? 'A fresh approved reconciliation records a closed decision' : [ 'excluded' , 'unsupported' ]. includes (outcome) ? 'A current user approval covers this exact reconciliation' : 'A fresh completion report gives this row imported' ,
128 importOperatorPrompt: `Open the RePlatform migration project at ${ path . resolve ( projectDir ) } in the original migration environment. Census row ${ row . censusId } (${ label }) ${ instruction } Do not paste or log record values.`
129 };
130 }
131 function leastFavorable ( rows ) {
132 return rows. reduce (( worst , row ) => ! worst || OUTCOME_ORDER . indexOf (row.outcome) < OUTCOME_ORDER . indexOf (worst.outcome) ? row : worst, null );
133 }
134 function deriveRows ( projectDir , reconciliation , report , approvedIds , resolutions ) {
135 const resolvedIds = new Set (resolutions. map ( row => row.censusId));
136 const censusRows = reconciliation.rows || [];
137 const rows = censusRows. map ( source => {
138 const row = {
139 ... source,
140 ownerApproved: [ 'excluded' , 'unsupported' ]. includes (source.state) ? approvedIds. has (source.censusId) : null
141 };
142 let outcome = source.state;
143 if (source.kind === 'source-coverage' ) {
144 outcome = (report.sourceCoverage || []). find ( entry => entry.coverageId === source.coverageId)?.treatment || 'recovered' ;
145 if ( ! (outcome in COVERAGE_OPEN )) throw refusal ( 'source_coverage_treatment_unknown' , {
146 coverageId: source.coverageId
147 });
148 } else if (source.state === 'manual' ) outcome = resolvedIds. has (source.censusId) ? 'manual-done' : 'manual-open' ; else if (source.state === 'pending' ) outcome = 'not-decided' ; else if (source.state === 'planned' && [ 'entity' , 'sub-part' ]. includes (source.kind)) {
149 const completeness = (report.entityCompleteness || []). find ( entry => entry.entity === source.entity && (entry.subtype || 'all' ) === (source.subtype || 'all' ));
150 const dropped = (report.droppedFields || []). some ( field => {
151 if (field.entity !== source.entity) return false ;
152 if (field.subtype) return field.subtype === (source.subtype || 'all' );
153 const subparts = censusRows. filter ( entry => entry.kind === 'sub-part' && entry.entity === field.entity && (entry.evidenceFields || []). includes (field.field));
154 return subparts. length ? subparts. some ( entry => entry.censusId === source.censusId) : (source.subtype || 'all' ) === 'all' ;
155 });
156 outcome = ! completeness ? 'no-outcome-recorded' : completeness.failed > 0 ? 'failed' : completeness.unresolved > 0 ? 'unresolved' : completeness.deferred > 0 ? 'deferred' : completeness.imported + completeness.alreadyPresentByCrosswalk < completeness.inScope || dropped ? 'partial' : 'imported' ;
157 }
158 if (source.state === 'planned' && source.kind === 'plugin' && ! (source.linkedCapabilities || []). length ) outcome = 'no-outcome-recorded' ;
159 return {
160 censusId: source.censusId,
161 kind: source.kind,
162 state: source.state,
163 outcome,
164 ownerApproved: row.ownerApproved,
165 count: Object . prototype .hasOwnProperty. call (source, 'count' ) ? typeof source.count === 'number' ? source.count : null : typeof source.recordCount === 'number' ? source.recordCount : null ,
166 open: source.kind === 'source-coverage' ? COVERAGE_OPEN [outcome] : ! CLOSED . has (outcome) && ! ([ 'excluded' , 'unsupported' ]. includes (outcome) && row.ownerApproved),
167 action: null ,
168 ... (source.kind === 'source-coverage' ? {
169 actionSource: 'completion-report'
170 } : {})
171 };
172 });
173 // Capabilities are based on actual plan links, never guessed from source entity names.
174 for ( const row of rows. filter ( entry => entry.kind === 'capability' && entry.state === 'planned' )) {
175 const source = censusRows. find ( entry => entry.censusId === row.censusId);
176 const children = (source.relatedEntityIds || []). map ( id => rows. find ( entry => entry.censusId === id)). filter (Boolean);
177 const worst = leastFavorable (children);
178 row.outcome = worst?.outcome || 'no-outcome-recorded' ;
179 row.open = children. length ? children. some ( entry => entry.open) : true ;
180 }
181 for ( const row of rows. filter ( entry => entry.kind === 'plugin' )) {
182 const source = censusRows. find ( entry => entry.censusId === row.censusId);
183 if ( ! (source.linkedCapabilities || []). length ) continue ;
184 const children = source.linkedCapabilities. map ( id => rows. find ( entry => entry.censusId === id)). filter (Boolean);
185 const worst = leastFavorable (children);
186 row.outcome = worst?.outcome || 'no-outcome-recorded' ;
187 row.open = children. length ? children. some ( entry => entry.open) : true ;
188 row.ownerApproved = [ 'excluded' , 'unsupported' ]. includes (row.outcome) ? children. every ( entry => ! entry.open) : null ;
189 }
190 for ( const row of rows) if (row.open && row.kind !== 'source-coverage' ) row.action = actionFor (projectDir, {
191 ... censusRows. find ( entry => entry.censusId === row.censusId),
192 ownerApproved: row.ownerApproved
193 }, row.outcome, row.count);
194 return rows;
195 }
196 function renderStatus ( status ) {
197 return [ '# Discovered set status' , '' , '| Census ID | State | Outcome | Count | Open |' , '| --- | --- | --- | --- | --- |' , ... status.rows. map ( row => `| ${ row . censusId } | ${ row . state || 'derived'} | ${ row . outcome } | ${ row . count === null ? 'unknown' : row . count } | ${ row . open ? 'yes' : 'no'} |` ), '' , `Open discovered-set rows: ${ status . openCount }` , '' ]. join ( ' \n ' );
198 }
199 async function buildDiscoveredSetStatus ( projectDir ) {
200 const {
201 censusReconciliationFreshness ,
202 mappingApprovalBound ,
203 ownerApprovedCensusIds
204 } = require ( './census-reconciliation.js' );
205 const reconciliation = await json (projectDir, RECONCILIATION_FILE );
206 if ( ! reconciliation) throw refusal ( 'census_reconciliation_missing' );
207 const fresh = await censusReconciliationFreshness (projectDir, reconciliation);
208 if ( ! fresh.ok) throw refusal ( 'census_reconciliation_stale' , fresh);
209 if (reconciliation.errors?. length ) throw refusal ( 'census_reconciliation_blocking' , {
210 errors: reconciliation.errors
211 });
212 const approval = ( await json (projectDir, 'orchestration/approvals.json' ))?.mapping;
213 const ref = approval?.artifactRefs?. find ( ref => ref && typeof ref === 'object' && ref.kind === 'census-reconciliation' && ref.path === RECONCILIATION_FILE && / ^ [0-9a-f] {64}$ / i . test (ref.sha256));
214 if ( ! ref) throw refusal ( 'approval_missing_census_reference' );
215 if ( ! ( await mappingApprovalBound (projectDir, approval))) throw refusal ( 'mapping_approval_stale_census' );
216 const report = await json (projectDir, REPORT_FILE );
217 if ( ! report) throw refusal ( 'completion_report_missing' );
218 const resolutions = await resolutionInputs (projectDir);
219 const rows = deriveRows (projectDir, reconciliation, report, await ownerApprovedCensusIds (projectDir), resolutions.lines. map ( JSON .parse));
220 const status = {
221 schemaVersion: 1 ,
222 generatedAt: new Date (). toISOString (),
223 inputs: await inputDigests (projectDir),
224 rows,
225 openCount: rows. filter ( row => row.open && row.kind !== 'source-coverage' ). length
226 };
227 await writeJsonAtomic (path. join (projectDir, STATUS_FILE ), status);
228 await fs. mkdir (path. dirname (path. join (projectDir, STATUS_MARKDOWN )), {
229 recursive: true
230 });
231 await fs. writeFile (path. join (projectDir, STATUS_MARKDOWN ), renderStatus (status));
232 return status;
233 }
234 async function resolveDiscoveredSet ( projectDir , {
235 censusId ,
236 by ,
237 evidence
238 }) {
239 if ( ! [ 'merchant' , 'cutover-operator' ]. includes (by) || typeof evidence !== 'string' || ! evidence. trim () || evidence. length > 500 || / [\r\n \x00 ] / . test (evidence) || / [\w.+-] + @ [\w-] + \. [\w.-] + / . test (evidence)) throw refusal ( 'resolution_invalid' );
240 // Recompute from trusted inputs before appending; callers cannot resolve a forged status row.
241 const status = await buildDiscoveredSetStatus (projectDir);
242 if ( ! status.rows. some ( row => row.censusId === censusId && row.outcome === 'manual-open' )) throw refusal ( 'resolution_not_manual_open' );
243 const entry = {
244 censusId,
245 by,
246 evidence: evidence. trim (),
247 recordedAt: new Date (). toISOString ()
248 };
249 await fs. appendFile (path. join (projectDir, RESOLUTIONS_FILE ), `${ JSON . stringify ( entry ) } \n ` );
250 return buildDiscoveredSetStatus (projectDir);
251 }
252 async function checkResolutionPrefix ( projectDir , recorded ) {
253 const current = await resolutionInputs (projectDir);
254 if (current.lines. length < recorded.lines) return false ;
255 if ( ! recorded.lines) return recorded.sha256 === null || recorded.sha256 === digest ( '' );
256 return digest ( `${ current . lines . slice ( 0 , recorded . lines ). join ( ' \n ' ) } \n ` ) === recorded.sha256;
257 }
258 module . exports = {
259 STATUS_FILE,
260 STATUS_MARKDOWN,
261 RESOLUTIONS_FILE,
262 RECONCILIATION_FILE,
263 inputDigests,
264 discoveredSetStatusFreshness,
265 deriveFinalStatus,
266 deriveRows,
267 buildDiscoveredSetStatus,
268 resolveDiscoveredSet,
269 checkResolutionPrefix,
270 renderStatus
271 };