Setting the file. One moment.
Census Reconciliation · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page 205
const stepText
— line 205
This file
Number 45.54
Position 54 of 182
Type JavaScript
Size 23 KB
Lines 460 lib/ census-reconciliation.js
JavaScript · 460 lines · 23 KB
require
(
'./target-import-support.js'
);
8 const {
9 getDecisionValue ,
10 isExplicitUserOneClick ,
11 oneClickRunStart
12 } = require ( './orchestration-decisions.js' );
13 const FILE = 'mapping/review/census-reconciliation.json' ;
14 const ASSIGNMENTS = 'mapping/review/finalized-census-assignments.json' ;
15 const STATES = [ 'pending' , 'unsupported' , 'excluded' , 'manual' , 'planned' , 'reference-only' , 'no-data' ];
16 const EXPECTED = {
17 'migration-planned' : 'planned' ,
18 'manual-mapping' : 'manual' ,
19 'no-need-to-migrate' : 'no-data' ,
20 pending: 'pending' ,
21 'requires-development' : 'unsupported'
22 };
23 const nonempty = value => typeof value === 'string' && value. trim (). length > 0 ;
24 const gapsOf = value => Array. isArray (value) ? value : value && Array. isArray (value.gaps) ? value.gaps : [];
25 // A plan row's census IDs: explicit `censusIds`, else the implicit entity named by its key or by
26 // exactly one census entity's recorded canonical `sourceEntityRef`. Several matching rows (an
27 // exact key included) are `ambiguous`; the reconciliation refuses them as `unknown_census_id`.
28 function planCensusIds ( row , censusRows = []) {
29 if (Array. isArray (row.censusIds)) return { ids: row.censusIds };
30 if ( ! nonempty (row.sourceEntity)) return { ids: [] };
31 const id = `entity:${ row . sourceEntity }` ;
32 const matches = censusRows. filter ( entry => entry.censusId === id || entry.kind === 'entity' && entry.sourceEntityRef === row.sourceEntity). map ( entry => entry.censusId);
33 return matches. length === 1 ? { ids: matches, implicitFrom: row.sourceEntity } : { ids: [id], implicitFrom: row.sourceEntity, ambiguous: matches. length > 1 ? matches : null };
34 }
35 async function readReconciliation ( projectDir ) {
36 try {
37 return await census. readJson (projectDir, FILE );
38 } catch (error) {
39 if (error instanceof SyntaxError ) return {
40 schemaVersion: null ,
41 rows: [],
42 errors: []
43 };
44 throw error;
45 }
46 }
47 function validCensusReference ( ref ) {
48 return Boolean (ref && typeof ref === 'object' && ref.kind === 'census-reconciliation' && ref.path === FILE && typeof ref.sha256 === 'string' && / ^ [a-f0-9] {64}$ / i . test (ref.sha256));
49 }
50 async function reconcileSourceCoverageForMapping ( projectDir , {
51 mappingStatus = 'approved' ,
52 registers = null
53 } = {}) {
54 try {
55 const contract = await coverage. readCoverageContract (projectDir);
56 if ( ! contract.stamped) return {
57 ok: true ,
58 failures: [],
59 resolved: [],
60 stamped: false
61 };
62 const inputs = await coverage. loadCoverageInputs (projectDir);
63 return {
64 ... coverage. reconcileSourceCoverage ({
65 ... inputs,
66 sourceSchema: contract.document,
67 registers: registers || ( await coverage. loadRegisters ()),
68 approvals: {
69 mapping: {
70 status: mappingStatus
71 }
72 }
73 }),
74 stamped: true
75 };
76 } catch (error) {
77 if ( ! (error instanceof coverage . SourceCoverageError )) throw error;
78 return {
79 ok: false ,
80 failures: error.failures. length ? error.failures : [{
81 code: 'invalid_coverage' ,
82 message: error.message
83 }],
84 resolved: [],
85 stamped: true
86 };
87 }
88 }
89 async function inputDigests ( projectDir , options = {}) {
90 // Read-only here; parsed once per plan version (readiness checks freshness every batch).
91 const plan = require ( './cached-json' ). readJson (path. join (projectDir, 'mapping/mapping-plan.json' ), null );
92 const decisions = ( await census. readJson (projectDir, 'orchestration/decisions.json' )) || {};
93 const selectedDecisions = Object. fromEntries (Object. entries (decisions). filter (([ key ]) => key. startsWith ( 'sourceCoverage:' )));
94 const gaps = gapsOf ( await census. readJson (projectDir, 'mapping/review/mapping-gaps.json' ));
95 const cited = new Map ();
96 const registers = options.registers || ( await coverage. loadRegisters ());
97 for ( const gap of gaps) {
98 if ( ! gap.coverageId || ! gap.gateEvidence || ! nonempty (gap.gateEvidence.register)) continue ;
99 const {
100 register ,
101 capability
102 } = gap.gateEvidence;
103 // Match 0017's registerEntry exactly, including its named human actor requirement.
104 const entry = (registers[register] || []). find ( row => row && row.capability === capability && nonempty (row.decidedBy)) || null ;
105 cited. set ( `${ register }:${ capability }` , entry);
106 }
107 return {
108 census: await census. fileDigest (projectDir, census. CENSUS_FILE ),
109 mappingPlan: await census. fileDigest (projectDir, 'mapping/mapping-plan.json' ),
110 mappingGaps: await census. fileDigest (projectDir, 'mapping/review/mapping-gaps.json' ),
111 finalizedAssignments: await census. fileDigest (projectDir, ASSIGNMENTS ),
112 sourceCoverageDecisions: Object. keys (selectedDecisions). length ? census. digestJson (selectedDecisions) : null ,
113 sourceCoverageRegisters: cited.size ? census. digestJson (Object. fromEntries ([ ... cited]. sort (([ a ], [ b ]) => a. localeCompare (b)))) : null ,
114 targetSupport: support. supportKnowledgeDigest (plan)
115 };
116 }
117 async function isReviewOnly ( projectDir , options ) {
118 const classify = options.classifyFinalizedRun || require ( './migration-completion.js' ).classifyFinalizedRun;
119 const result = await classify (projectDir, {
120 decisions: ( await census. readJson (projectDir, 'orchestration/decisions.json' )) || {}
121 });
122 return result.kind === 'valid-v1' ;
123 }
124 async function buildReconciliation ( projectDir , options = {}) {
125 const c = await census. readCensus (projectDir);
126 const errors = [];
127 const addError = ( code , censusId = null , details = {}) => errors. push ({
128 code,
129 censusId,
130 ... details
131 });
132 if ( ! c) addError ( 'census_missing' ); else if ( ! ( await census. censusFreshness (projectDir, c, options)).ok) addError ( 'census_stale' );
133 const reviewOnly = await isReviewOnly (projectDir, options);
134 const plan = await census. readJson (projectDir, 'mapping/mapping-plan.json' );
135 const gaps = gapsOf ( await census. readJson (projectDir, 'mapping/review/mapping-gaps.json' ));
136 const finalized = await census. readJson (projectDir, ASSIGNMENTS );
137 if (finalized && ! reviewOnly) addError ( 'finalized_assignments_not_allowed' );
138 if (reviewOnly) {
139 const validation = await require ( './website-handoff.js' ). validateWebsiteHandoff (projectDir);
140 if ( ! validation.ok) addError ( 'finalized_plan_changed' , null , {
141 changes: validation.changes || []
142 });
143 }
144 const planRows = require ( './website-handoff.js' ). readEntityMappings (plan);
145 const byId = new Map ((c && c.rows || []). map ( row => [row.censusId, row]));
146 const assignments = new Map ();
147 function assign ( id , item , details = {}) {
148 const row = byId. get (id);
149 if ( ! row) return addError ( 'unknown_census_id' , id, { assignedBy: item.assignedBy, ... details });
150 if ( ! row.decisionRow) return addError ( 'not_a_decision_row' , id);
151 if ( ! assignments. has (id)) assignments. set (id, []);
152 assignments. get (id). push (item);
153 }
154 const links = new Map ();
155 if (reviewOnly && finalized) {
156 if (finalized.schemaVersion !== 1 || ! Array. isArray (finalized.planLinks) || ! Array. isArray (finalized.gaps)) addError ( 'invalid_evidence' , null , {
157 source: ASSIGNMENTS
158 });
159 for ( const [ index , entry ] of (finalized.planLinks || []). entries ()) {
160 if ( ! Number. isInteger (entry.planRow) || ! planRows[entry.planRow] || ! Array. isArray (entry.censusIds)) {
161 addError ( 'invalid_evidence' , null , {
162 source: `finalized-planLinks#${ index }`
163 });
164 continue ;
165 }
166 if ( ! links. has (entry.planRow)) links. set (entry.planRow, []);
167 links. get (entry.planRow). push ( ... entry.censusIds);
168 }
169 }
170 planRows. forEach (( row , index ) => {
171 const named = planCensusIds (row, c && c.rows || []);
172 const details = named.implicitFrom ? { implicitFrom: named.implicitFrom, ... (named.ambiguous ? { ambiguous: named.ambiguous } : {}) } : {};
173 const ids = [ ... named.ids, ... (links. get (index) || [])];
174 const state = row.targetClassification === 'manual-mapping' ? 'manual' : 'planned' ;
175 const linkedEntities = ids. filter ( id => byId. has (id) && [ 'entity' , 'sub-part' ]. includes (byId. get (id).kind));
176 for ( const id of ids) if (named.ambiguous && named.ids. includes (id)) addError ( 'unknown_census_id' , id, { assignedBy: `mapping-plan#${ index }` , ... details }); else assign (id, {
177 state,
178 assignedBy: `mapping-plan#${ index }` ,
179 row,
180 relatedEntityIds: linkedEntities
181 }, named.ids. includes (id) ? details : {});
182 });
183 for ( const [ index , gap ] of [ ... gaps, ... (reviewOnly && finalized && Array. isArray (finalized.gaps) ? finalized.gaps : [])]. entries ()) {
184 if (gap.censusId == null ) continue ; // Existing record-level and 0017 gaps retain their own contracts.
185 assign (gap.censusId, {
186 state: gap.disposition,
187 assignedBy: `mapping-gaps#${ index }` ,
188 gap,
189 relatedEntityIds: []
190 });
191 }
192 const resolvedStates = new Map ();
193 for ( const [ id , entries ] of assignments) {
194 if (entries. length > 1 ) addError ( 'duplicate_disposition' , id);
195 resolvedStates. set (id, entries[ 0 ].state);
196 }
197 let summary = '' ;
198 try {
199 summary = await fs. readFile (path. join (projectDir, 'mapping/review/mapping-summary.md' ), 'utf8' );
200 } catch (error) {
201 if (error.code !== 'ENOENT' ) throw error;
202 }
203 const manualSection = summary. match ( / ^ # {1,6} \s + [ ^ \n] * manual [ ^ \n] * \n ( [\s\S] *? )(?= ^ # {1,6} \s | (?! [\s\S] ))/ im );
204 const manualText = manualSection ? manualSection[ 1 ] : '' ;
205 const stepText = step => typeof step === 'string' ? step : step && (step.description || step.instruction || step.action || step.text);
206 const stepsShown = steps => Array. isArray (steps) && steps. length > 0 && steps. every ( step => nonempty ( stepText (step)) && manualText. includes ( stepText (step)));
207 function validEvidence ( row , item ) {
208 const {
209 state ,
210 gap ,
211 row : planRow
212 } = item;
213 const reason = gap && gap.reasonCode;
214 if ( ! STATES . includes (state)) return false ;
215 if (reason === 'import-after-finalization' && ( ! reviewOnly || state !== 'pending' )) return false ;
216 if (row.kind === 'capability' && EXPECTED [row.coverageStatus] !== state && ! nonempty (gap && gap.rationale || planRow && planRow.rationale)) return false ;
217 if (planRow) {
218 if (state === 'manual' && ! stepsShown (planRow.manualSteps)) return false ;
219 if (row.kind === 'capability' && state === 'planned' ) {
220 if ( ! item.relatedEntityIds. length || item.relatedEntityIds. some ( id => ! [ 'planned' , 'manual' ]. includes (resolvedStates. get (id)))) return false ;
221 }
222 return true ;
223 }
224 if (state === 'planned' ) return false ;
225 if (state === 'manual' ) return row.kind === 'capability' && row.coverageStatus === 'manual-mapping' && ( stepsShown (gap.manualSteps || row.manualSteps && row.manualSteps.steps) || manualText. includes (row.capability));
226 if (state === 'no-data' ) return row.recordCount === 0 || row.kind === 'capability' && row.coverageStatus === 'no-need-to-migrate' && Boolean (row.basis || row.rationale);
227 if (state === 'reference-only' ) return Array. isArray (gap.referencedBy) && gap.referencedBy. length > 0 && gap.referencedBy. every ( id => resolvedStates. get (id) === 'planned' ) && nonempty (gap.rationale);
228 if (state === 'unsupported' ) return nonempty (reason) && nonempty (gap.severity) && (row.kind !== 'capability' || row.coverageStatus === 'requires-development' );
229 if (state === 'excluded' ) return nonempty (reason) && nonempty (gap.rationale) && Object . prototype .hasOwnProperty. call (gap, 'count' ) && (gap.count === null || typeof gap.count === 'number' && Number. isFinite (gap.count));
230 if (state === 'pending' ) return nonempty (gap.question);
231 return false ;
232 }
233 const coverageResult = await reconcileSourceCoverageForMapping (projectDir, {
234 mappingStatus: 'approved' ,
235 registers: options.registers
236 });
237 const coverageById = new Map ((coverageResult.resolved || []). map ( entry => [entry.row.coverageId, entry]));
238 const rows = [];
239 for ( const row of c && c.rows || []) {
240 if (row.kind === 'plugin' && ! row.decisionRow) continue ;
241 if (row.kind === 'source-coverage' ) {
242 const entry = coverageById. get (row.coverageId);
243 rows. push ({
244 ... row,
245 state: 'source-coverage' ,
246 assignedBy: 'source-coverage' ,
247 count: null ,
248 disposition: entry && entry.disposition || null ,
249 treatment: entry && entry.treatment || null
250 });
251 continue ;
252 }
253 const entries = assignments. get (row.censusId) || [];
254 if ( ! entries. length ) addError ( 'missing_disposition' , row.censusId);
255 const item = entries[ 0 ];
256 if (item && ! validEvidence (row, item)) addError ( 'invalid_evidence' , row.censusId);
257 // The census measurement is the count; a gap's stated count only fills an unmeasured row, shown as unmeasured.
258 const measured = typeof row.recordCount === 'number' ? row.recordCount : null ;
259 const stated = item && item.gap && typeof item.gap.count === 'number' ? item.gap.count : null ;
260 if (item && item.state === 'excluded' && measured !== null && stated !== null && stated !== measured) addError ( 'excluded_count_mismatch' , row.censusId, { count: stated, recordCount: measured });
261 rows. push ({
262 ... row,
263 ... (item && item.gap || {}),
264 censusId: row.censusId,
265 kind: row.kind,
266 state: item ? item.state : 'pending' ,
267 assignedBy: item ? item.assignedBy : null ,
268 count: measured,
269 ... (measured === null && stated !== null ? { statedCount: stated } : {}),
270 relatedEntityIds: item ? item.relatedEntityIds : []
271 });
272 }
273 for ( const { code , ... details } of support. supportErrors ({ plan, gaps })) addError (code, null , details);
274 if (plan?.dependencyBindings || rows. some ( row => row.dependencyFieldRules)) {
275 const contract = require ( './dependency-readiness' ). validateAssociations ({ plan, reconciliation: { rows } });
276 for ( const code of contract.errors) addError (code);
277 }
278 const rowMap = new Map (rows. map ( row => [row.censusId, row]));
279 for ( const row of c && c.rows || []) {
280 if (row.kind !== 'plugin' || row.decisionRow) continue ;
281 const states = row.linkedCapabilities. map ( id => rowMap. get (id) && rowMap. get (id).state || 'pending' );
282 rows. push ({
283 ... row,
284 state: STATES . find ( state => states. includes (state)) || 'pending' ,
285 assignedBy: 'derived' ,
286 count: null
287 });
288 }
289 rows. sort (( a , b ) => a.censusId. localeCompare (b.censusId));
290 const result = {
291 schemaVersion: 1 ,
292 inputs: await inputDigests (projectDir, options),
293 reviewOnly,
294 rows,
295 ... (plan?.dependencyBindings ? { dependencyBindings: plan.dependencyBindings } : {}),
296 errors,
297 pendingCount: rows. filter ( row => row.state === 'pending' && row.reasonCode !== 'import-after-finalization' ). length ,
298 importAfterFinalizationCount: rows. filter ( row => row.reasonCode === 'import-after-finalization' ). length
299 };
300 if (options.write !== false ) {
301 await coverage. writeJsonAtomic (path. join (projectDir, FILE ), result);
302 const section = renderDiscoveredSet (result, c);
303 const withoutOld = summary. replace ( / \n ? <!-- discovered-set:start --> [\s\S] *? <!-- discovered-set:end --> \n ? / g , ' \n ' );
304 await fs. writeFile (path. join (projectDir, 'mapping/review/mapping-summary.md' ), `${ withoutOld . trimEnd () } \n\n ${ section } \n ` );
305 }
306 return result;
307 }
308 function renderDiscoveredSet ( reconciliation , censusArtifact ) {
309 const order = [ 'pending' , 'excluded' , 'unsupported' , 'source-coverage' , 'manual' , 'reference-only' , 'no-data' , 'planned' ];
310 const lines = [ '<!-- discovered-set:start -->' , '## Discovered set' , '' , 'Approving this mapping approves the exclusions listed here.' ];
311 for ( const state of order) {
312 const rows = reconciliation.rows. filter ( row => row.state === state);
313 if ( ! rows. length ) continue ;
314 lines. push ( '' , `### ${ state }` , '' );
315 for ( const row of rows) lines. push ( `- ${ row . censusId }: ${ row . count == null ? `unknown${ row . statedCount == null ? '' : ` (stated ${ row . statedCount }, not measured)`}` : row . count }${ row . blocked && row . blocked . length ? `; unresolved blockers: ${ row . blocked . length }` : ''}` );
316 }
317 if ( ! censusArtifact || ! censusArtifact.inputs.subPartRegistry) lines. push ( '' , 'Sub-parts were not checked for this source.' );
318 if ( ! censusArtifact || ! censusArtifact.inputs.sourceCoverage) lines. push ( '' , 'Uncaptured rows were not checked for this source.' );
319 const dependencySection = renderDependencyRules (reconciliation);
320 if (dependencySection) lines. push ( '' , dependencySection);
321 lines. push ( '<!-- discovered-set:end -->' );
322 return lines. join ( ' \n ' );
323 }
324 function renderDependencyRules ( reconciliation ) {
325 const bindings = reconciliation.dependencyBindings || [];
326 const rules = (reconciliation.rows || []). flatMap ( row => (row.dependencyFieldRules || []). map ( rule => ({ ... rule, censusId: row.censusId, count: row.count ?? null })));
327 if ( ! bindings. length && ! rules. length ) return '' ;
328 const lines = [ '<!-- dependency-rules:start -->' , '### Dependency associations and payload consequences' , '' ];
329 for ( const binding of [ ... bindings]. sort (( a , b ) => a.ref. localeCompare (b.ref))) lines. push ( `- Target ${ binding . ref }: ${ [ ... binding . censusIds ]. sort (). join ( ', ' ) }` );
330 for ( const rule of rules. sort (( a , b ) => JSON . stringify (a). localeCompare ( JSON . stringify (b)))) lines. push ( `- ${ rule . censusId } (${ rule . count === null ? 'unknown population' : rule . count + ' source records'}): ${ rule . dependentRef } depends on ${ rule . dependencyRef }; ${ rule . action } ${ rule . path }. ${ rule . action === 'omit' ? 'The excluded reference will be absent from affected imported records.' : 'Affected dependent records will remain deferred.'}` );
331 lines. push ( '<!-- dependency-rules:end -->' ); return lines. join ( ' \n ' );
332 }
333 async function dependencyRulesShown ( projectDir , reconciliation ) {
334 const section = renderDependencyRules (reconciliation);
335 if ( ! section) return true ;
336 try { return ( await fs. readFile (path. join (projectDir, 'mapping/review/mapping-summary.md' ), 'utf8' )). includes (section); } catch { return false ; }
337 }
338 async function censusReconciliationFreshness ( projectDir , reconciliation , options = {}) {
339 if ( ! reconciliation || reconciliation.schemaVersion !== 1 || ! Array. isArray (reconciliation.rows) || ! Array. isArray (reconciliation.errors)) return {
340 ok: false ,
341 staleInputs: [ 'reconciliation' ]
342 };
343 try {
344 const current = await inputDigests (projectDir, options);
345 const staleInputs = Object. keys (current). filter ( key => ! reconciliation.inputs || current[key] !== reconciliation.inputs[key]);
346 const c = await census. readCensus (projectDir);
347 if ( ! ( await census. censusFreshness (projectDir, c, options)).ok && ! staleInputs. includes ( 'census' )) staleInputs. push ( 'census' );
348 return {
349 ok: staleInputs. length === 0 ,
350 staleInputs
351 };
352 } catch {
353 return {
354 ok: false ,
355 staleInputs: [ 'reconciliation' ]
356 };
357 }
358 }
359 async function mappingApprovalBound ( projectDir , approval , options = {}) {
360 if ( ! approval || ! [ 'approved' , 'provisional' ]. includes (approval.status)) return false ;
361 const refs = (approval.artifactRefs || []). filter (validCensusReference);
362 if (refs. length !== 1 ) return false ;
363 let reconciliation;
364 try {
365 reconciliation = await readReconciliation (projectDir);
366 } catch {
367 return false ;
368 }
369 return Boolean (reconciliation && refs[ 0 ].sha256 === ( await census. fileDigest (projectDir, FILE )) && ( await censusReconciliationFreshness (projectDir, reconciliation, options)).ok);
370 }
371 async function bindCensusReconciliation ( projectDir , {
372 approvalType ,
373 status ,
374 decidedBy ,
375 requestRef = null ,
376 ... options
377 }) {
378 const decisions = ( await census. readJson (projectDir, 'orchestration/decisions.json' )) || {};
379 const fail = ( code , details = {}) => ({
380 ok: false ,
381 code,
382 ... details
383 });
384 if (approvalType !== 'mapping' || [ 'pending' , 'rejected' ]. includes (status) || getDecisionValue (decisions, 'managementImportMode' ) === 'quick' ) return {
385 ok: true ,
386 ref: null
387 };
388 if ( ! [ 'user' , 'agent' ]. includes (decidedBy)) return fail ( 'approval_actor_invalid' );
389 if (decidedBy === 'agent' && ! isExplicitUserOneClick (decisions)) return fail ( 'approval_actor_not_one_click' );
390 let reconciliation;
391 try {
392 reconciliation = await readReconciliation (projectDir);
393 } catch {
394 return fail ( 'census_reconciliation_stale' , {
395 staleInputs: [ 'reconciliation' ]
396 });
397 }
398 if ( ! reconciliation) return fail ( 'census_reconciliation_missing' );
399 const freshness = await censusReconciliationFreshness (projectDir, reconciliation, options);
400 if ( ! freshness.ok) return fail ( 'census_reconciliation_stale' , freshness);
401 if ( ! ( await dependencyRulesShown (projectDir, reconciliation))) return fail ( 'dependency_rules_not_shown' );
402 if (reconciliation.errors. length ) return fail ( 'census_reconciliation_blocking' , {
403 errors: reconciliation.errors
404 });
405 const gate = await reconcileSourceCoverageForMapping (projectDir, {
406 mappingStatus: status,
407 registers: options.registers
408 });
409 if ( ! gate.ok) return fail ( 'source_coverage_unreconciled' , {
410 failures: gate.failures
411 });
412 // Interactive runs ask every pending question first. A one-click run never asks mid-run
413 // (also after an owner joins to approve): pending rows stay unimported and not approved,
414 // and the discovered-set status lists each as a not-decided cutover task.
415 if (status === 'approved' && decidedBy === 'user' && reconciliation.pendingCount > 0 && ! oneClickRunStart (decisions)) return fail ( 'approval_pending_rows' );
416 // Unsupported dependencies surface before approval, not at codegen. Read only: the approval
417 // re-checks its pinned artifacts after this, and the preflight review file is one of them.
418 if (status === 'approved' && ( await census. readJson (projectDir, 'mapping/mapping-plan.json' ))?.dependencyBindings) {
419 const preflight = await require ( './dependency-readiness' ). preflightMappingDependencies (projectDir, { write: false });
420 if ( ! preflight.structuralReady) return fail ( 'dependency_preflight_blocking' , { errors: preflight.errors });
421 }
422 const sha256 = await census. fileDigest (projectDir, FILE );
423 if (requestRef && ( ! validCensusReference (requestRef) || requestRef.sha256 !== sha256)) return fail ( 'approval_reference_stale' );
424 return {
425 ok: true ,
426 ref: {
427 kind: 'census-reconciliation' ,
428 path: FILE ,
429 sha256
430 }
431 };
432 }
433 async function ownerApprovedCensusIds ( projectDir , options = {}) {
434 const approvals = await census. readJson (projectDir, 'orchestration/approvals.json' );
435 const approval = approvals && approvals.mapping;
436 if ( ! approval || approval.status !== 'approved' || approval.decidedBy !== 'user' || ! ( await mappingApprovalBound (projectDir, approval, options))) return new Set ();
437 const reconciliation = await readReconciliation (projectDir);
438 if (reconciliation.errors. length || ! ( await dependencyRulesShown (projectDir, reconciliation))) return new Set ();
439 return new Set (reconciliation.rows. filter ( row => [ 'excluded' , 'unsupported' ]. includes (row.state)). map ( row => row.censusId));
440 }
441 module . exports = {
442 renderDependencyRules, dependencyRulesShown, planCensusIds,
443 FILE,
444 RECONCILIATION_FILE: FILE ,
445 ASSIGNMENTS,
446 STATES,
447 buildReconciliation,
448 readReconciliation,
449 renderDiscoveredSet,
450 censusReconciliationFreshness,
451 reconcileSourceCoverageForMapping,
452 mappingApprovalBound,
453 bindCensusReconciliation,
454 ownerApprovedCensusIds,
455 validCensusReference
456 };
457 for ( const name of [ 'buildReconciliation' , 'censusReconciliationFreshness' , 'reconcileSourceCoverageForMapping' , 'mappingApprovalBound' , 'bindCensusReconciliation' , 'ownerApprovedCensusIds' ]) {
458 const run = module . exports [name];
459 module . exports [name] = ( projectDir , ... args ) => require ( './operation-assessments' ). withProject (projectDir,() => run (projectDir, ... args));
460 }