Setting the file. One moment. Operation Assessments · Wix Replatform · wix/skills · Skills Docs — line 132
This file
- Number
- 45.113
- Position
- 113 of 182
- Type
- JavaScript
- Size
- 10 KB
- Lines
- 156
lib/operation-assessments.js
JavaScript·156 lines·10 KB
require
(
'node:async_hooks'
);
9const context = new AsyncLocalStorage();
10const artifactCache = new Map();
11const REF = /^[a-z0-9-]+\/[a-z0-9-]+$/;
12const HASH = /^[a-f0-9]{64}$/;
13const stable = v => JSON.stringify(v, (_, x) => x && typeof x === 'object' && !Array.isArray(x)
14 ? Object.fromEntries(Object.keys(x).sort().map(k => [k, x[k]])) : x);
15const digest = v => crypto.createHash('sha256').update(stable(v) ?? 'undefined').digest('hex');
16function fail(code) { throw Object.assign(new Error(code), { code }); }
17function artifact(projectDir, ref) {
18 if (!projectDir || !ref || typeof ref.artifact !== 'string' || !HASH.test(ref.sha256 || '')) fail('mapping_assessment_evidence_missing');
19 const root = fs.realpathSync(projectDir), file = fs.realpathSync(path.resolve(root, ref.artifact));
20 if (!file.startsWith(root + path.sep)) fail('mapping_assessment_evidence_outside_project');
21 const stat = fs.statSync(file);
22 const key = `${file}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}:${ref.sha256}`;
23 if (artifactCache.has(key)) return artifactCache.get(key);
24 const bytes = fs.readFileSync(file);
25 if (crypto.createHash('sha256').update(bytes).digest('hex') !== ref.sha256) fail('mapping_assessment_evidence_stale');
26 if (artifactCache.size >= 32) artifactCache.delete(artifactCache.keys().next().value);
27 artifactCache.set(key, bytes);
28 return bytes;
29}
30function rows(plan) {
31 for (const key of ['entities', 'entityMappings', 'entityDecisions', 'mappings']) {
32 if (Array.isArray(plan?.[key])) return plan[key];
33 if (key === 'entities' && plan?.entities && typeof plan.entities === 'object') return Object.entries(plan.entities).map(([sourceEntity, row]) => ({ sourceEntity, ...row }));
34 }
35 return [];
36}
37function mergeKnown(known, supplement, closed = false) {
38 if (known === undefined) return structuredClone(supplement);
39 if (known && supplement && typeof known === 'object' && typeof supplement === 'object' && !Array.isArray(known) && !Array.isArray(supplement)) {
40 const result = structuredClone(known);
41 for (const key of Object.keys(supplement)) {
42 if (['__proto__', 'constructor', 'prototype'].includes(key)) fail('mapping_assessment_invalid');
43 if (closed && !Object.hasOwn(known, key)) fail('mapping_assessment_known_rule_conflict');
44 result[key] = mergeKnown(known[key], supplement[key], closed);
45 }
46 return result;
47 }
48 if (stable(known) !== stable(supplement)) fail('mapping_assessment_known_rule_conflict');
49 return structuredClone(known);
50}
51function mergeContract(known, supplement) {
52 for (const [ref, dependency] of Object.entries(supplement.dependencies || {})) {
53 if (Object.hasOwn(known?.dependencies || {}, ref)) mergeKnown(known.dependencies[ref], dependency, true);
54 }
55 return mergeKnown(known, supplement);
56}
57function validateSourceScopes(selected, coverage) {
58 if (!selected.length) fail('mapping_assessment_scope_stale');
59 if (coverage.sourceScopes === undefined) {
60 // Compatibility for single-source assessments; multiple sources must name every scope.
61 if (selected.length !== 1 || !coverage.sourceSnapshotDigest || !coverage.inventoryDigest ||
62 selected[0].discoveryCoverage?.sourceSnapshotDigest !== coverage.sourceSnapshotDigest ||
63 selected[0].discoveryCoverage?.inventoryDigest !== coverage.inventoryDigest) fail('mapping_assessment_scope_stale');
64 return;
65 }
66 const scopes = coverage.sourceScopes;
67 const ref = require('./mapping-field-contract').entityRef;
68 if (!Array.isArray(scopes) || scopes.length !== selected.length ||
69 new Set(scopes.map(s => s?.sourceEntityRef)).size !== scopes.length ||
70 new Set(selected.map(ref)).size !== selected.length || selected.some(row => {
71 const scope = scopes.find(s => s?.sourceEntityRef && s.sourceEntityRef === ref(row));
72 return !scope?.sourceSnapshotDigest || !scope.inventoryDigest ||
73 scope.sourceSnapshotDigest !== row.discoveryCoverage?.sourceSnapshotDigest ||
74 scope.inventoryDigest !== row.discoveryCoverage?.inventoryDigest;
75 })) fail('mapping_assessment_scope_stale');
76}
77function validate(plan, projectDir) {
78 const assessments = plan?.operationAssessments;
79 if (assessments === undefined) return [];
80 if (!Array.isArray(assessments)) fail('mapping_assessment_invalid');
81 const keys = new Set();
82 for (const a of assessments) {
83 if (!a || !REF.test(a.targetRef || '') || !/^[A-Za-z][A-Za-z0-9]*$/.test(a.operation || '') ||
84 !a.coverage || a.coverage.complete !== true || !Array.isArray(a.coverage.referencePaths) || !Array.isArray(a.evidence) || !a.evidence.length || !a.contract?.dependencies ||
85 !Array.isArray(a.examples) || !a.examples.length) fail('mapping_assessment_invalid');
86 const key = a.targetRef + ':' + a.operation;
87 if (keys.has(key)) fail('mapping_assessment_duplicate');
88 keys.add(key);
89 const selected = rows(plan).filter(r => r.targetRef === a.targetRef);
90 validateSourceScopes(selected, a.coverage);
91 const bundled = require('./dependency-proofs').bundledDomain(a.targetRef);
92 const known = Object.entries(bundled.operationContracts || {}).find(([name, c]) => name === a.operation || c.aliases?.includes(a.operation));
93 mergeContract(known?.[1], a.contract);
94 if (!a.evidence.some(e => e.kind === 'authoritative-schema' && typeof e.revision === 'string' && e.revision)) fail('mapping_assessment_schema_missing');
95 for (const e of a.evidence) artifact(projectDir, e);
96 if (!a.examples.every(e => e && typeof e.id === 'string' && e.id && e.input && typeof e.input === 'object' &&
97 Object.hasOwn(e, 'expected'))) fail('mapping_assessment_examples_missing');
98 for (const [ref, dep] of Object.entries(a.contract.dependencies)) {
99 if (!REF.test(ref) || !dep || typeof dep !== 'object' ||
100 (!dep.inapplicable && (!Array.isArray(dep.guardedPaths) || !dep.guardedPaths.length)) ||
101 (dep.guardedPaths || []).some(p => typeof (typeof p === 'string' ? p : p?.path) !== 'string')) fail('mapping_assessment_reference_unknown');
102 if (a.dependencyModes?.[ref] !== undefined && !['per-record', 'task-complete'].includes(a.dependencyModes[ref])) fail('mapping_assessment_mode_invalid');
103 }
104 if (Object.keys(a.dependencyModes || {}).some(ref => !a.contract.dependencies[ref])) fail('mapping_assessment_mode_invalid');
105 const paths = Object.values(a.contract.dependencies).filter(d=>!d.inapplicable).flatMap(d=>d.guardedPaths.map(p=>typeof p==='string'?p:p.path));
106 if (stable([...new Set(paths)].sort())!==stable([...new Set(a.coverage.referencePaths)].sort())) fail('mapping_assessment_reference_coverage_missing');
107 }
108 return assessments;
109}
110function withPlan(plan, projectDir, fn) {
111 projectDir ||= context.getStore()?.projectDir;
112 if (context.getStore()?.plan === plan && context.getStore()?.projectDir === projectDir) return fn();
113 const assessments = plan?.operationAssessments || [];
114 if (!Array.isArray(assessments) || assessments.some(a=>!a || !REF.test(a.targetRef || ''))) fail('mapping_assessment_invalid');
115 // A bad assessment belongs to its target operation. Independent targets still get
116 // their own normal checks; authority persistence separately validates the whole plan.
117 return context.run({ plan, projectDir, assessments, validated:new Set() }, fn);
118}
119function withProject(projectDir, fn) {
120 const file = path.join(projectDir, 'mapping/mapping-plan.json');
121 // Old plans without supplements retain their prior behavior, including missing-file diagnostics.
122 let plan;
123 try { plan = require('./cached-json').readJson(file, {}); }
124 catch(error) {
125 if (!(error instanceof SyntaxError)) throw error;
126 // Keep each consumer's existing malformed-plan diagnostic. No local assessment is
127 // installed; import/transport still re-read and validate their required plan themselves.
128 return context.run({plan:{},projectDir,assessments:[]},fn);
129 }
130 return withPlan(plan, projectDir, fn);
131}
132function effectiveDomain(ref, bundled) {
133 const scope=context.getStore();
134 const assessments = (scope?.assessments || []).filter(a => a.targetRef === ref);
135 if (!assessments.length) return bundled;
136 if (!scope.validated.has(ref)) { validate({...scope.plan,operationAssessments:assessments},scope.projectDir); scope.validated.add(ref); }
137 const result = structuredClone(bundled);
138 result.operationContracts ||= {};
139 for (const a of assessments) {
140 const alias = Object.entries(result.operationContracts).find(([name, c]) => name === a.operation || c.aliases?.includes(a.operation));
141 const name = alias?.[0] || a.operation;
142 result.operationContracts[name] = mergeContract(alias?.[1], a.contract);
143 if ((bundled.dependsOn || []).some(ref=>!result.operationContracts[name].dependencies?.[ref])) fail('mapping_assessment_reference_unknown');
144 // The effective digest binds the used evidence as well as the contract semantics.
145 result.operationContracts[name].assessmentBinding = digest({ coverage: a.coverage, evidence: a.evidence, examples: a.examples });
146 result.dependencyModes = mergeKnown(result.dependencyModes || {}, a.dependencyModes || {});
147 const referenceFields = {};
148 for (const [dependency, d] of Object.entries(a.contract.dependencies)) if (!d.inapplicable) {
149 // Discriminator guards are proof checks, not target identity locations.
150 referenceFields[dependency] = d.guardedPaths.filter(p=>typeof p==='string' || p.equals===undefined).map(p=>typeof p==='string'?p:p.path);
151 }
152 result.operationContracts[name].assessmentReferenceFields=referenceFields;
153 }
154 return result;
155}
156module.exports = { validate, artifact, withPlan, withProject, effectiveDomain, mergeKnown, digest, current: () => context.getStore() };