Setting the file. One moment. Domain Knowledge · Rp Target Wix · wix/skills · Skills Docs · cms
function requireFields
— line 174
This file
- Number
- 19.38
- Position
- 38 of 44
- Type
- JavaScript
- Size
- 15 KB
- Lines
- 384
lib/domain-knowledge.js
JavaScript·384 lines·15 KB
9
'cms'
,
10 'native-plus-cms',
11 'setup-config',
12 'skip-by-default',
13 'unsupported-native-gap',
14]);
15const ID_POLICIES = new Set(['client-assigned', 'server-assigned', 'natural-key', 'not-applicable']);
16const VERIFICATIONS = new Set(['verified-live', 'docs', 'source-review', 'internal-only', 'unverified', 'none']);
17const RELIABILITY = new Set(['reliable', 'partially-reliable', 'unreliable', 'unknown']);
18const SAFE_MODE_CONTACT_KINDS = new Set(['email', 'phone']);
19const WRITER_IDS = new Set(Object.keys(wixWriters).filter((name) => typeof wixWriters[name] === 'function'));
20
21function knowledgeRoot(rootDir = path.resolve(__dirname, '..')) {
22 return path.join(rootDir, 'domains');
23}
24
25function readJson(filePath) {
26 return JSON.parse(fs.readFileSync(filePath, 'utf8'));
27}
28
29function writeJson(filePath, value) {
30 fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
31}
32
33function entityRef(entity) {
34 return `${entity.domain}/${entity.entity}`;
35}
36
37function listDomainDirs(domainsDir) {
38 return fs
39 .readdirSync(domainsDir, { withFileTypes: true })
40 .filter((entry) => entry.isDirectory())
41 .map((entry) => entry.name)
42 .sort();
43}
44
45function listEntityFiles(domainDir) {
46 const entitiesDir = path.join(domainDir, 'entities');
47 if (!fs.existsSync(entitiesDir)) return [];
48 return fs
49 .readdirSync(entitiesDir, { withFileTypes: true })
50 .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
51 .map((entry) => entry.name)
52 .sort();
53}
54
55function loadDomain(domainsDir, domain) {
56 return readJson(path.join(domainsDir, domain, 'domain.json'));
57}
58
59function loadEntity(domainsDir, domain, entity) {
60 return readJson(path.join(domainsDir, domain, 'entities', `${entity}.json`));
61}
62
63function generateIndex(domainsDir) {
64 const index = {
65 schemaVersion: 1,
66 domains: {},
67 sourceAliasIndex: {},
68 routeAliasIndex: {},
69 flags: {},
70 };
71
72 for (const domain of listDomainDirs(domainsDir)) {
73 const domainPath = path.join(domain, 'domain.json');
74 const domainJson = loadDomain(domainsDir, domain);
75 const entities = {};
76 for (const fileName of listEntityFiles(path.join(domainsDir, domain))) {
77 const entityId = fileName.replace(/\.json$/, '');
78 const entityPath = path.join(domain, 'entities', fileName);
79 const entity = readJson(path.join(domainsDir, entityPath));
80 entities[entityId] = {
81 path: `domains/${entityPath}`,
82 displayName: entity.displayName,
83 classification: entity.target && entity.target.classification,
84 reliabilityStatus: entity.reliability && entity.reliability.status,
85 reliabilityFlags: (entity.reliability && entity.reliability.flags) || [],
86 summary: firstPitfallOrGuidance(entity),
87 };
88 for (const alias of entity.sourceAliases || []) {
89 const key = `${alias.sourceSystem}:${alias.sourceEntity}`;
90 if (!index.sourceAliasIndex[key]) index.sourceAliasIndex[key] = [];
91 index.sourceAliasIndex[key].push(`${domain}/${entityId}`);
92 for (const route of alias.routes || []) {
93 if (!index.routeAliasIndex[route]) index.routeAliasIndex[route] = [];
94 index.routeAliasIndex[route].push(`${domain}/${entityId}`);
95 }
96 }
97 for (const flag of (entity.reliability && entity.reliability.flags) || []) {
98 if (!index.flags[flag]) index.flags[flag] = [];
99 index.flags[flag].push(`${domain}/${entityId}`);
100 }
101 }
102 index.domains[domain] = {
103 path: `domains/${domainPath}`,
104 displayName: domainJson.displayName,
105 ownerHint: domainJson.ownerHint,
106 entities,
107 };
108 }
109
110 for (const collection of [index.sourceAliasIndex, index.routeAliasIndex, index.flags]) {
111 for (const key of Object.keys(collection)) collection[key] = Array.from(new Set(collection[key])).sort();
112 }
113
114 return index;
115}
116
117function firstPitfallOrGuidance(entity) {
118 if (Array.isArray(entity.pitfalls) && entity.pitfalls[0] && entity.pitfalls[0].summary) {
119 return entity.pitfalls[0].summary;
120 }
121 if (Array.isArray(entity.mappingGuidance) && entity.mappingGuidance[0]) return entity.mappingGuidance[0];
122 return '';
123}
124
125function validateKnowledge(domainsDir) {
126 const errors = [];
127 const indexPath = path.join(domainsDir, 'index.json');
128 const index = fs.existsSync(indexPath) ? readJson(indexPath) : null;
129 const generated = generateIndex(domainsDir);
130
131 if (!fs.existsSync(path.join(domainsDir, 'schema.json'))) {
132 errors.push('domains/schema.json is missing');
133 }
134
135 for (const domain of listDomainDirs(domainsDir)) {
136 const domainFile = path.join(domainsDir, domain, 'domain.json');
137 if (!fs.existsSync(domainFile)) {
138 errors.push(`${domain}: missing domain.json`);
139 continue;
140 }
141 const domainJson = readJson(domainFile);
142 requireFields(domainJson, ['schemaVersion', 'domain', 'displayName', 'ownerHint', 'defaultImportOrder', 'evidence'], `${domain}/domain.json`, errors);
143 if (domainJson.schemaVersion !== 1) errors.push(`${domain}/domain.json: schemaVersion must be 1`);
144 if (domainJson.domain !== domain) errors.push(`${domain}/domain.json: domain must match directory`);
145 for (const entityId of domainJson.defaultImportOrder || []) {
146 if (!fs.existsSync(path.join(domainsDir, domain, 'entities', `${entityId}.json`))) {
147 errors.push(`${domain}/domain.json: defaultImportOrder references missing entity ${entityId}`);
148 }
149 }
150 validateEvidence(domainJson.evidence || [], `${domain}/domain.json`, errors);
151
152 for (const fileName of listEntityFiles(path.join(domainsDir, domain))) {
153 const entityId = fileName.replace(/\.json$/, '');
154 const entity = loadEntity(domainsDir, domain, entityId);
155 const label = `${domain}/entities/${fileName}`;
156 validateEntity(entity, domain, entityId, label, errors);
157 }
158 }
159
160 if (!index) {
161 errors.push('domains/index.json is missing; run domain-knowledge-validate.js --write-index');
162 } else {
163 const current = JSON.stringify(index);
164 const expected = JSON.stringify(generated);
165 if (current !== expected) {
166 errors.push('domains/index.json is stale; run domain-knowledge-validate.js --write-index');
167 }
168 validateIndexConsistency(index, domainsDir, errors);
169 }
170
171 return { ok: errors.length === 0, errors, generatedIndex: generated };
172}
173
174function requireFields(value, fields, label, errors) {
175 for (const field of fields) {
176 if (value[field] === undefined) errors.push(`${label}: missing required field ${field}`);
177 }
178}
179
180function validateEntity(entity, domain, entityId, label, errors) {
181 requireFields(entity, ['schemaVersion', 'domain', 'entity', 'displayName', 'target', 'sourceAliases', 'preferredWrite', 'reliability', 'pitfalls', 'mappingGuidance', 'evidence'], label, errors);
182 if (entity.schemaVersion !== 1) errors.push(`${label}: schemaVersion must be 1`);
183 if (entity.domain !== domain) errors.push(`${label}: domain must match file path`);
184 if (entity.entity !== entityId) errors.push(`${label}: entity must match file path`);
185 if (!CLASSIFICATIONS.has(entity.target && entity.target.classification)) errors.push(`${label}: invalid target.classification`);
186 if (!ID_POLICIES.has(entity.target && entity.target.idPolicy)) errors.push(`${label}: invalid target.idPolicy`);
187 if (typeof (entity.target && entity.target.crosswalkRequired) !== 'boolean') errors.push(`${label}: target.crosswalkRequired must be boolean`);
188 if (!VERIFICATIONS.has(entity.preferredWrite && entity.preferredWrite.verification)) errors.push(`${label}: invalid preferredWrite.verification`);
189 if (!RELIABILITY.has(entity.reliability && entity.reliability.status)) errors.push(`${label}: invalid reliability.status`);
190 if (!Array.isArray(entity.reliability && entity.reliability.flags)) errors.push(`${label}: reliability.flags must be an array`);
191 if (entity.preferredWrite && entity.preferredWrite.writerId !== null && !WRITER_IDS.has(entity.preferredWrite.writerId)) {
192 errors.push(`${label}: writerId ${entity.preferredWrite.writerId} is not exported by wix-writers.js`);
193 }
194 if (entity.target && entity.target.classification !== 'cms' && entity.target.idPolicy === 'server-assigned' && entity.target.crosswalkRequired !== true) {
195 errors.push(`${label}: server-assigned non-CMS targets must require crosswalk`);
196 }
197 for (const alias of entity.sourceAliases || []) {
198 if (!alias.sourceSystem || !alias.sourceEntity) errors.push(`${label}: sourceAliases entries must include sourceSystem and sourceEntity`);
199 }
200 validateSafeModeContactFields(entity.safeModeContactFields, label, errors);
201 validateEvidence(entity.evidence || [], label, errors);
202}
203
204function validateSafeModeContactFields(fields, label, errors) {
205 if (fields === undefined) return;
206 if (!Array.isArray(fields)) {
207 errors.push(`${label}: safeModeContactFields must be an array when present`);
208 return;
209 }
210 for (const [index, field] of fields.entries()) {
211 const fieldLabel = `${label}: safeModeContactFields[${index}]`;
212 if (!field || typeof field !== 'object' || Array.isArray(field)) {
213 errors.push(`${fieldLabel} must be an object`);
214 continue;
215 }
216 if (!SAFE_MODE_CONTACT_KINDS.has(field.kind)) errors.push(`${fieldLabel}.kind must be email or phone`);
217 if (!field.targetPath || typeof field.targetPath !== 'string') errors.push(`${fieldLabel}.targetPath must be a non-empty string`);
218 if (!field.source || typeof field.source !== 'string') errors.push(`${fieldLabel}.source must be a non-empty string`);
219 }
220}
221
222function validateEvidence(items, label, errors) {
223 if (!Array.isArray(items) || items.length === 0) {
224 errors.push(`${label}: evidence must be a non-empty array`);
225 return;
226 }
227 for (const item of items) {
228 if (!item.url && !item.path) errors.push(`${label}: every evidence item must include url or path`);
229 }
230}
231
232function validateIndexConsistency(index, domainsDir, errors) {
233 const flagged = new Set(index.flags && index.flags.IMPORT_UNRELIABLE ? index.flags.IMPORT_UNRELIABLE : []);
234 const entityFlagged = new Set();
235
236 for (const [domain, domainEntry] of Object.entries(index.domains || {})) {
237 if (!fs.existsSync(path.join(domainsDir, domain, 'domain.json'))) errors.push(`index: missing domain file for ${domain}`);
238 for (const [entityId, entityEntry] of Object.entries(domainEntry.entities || {})) {
239 const ref = `${domain}/${entityId}`;
240 const entityPath = path.join(domainsDir, domain, 'entities', `${entityId}.json`);
241 if (!fs.existsSync(entityPath)) {
242 errors.push(`index: missing entity file for ${ref}`);
243 continue;
244 }
245 const entity = readJson(entityPath);
246 if ((entity.reliability.flags || []).includes('IMPORT_UNRELIABLE')) entityFlagged.add(ref);
247 if (entityEntry.path !== `domains/${domain}/entities/${entityId}.json`) {
248 errors.push(`index: invalid path for ${ref}`);
249 }
250 }
251 }
252
253 for (const ref of entityFlagged) {
254 if (!flagged.has(ref)) errors.push(`index: ${ref} has IMPORT_UNRELIABLE but is missing from flags`);
255 }
256 for (const ref of flagged) {
257 if (!entityFlagged.has(ref)) errors.push(`index: ${ref} is flagged IMPORT_UNRELIABLE but entity file is not`);
258 }
259}
260
261function loadIndex(domainsDir) {
262 return readJson(path.join(domainsDir, 'index.json'));
263}
264
265function listDomains(domainsDir) {
266 const index = loadIndex(domainsDir);
267 return Object.entries(index.domains || {}).map(([domain, info]) => ({
268 domain,
269 displayName: info.displayName,
270 ownerHint: info.ownerHint,
271 path: info.path,
272 }));
273}
274
275function listEntities(domainsDir, domain) {
276 const index = loadIndex(domainsDir);
277 const domainInfo = index.domains && index.domains[domain];
278 if (!domainInfo) throw new Error(`Unknown domain: ${domain}`);
279 return Object.entries(domainInfo.entities || {}).map(([entity, info]) => ({
280 ref: `${domain}/${entity}`,
281 entity,
282 displayName: info.displayName,
283 classification: info.classification,
284 reliabilityStatus: info.reliabilityStatus,
285 reliabilityFlags: info.reliabilityFlags,
286 path: info.path,
287 }));
288}
289
290function readEntityByRef(domainsDir, ref) {
291 const [domain, entity] = ref.split('/');
292 if (!domain || !entity) throw new Error(`Invalid ref: ${ref}`);
293 return loadEntity(domainsDir, domain, entity);
294}
295
296function resolveSource(domainsDir, { sourceSystem, sourceEntity, route }) {
297 const index = loadIndex(domainsDir);
298 const refs = new Set();
299 if (sourceSystem && sourceEntity) {
300 for (const ref of index.sourceAliasIndex[`${sourceSystem}:${sourceEntity}`] || []) refs.add(ref);
301 }
302 if (route) {
303 for (const [pattern, patternRefs] of Object.entries(index.routeAliasIndex || {})) {
304 if (route === pattern || routeMatches(pattern, route)) {
305 for (const ref of patternRefs) refs.add(ref);
306 }
307 }
308 }
309 return Array.from(refs).sort().map((ref) => {
310 const entity = readEntityByRef(domainsDir, ref);
311 return {
312 ref,
313 confidence: bestAliasConfidence(entity, { sourceSystem, sourceEntity, route }),
314 sourceAliases: (entity.sourceAliases || []).filter((alias) => aliasMatches(alias, { sourceSystem, sourceEntity, route })),
315 };
316 });
317}
318
319function routeMatches(pattern, route) {
320 if (!pattern.includes('{') && !pattern.includes(':')) return false;
321 const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\\\{[^}]+\\\}/g, '[^/]+').replace(/:[A-Za-z0-9_-]+/g, '[^/]+');
322 return new RegExp(`^${escaped}$`).test(route);
323}
324
325function aliasMatches(alias, query) {
326 const sourceMatches = (!query.sourceSystem || alias.sourceSystem === query.sourceSystem) && (!query.sourceEntity || alias.sourceEntity === query.sourceEntity);
327 const routeMatchesAlias = !query.route || (alias.routes || []).some((pattern) => query.route === pattern || routeMatches(pattern, query.route));
328 return sourceMatches && routeMatchesAlias;
329}
330
331function bestAliasConfidence(entity, query) {
332 const order = { high: 3, medium: 2, low: 1 };
333 let best = 'low';
334 for (const alias of entity.sourceAliases || []) {
335 if (aliasMatches(alias, query) && (order[alias.confidence] || 0) > (order[best] || 0)) best = alias.confidence;
336 }
337 return best;
338}
339
340function listFlagged(domainsDir, flag) {
341 const index = loadIndex(domainsDir);
342 return (index.flags && index.flags[flag] ? index.flags[flag] : []).map((ref) => {
343 const entity = readEntityByRef(domainsDir, ref);
344 return {
345 ref,
346 displayName: entity.displayName,
347 classification: entity.target.classification,
348 reliabilityStatus: entity.reliability.status,
349 summary: firstPitfallOrGuidance(entity),
350 };
351 });
352}
353
354function summarizeEntities(domainsDir, refs, { includeEvidence = false } = {}) {
355 return refs.map((ref) => {
356 const entity = readEntityByRef(domainsDir, ref);
357 const summary = {
358 ref,
359 displayName: entity.displayName,
360 target: entity.target,
361 preferredWrite: entity.preferredWrite,
362 reliability: entity.reliability,
363 pitfalls: entity.pitfalls,
364 mappingGuidance: entity.mappingGuidance,
365 setupRequirements: entity.setupRequirements || [],
366 fieldContracts: entity.fieldContracts || [],
367 };
368 if (includeEvidence) summary.evidence = entity.evidence;
369 return summary;
370 });
371}
372
373module.exports = {
374 knowledgeRoot,
375 generateIndex,
376 validateKnowledge,
377 writeJson,
378 listDomains,
379 listEntities,
380 readEntityByRef,
381 resolveSource,
382 listFlagged,
383 summarizeEntities,
384};