Setting the file. One moment.
Discovered Census · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page function validateRegistry
— line 73
This file
Number 45.70
Position 70 of 182
Type JavaScript
Size 11 KB
Lines 255 lib/ discovered-census.js
JavaScript · 255 lines · 11 KB
const
CENSUS_FILE
=
'discovery/census.json'
;
9 const hash = bytes => crypto. createHash ( 'sha256' ). update (bytes). digest ( 'hex' );
10 async function readJson ( projectDir , file ) {
11 try {
12 return JSON . parse ( await fs. readFile (path. join (projectDir, file), 'utf8' ));
13 } catch (error) {
14 if (error.code === 'ENOENT' ) return null ;
15 throw error;
16 }
17 }
18 // Readiness re-checks freshness on every batch: a digest is reused while the file's identity
19 // (inode, size, mtime, ctime) is unchanged, and recomputed as soon as it changes.
20 const digests = new Map ();
21 async function fileDigest ( projectDir , file ) {
22 const absolute = path. resolve (projectDir, file);
23 try {
24 const stat = await fs. stat (absolute, { bigint: true });
25 const signature = [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs]. join ( ':' );
26 const prior = digests. get (absolute);
27 if (prior?.signature === signature) return prior.digest;
28 const digest = hash ( await fs. readFile (absolute));
29 if (digests.size >= 256 ) digests. delete (digests. keys (). next ().value);
30 digests. set (absolute, { signature, digest });
31 return digest;
32 } catch (error) {
33 digests. delete (absolute);
34 if (error.code === 'ENOENT' ) return null ;
35 throw error;
36 }
37 }
38 function sorted ( value ) {
39 if (Array. isArray (value)) return value. map (sorted);
40 if (value && typeof value === 'object' ) return Object. fromEntries (Object. keys (value). sort (). map ( key => [key, sorted (value[key])]));
41 return value;
42 }
43 const digestJson = value => hash ( JSON . stringify ( sorted (value)));
44 const slugOf = value => String (value || '' ). split ( '/' )[ 0 ];
45 const countOf = value => typeof value === 'number' && Number. isFinite (value) ? value : null ;
46 function entityEntries ( schema ) {
47 const entities = schema && schema.entities;
48 if (Array. isArray (entities)) return entities. map ( row => [row.name || row.entityName || row.sourceEntity, row]);
49 return Object. entries (entities || {});
50 }
51 function fieldNames ( fields ) {
52 if (Array. isArray (fields)) return fields. map ( field => typeof field === 'string' ? field : field && (field.name || field.path || field.field)). filter (Boolean);
53 return Object. keys (fields || {});
54 }
55 async function registryPath ( projectDir , options = {}) {
56 if ( Object . prototype .hasOwnProperty. call (options, 'subPartRegistryPath' )) return options.subPartRegistryPath;
57 const schema = await readJson (projectDir, 'source-schema.json' );
58 const decisions = await readJson (projectDir, 'orchestration/decisions.json' );
59 const adapter = schema && (schema.platform || schema.sourcePlatform) || decisions && decisions.sourcePlatform && decisions.sourcePlatform.value;
60 if ( ! adapter || ! / ^ [a-z0-9-] +$ / i . test (adapter)) return null ;
61 return path. join (__dirname, '..' , 'resources' , `rp-source-${ adapter }` , 'sub-parts.json' );
62 }
63 async function censusInputs ( projectDir , options = {}) {
64 const registry = await registryPath (projectDir, options);
65 const contract = await coverage. readCoverageContract (projectDir);
66 return {
67 sourceSchema: await fileDigest (projectDir, 'source-schema.json' ),
68 pluginCoverage: await fileDigest (projectDir, 'discovery/plugin-coverage.json' ),
69 sourceCoverage: contract.stamped ? await fileDigest (projectDir, coverage. SOURCE_COVERAGE_FILE ) : null ,
70 subPartRegistry: registry ? await fileDigest ( '' , registry) : null
71 };
72 }
73 function validateRegistry ( registry ) {
74 for ( const [ entity , subtypes ] of Object. entries (registry || {})) {
75 const fields = new Set ();
76 for ( const [ subtype , entry ] of Object. entries (subtypes)) {
77 if ( ! entry || ! Array. isArray (entry.evidenceFields) || ! entry.evidenceFields. length ) throw new Error ( `invalid_sub_part_registry: ${ entity }/${ subtype }` );
78 for ( const field of entry.evidenceFields) {
79 if ( typeof field !== 'string' || fields. has (field)) throw new Error ( `duplicate_sub_part_field: ${ entity }/${ field }` );
80 fields. add (field);
81 }
82 }
83 }
84 }
85 async function buildCensus ( projectDir , options = {}) {
86 const schema = await readJson (projectDir, 'source-schema.json' );
87 if ( ! schema || ! schema.entities || typeof schema.entities !== 'object' ) throw new Error ( 'source_schema_missing' );
88 const plugins = await readJson (projectDir, 'discovery/plugin-coverage.json' );
89 const regPath = await registryPath (projectDir, options);
90 const registry = regPath ? await readJson ( '' , regPath) : null ;
91 validateRegistry (registry);
92 const rows = [];
93 for ( const [ entity , value ] of entityEntries (schema)) {
94 if ( ! entity || ! value || typeof value !== 'object' ) throw new Error ( 'source_entity_invalid' );
95 rows. push ({
96 censusId: `entity:${ entity }` ,
97 kind: 'entity' ,
98 decisionRow: true ,
99 entity,
100 subtype: 'all' ,
101 recordCount: countOf (value.recordCount),
102 inUse: value.inUse == null ? null : value.inUse,
103 // Discovery's canonical plugin ref (`plugin.<plugin>.<entity>`), which mapping rows may name instead of the key.
104 ... ( typeof value.sourceEntityRef === 'string' && value.sourceEntityRef && value.sourceEntityRef !== entity ? { sourceEntityRef: value.sourceEntityRef } : {})
105 });
106 // A bridge-read plugin entity carries its live table columns (`schema.columns`), not `fields`.
107 const fields = new Set ([ ... fieldNames (value.fields), ... fieldNames (value.schema && value.schema.columns)]);
108 // The registry may name a plugin entity by its canonical ref, whatever key discovery gave it.
109 const subParts = registry && (registry[entity] || typeof value.sourceEntityRef === 'string' && registry[value.sourceEntityRef]) || {};
110 for ( const [ subtype , entry ] of Object. entries (subParts)) {
111 if ( ! entry.evidenceFields. some ( field => fields. has (field))) continue ;
112 rows. push ({
113 censusId: `entity:${ entity }/${ subtype }` ,
114 kind: 'sub-part' ,
115 decisionRow: true ,
116 entity,
117 subtype,
118 recordCount: null ,
119 evidenceFields: entry.evidenceFields,
120 label: entry.label || subtype
121 });
122 }
123 }
124 const capabilities = new Map ();
125 for ( const row of plugins && plugins.capabilities || []) {
126 if ( ! row || typeof row.capability !== 'string' || ! row.capability) throw new Error ( 'capability_invalid' );
127 // A core-covered plugin (WooCommerce) is no decision of its own: the core entities
128 // it names are census rows already and carry the real outcome.
129 if (row.basis === 'core-reader' ) continue ;
130 if ( ! capabilities. has (row.capability)) capabilities. set (row.capability, []);
131 capabilities. get (row.capability). push (row);
132 }
133 for ( const [ capability , sources ] of capabilities) {
134 const selected = sources. find ( row => row.recognized === true ) || sources[ 0 ];
135 const union = key => [ ...new Map (sources. flatMap ( row => row[key] || []). map ( value => [ JSON . stringify ( sorted (value)), value])). values ()];
136 rows. push ({
137 censusId: `capability:${ capability }` ,
138 kind: 'capability' ,
139 decisionRow: true ,
140 capability,
141 coverageStatus: selected.status,
142 plugins: union ( 'plugins' ),
143 sourceEntities: union ( 'sourceEntities' ),
144 blocked: union ( 'blocked' ),
145 recordCounts: selected.recordCounts || {},
146 basis: selected.basis || null ,
147 rationale: selected.rationale || null ,
148 // A no-data cutover task (cutover-task-only.json): its signed runbook travels with the row
149 // so the Cutover Operator action names the task, not a generic line.
150 ... (selected.cutoverTask ? { cutoverTask: selected.cutoverTask, manualSteps: selected.manualSteps || null } : {})
151 });
152 }
153 for ( const plugin of plugins && plugins.perPlugin || []) {
154 const slug = slugOf (plugin.plugin);
155 if ( ! slug) throw new Error ( 'plugin_invalid' );
156 if (plugin.statusBasis === 'core-reader' ) continue ;
157 const linkedCapabilities = [ ... capabilities. keys ()]. filter ( capability => (plugin.capabilities || []). includes (capability) || capabilities. get (capability). some ( row => (row.plugins || []). some ( p => slugOf (p) === slug))). map ( key => `capability:${ key }` ). sort ();
158 rows. push ({
159 censusId: `plugin:${ slug }` ,
160 kind: 'plugin' ,
161 decisionRow: linkedCapabilities. length === 0 ,
162 pluginStatus: plugin.status,
163 linkedCapabilities
164 });
165 }
166 const contract = await coverage. readCoverageContract (projectDir);
167 if (contract.stamped) {
168 const inputs = await coverage. loadCoverageInputs (projectDir);
169 const validation = coverage. validateCoverageFile (inputs.coverage);
170 for ( const row of validation.rows) rows. push ({
171 censusId: `coverage:${ row . coverageId }` ,
172 kind: 'source-coverage' ,
173 decisionRow: false ,
174 coverageId: row.coverageId,
175 capability: row.capability,
176 sourceEntity: row.sourceEntity,
177 reason: row.reason
178 });
179 }
180 rows. sort (( a , b ) => a.censusId. localeCompare (b.censusId));
181 if ( new Set (rows. map ( row => row.censusId)).size !== rows. length ) throw new Error ( 'duplicate_census_id' );
182 const result = {
183 schemaVersion: 1 ,
184 inputs: await censusInputs (projectDir, options),
185 rows
186 };
187 if (options.write !== false ) await coverage. writeJsonAtomic (path. join (projectDir, CENSUS_FILE ), result);
188 return result;
189 }
190 async function readCensus ( projectDir ) {
191 return readJson (projectDir, CENSUS_FILE );
192 }
193 async function censusFreshness ( projectDir , census , options = {}) {
194 if ( ! census) return {
195 ok: false ,
196 staleInputs: [ 'census' ]
197 };
198 try {
199 const current = await censusInputs (projectDir, options);
200 const staleInputs = Object. keys (current). filter ( key => ! census.inputs || current[key] !== census.inputs[key]);
201 // A malformed or hand-edited projection is not accepted merely because input hashes match.
202 const rebuilt = await buildCensus (projectDir, {
203 ... options,
204 write: false
205 });
206 if ( digestJson (census.rows) !== digestJson (rebuilt.rows) || census.schemaVersion !== 1 ) staleInputs. push ( 'rows' );
207 return {
208 ok: staleInputs. length === 0 ,
209 staleInputs
210 };
211 } catch {
212 return {
213 ok: false ,
214 staleInputs: [ 'census' ]
215 };
216 }
217 }
218 async function rolloutScan ( root , options = {}) {
219 const classify = options.classifyFinalizedRun || require ( './migration-completion.js' ).classifyFinalizedRun;
220 const result = {
221 reviewOnly: [],
222 held: []
223 };
224 for ( const entry of await fs. readdir (root, {
225 withFileTypes: true
226 })) {
227 if ( ! entry. isDirectory ()) continue ;
228 const projectDir = path. join (root, entry.name);
229 const classification = await classify (projectDir, {
230 decisions: ( await readJson (projectDir, 'orchestration/decisions.json' )) || {}
231 });
232 if (classification.kind === 'valid-v1' ) result.reviewOnly. push ({
233 projectDir,
234 reason: 'mapping_approval_missing_census'
235 });
236 if (classification.kind === 'unverified-v1' ) result.held. push ({
237 projectDir,
238 reason: classification.reason || 'finalized_receipt_unverified' ,
239 errors: classification.errors || []
240 });
241 }
242 return result;
243 }
244 module . exports = {
245 CENSUS_FILE,
246 buildCensus,
247 readCensus,
248 censusFreshness,
249 censusInputs,
250 rolloutScan,
251 readJson,
252 fileDigest,
253 digestJson,
254 sorted
255 };