Setting the file. One moment. Artifact Freshness · Replatform · wix/skills · Skills Docs199
function writeImportPlanDelta
— line 199
This file
- Number
- 21.8
- Position
- 8 of 20
- Type
- JavaScript
- Size
- 7 KB
- Lines
- 215
lib/artifact-freshness.js
JavaScript·215 lines·7 KB
=
{
10 sourceSchema: 'source-schema.json',
11 mappingPlan: 'mapping/mapping-plan.json',
12 setupVerification: 'setup/setup-verification.json',
13 generatedImportCode: 'src/import',
14};
15
16function sha256Text(text) {
17 return crypto.createHash('sha256').update(text).digest('hex');
18}
19
20function fileHash(filePath) {
21 if (!fs.existsSync(filePath)) return null;
22 const stat = fs.statSync(filePath);
23 if (!stat.isFile()) return null;
24 return sha256Text(fs.readFileSync(filePath));
25}
26
27function directoryHash(dirPath) {
28 if (!fs.existsSync(dirPath)) return null;
29 const entries = listFilesRecursive(dirPath)
30 .map((file) => {
31 const relative = path.relative(dirPath, file).replace(/\\/g, '/');
32 return `${relative}\0${fileHash(file)}`;
33 })
34 .join('\n');
35 return sha256Text(entries);
36}
37
38function listFilesRecursive(root) {
39 const files = [];
40 const entries = fs.readdirSync(root, { withFileTypes: true })
41 .filter((entry) => !entry.name.startsWith('.'))
42 .sort((a, b) => a.name.localeCompare(b.name));
43 for (const entry of entries) {
44 const fullPath = path.join(root, entry.name);
45 if (entry.isDirectory()) {
46 files.push(...listFilesRecursive(fullPath));
47 } else if (entry.isFile()) {
48 files.push(fullPath);
49 }
50 }
51 return files;
52}
53
54function hashArtifact(projectDir, relativePath) {
55 if (!relativePath) return null;
56 const absolute = path.resolve(projectDir, relativePath);
57 if (!fs.existsSync(absolute)) return null;
58 const stat = fs.statSync(absolute);
59 if (stat.isDirectory()) return directoryHash(absolute);
60 if (stat.isFile()) return fileHash(absolute);
61 return null;
62}
63
64function targetLedgerRevision(domainsDir) {
65 if (!domainsDir || !fs.existsSync(domainsDir)) return null;
66 return directoryHash(domainsDir);
67}
68
69function createFreshnessMetadata({
70 projectDir,
71 domainsDir,
72 artifactPaths = DEFAULT_ARTIFACTS,
73 generatedImportCodeRevision = null,
74 createdAt = new Date().toISOString(),
75} = {}) {
76 if (!projectDir) throw new Error('projectDir is required');
77 return {
78 schemaVersion: SCHEMA_VERSION,
79 createdAt,
80 artifacts: {
81 sourceSchema: {
82 path: artifactPaths.sourceSchema || DEFAULT_ARTIFACTS.sourceSchema,
83 sha256: hashArtifact(projectDir, artifactPaths.sourceSchema || DEFAULT_ARTIFACTS.sourceSchema),
84 },
85 mappingPlan: {
86 path: artifactPaths.mappingPlan || DEFAULT_ARTIFACTS.mappingPlan,
87 sha256: hashArtifact(projectDir, artifactPaths.mappingPlan || DEFAULT_ARTIFACTS.mappingPlan),
88 },
89 setupVerification: {
90 path: artifactPaths.setupVerification || DEFAULT_ARTIFACTS.setupVerification,
91 sha256: hashArtifact(projectDir, artifactPaths.setupVerification || DEFAULT_ARTIFACTS.setupVerification),
92 },
93 generatedImportCode: {
94 path: artifactPaths.generatedImportCode || DEFAULT_ARTIFACTS.generatedImportCode,
95 sha256: hashArtifact(projectDir, artifactPaths.generatedImportCode || DEFAULT_ARTIFACTS.generatedImportCode),
96 revision: generatedImportCodeRevision,
97 },
98 },
99 targetContractLedger: {
100 path: domainsDir || null,
101 revision: targetLedgerRevision(domainsDir),
102 },
103 };
104}
105
106function compareFreshnessMetadata(current, recorded) {
107 const changes = [];
108 if (!recorded || typeof recorded !== 'object') {
109 return { ok: false, stale: true, changes: [{ field: 'metadata', reason: 'missing' }] };
110 }
111 if (recorded.schemaVersion !== SCHEMA_VERSION) {
112 changes.push({ field: 'schemaVersion', expected: SCHEMA_VERSION, actual: recorded.schemaVersion });
113 }
114 for (const name of Object.keys(current.artifacts || {})) {
115 const currentEntry = current.artifacts[name] || {};
116 const recordedEntry = (recorded.artifacts && recorded.artifacts[name]) || {};
117 if (currentEntry.sha256 !== recordedEntry.sha256) {
118 changes.push({
119 field: `artifacts.${name}.sha256`,
120 path: currentEntry.path || recordedEntry.path || null,
121 expected: recordedEntry.sha256 || null,
122 actual: currentEntry.sha256 || null,
123 });
124 }
125 if (name === 'generatedImportCode' && currentEntry.revision !== recordedEntry.revision) {
126 changes.push({
127 field: 'artifacts.generatedImportCode.revision',
128 expected: recordedEntry.revision || null,
129 actual: currentEntry.revision || null,
130 });
131 }
132 }
133 const currentLedger = current.targetContractLedger || {};
134 const recordedLedger = recorded.targetContractLedger || {};
135 if (currentLedger.revision !== recordedLedger.revision) {
136 changes.push({
137 field: 'targetContractLedger.revision',
138 path: currentLedger.path || recordedLedger.path || null,
139 expected: recordedLedger.revision || null,
140 actual: currentLedger.revision || null,
141 });
142 }
143 return { ok: changes.length === 0, stale: changes.length > 0, changes };
144}
145
146function validateImportPlanFreshness({
147 projectDir,
148 domainsDir,
149 metadataPath = 'execution/review/import-plan.freshness.json',
150 artifactPaths = DEFAULT_ARTIFACTS,
151 generatedImportCodeRevision = null,
152} = {}) {
153 const absoluteMetadataPath = path.resolve(projectDir, metadataPath);
154 const current = createFreshnessMetadata({
155 projectDir,
156 domainsDir,
157 artifactPaths,
158 generatedImportCodeRevision,
159 });
160 if (!fs.existsSync(absoluteMetadataPath)) {
161 return {
162 ok: false,
163 stale: true,
164 current,
165 recorded: null,
166 changes: [{ field: metadataPath, reason: 'missing' }],
167 };
168 }
169 const recorded = JSON.parse(fs.readFileSync(absoluteMetadataPath, 'utf8'));
170 const comparison = compareFreshnessMetadata(current, recorded);
171 return { ...comparison, current, recorded };
172}
173
174function writeFreshnessMetadata(filePath, metadata) {
175 fs.mkdirSync(path.dirname(filePath), { recursive: true });
176 fs.writeFileSync(filePath, `${JSON.stringify(metadata, null, 2)}\n`);
177}
178
179function renderImportPlanDelta({ staleResult, generatedAt = new Date().toISOString() }) {
180 const lines = [
181 '# Import Plan Delta',
182 '',
183 `Generated: ${generatedAt}`,
184 '',
185 'The approved import plan is stale relative to current artifacts.',
186 '',
187 '## Changed Assumptions',
188 '',
189 ];
190 for (const change of staleResult.changes || []) {
191 const pathPart = change.path ? ` (${change.path})` : '';
192 const reasonPart = change.reason ? `: ${change.reason}` : '';
193 lines.push(`- ${change.field}${pathPart}${reasonPart}`);
194 }
195 lines.push('', 'Regenerate `execution/review/import-plan.md` or accept this delta before live writes/final reporting.');
196 return `${lines.join('\n')}\n`;
197}
198
199function writeImportPlanDelta(filePath, staleResult) {
200 fs.mkdirSync(path.dirname(filePath), { recursive: true });
201 fs.writeFileSync(filePath, renderImportPlanDelta({ staleResult }));
202}
203
204module.exports = {
205 SCHEMA_VERSION,
206 DEFAULT_ARTIFACTS,
207 createFreshnessMetadata,
208 compareFreshnessMetadata,
209 validateImportPlanFreshness,
210 writeFreshnessMetadata,
211 renderImportPlanDelta,
212 writeImportPlanDelta,
213 hashArtifact,
214 targetLedgerRevision,
215};