Setting the file. One moment. Blog Post Build Duplicates · Rp Target Wix · wix/skills · Skills Docs41.10
Multilingual Restore
async function finishPendingEvidence
— line 93
This file
- Number
- 41.124
- Position
- 124 of 174
- Type
- JavaScript
- Size
- 11 KB
- Lines
- 203
lib/blog-post-build-duplicates.js
JavaScript·203 lines·11 KB
}
\u0000
${
extraId
}`
; }
8function currentPair(projectDir, key, extraId) {
9 return ledger.fold(ledger.readRows(projectDir)).duplicates.get(pairKey(key, extraId));
10}
11function append(projectDir, key, recordedDraftId, extraDraftId, event, rest = {}) {
12 return ledger.appendRow(projectDir, { kind: 'duplicate', sourceStableKey: key,
13 recordedDraftId, extraDraftId, event, ...rest });
14}
15function isNotFound(error) {
16 if (error?.status !== 404) return false;
17 const code = error.code || error.applicationErrorCode || error.details?.code ||
18 error.body?.details?.applicationError?.code;
19 const message = error.body?.message || error.message || '';
20 return ['NOT_FOUND', 'ENTITY_NOT_FOUND'].includes(code) ||
21 (!code && error.outcomeKnown === true && /^entity not found\b/i.test(String(message)));
22}
23async function readOrNull(fn, id) {
24 try { return await fn(id); } catch (error) { if (isNotFound(error)) return null; throw error; }
25}
26async function listAll(client) {
27 const drafts = [];
28 let cursor = null;
29 const cursors = new Set();
30 for (let n = 0; n < 1000; n += 1) {
31 const page = await client.listDrafts({ cursor });
32 if (!Array.isArray(page?.drafts)) throw new Error('draft listing lacks drafts array');
33 drafts.push(...page.drafts);
34 if (!page.nextCursor) return drafts;
35 if (cursors.has(page.nextCursor)) throw new Error('draft listing cursor repeated');
36 cursors.add(page.nextCursor);
37 cursor = page.nextCursor;
38 }
39 throw new Error('draft listing page cap reached');
40}
41function sourceRows(state, key) {
42 return state.posts.get(key)?.rows || [];
43}
44function intentAndCreate(state, key) {
45 const rows = sourceRows(state, key);
46 const create = [...rows].reverse().find(row => row.kind === 'phase' && ['drafted', 'contain-drafted'].includes(row.phase));
47 return {
48 intent: [...rows].reverse().find(row => row.seq < create?.seq && row.kind === 'phase' &&
49 ['creating', 'contain-recreating'].includes(row.phase)),
50 create,
51 };
52}
53function recordedElsewhere(state, id, key, crosswalk) {
54 if (typeof crosswalk?.idOwners !== 'function') return true;
55 for (const [otherKey, post] of state.posts) {
56 if (otherKey !== key && post.rows.some(row => row.draftId === id || row.postId === id)) return true;
57 }
58 return Boolean(crosswalk?.idOwners?.(id)?.some(owner => String(owner) !== key));
59}
60function assessProvenance(state, key, extraId, draft, crosswalk) {
61 const { intent, create } = intentAndCreate(state, key);
62 const sourceFp = intent?.fingerprint;
63 const creation = Date.parse(draft?.createdDate || draft?.createdAt);
64 const edited = Date.parse(draft?.editedDate || draft?.updatedDate);
65 const started = Date.parse(intent?.ts);
66 const finished = Date.parse(create?.ts);
67 const tests = {
68 fingerprint: Boolean(draft && sourceFp && ledger.fingerprint(draft) === sourceFp),
69 storedBody: Boolean(draft && create?.storedBodySha && ledger.bodySha(draft) === create.storedBodySha),
70 creationWindow: Number.isFinite(creation) && Number.isFinite(started) && Number.isFinite(finished) &&
71 creation >= started - 5 * 60_000 && creation <= finished,
72 unedited: Number.isFinite(edited) && Number.isFinite(creation) && Math.abs(edited - creation) <= 1000,
73 unowned: !recordedElsewhere(state, extraId, key, crosswalk),
74 };
75 return { status: Object.values(tests).every(Boolean) ? 'consistent' : 'inconsistent', tests,
76 title: draft?.title || null, createdDate: draft?.createdDate || draft?.createdAt || null };
77}
78async function recordProvenance(projectDir, key, extraId, client, crosswalk, alreadyRead) {
79 const pair = currentPair(projectDir, key, extraId);
80 if (!pair || pair.closed) throw new Error('no open duplicate pair');
81 let draft = alreadyRead;
82 let readError;
83 if (!draft) {
84 try { draft = await client.getDraft(extraId); }
85 catch (error) { if (wixAuth.isRunInterruption(error)) throw error; readError = error.message; }
86 }
87 const state = ledger.fold(ledger.readRows(projectDir));
88 const evidence = draft ? assessProvenance(state, key, extraId, draft, crosswalk)
89 : { status: 'inconsistent', tests: {}, readError };
90 append(projectDir, key, pair.recordedDraftId, extraId, 'provenance', { evidence });
91 return evidence;
92}
93async function finishPendingEvidence(projectDir, client, crosswalk) {
94 const state = ledger.fold(ledger.readRows(projectDir));
95 for (const pair of state.duplicates.values()) {
96 if (!pair.closed && !pair.evidence) await recordProvenance(projectDir, pair.sourceStableKey, pair.extraDraftId, client, crosswalk);
97 }
98}
99async function detectExtraDrafts(projectDir, client, crosswalk, onlyKey) {
100 const state = ledger.fold(ledger.readRows(projectDir));
101 const targets = [...state.posts.entries()].filter(([key, post]) => (!onlyKey || key === onlyKey) &&
102 post.rows.some(row => ['drafted', 'contain-drafted'].includes(row.phase) && row.recreatedAfterIntent));
103 if (!targets.length) return [];
104 const drafts = await listAll(client);
105 const added = [];
106 for (const [key, post] of targets) {
107 const { intent, create } = intentAndCreate(state, key);
108 if (!intent || !create) continue;
109 const intentBody = [...post.rows].reverse().find(row => row.kind === 'desired-state' &&
110 row.seq < intent.seq);
111 if (!intentBody) throw new Error(`blog duplicate ${key}: create intent has no pinned body`);
112 const expectedTitle = ledger.loadGeneration(projectDir, intentBody.snapshotSha, key).desiredPost.title;
113 for (const draft of drafts) {
114 if (draft.id === create.draftId || draft.title !== expectedTitle) continue;
115 if (currentPair(projectDir, key, draft.id)) continue;
116 // List responses need not include every fingerprint field; use one full read
117 // for matching and provenance rather than missing a late-visible duplicate.
118 let full;
119 try { full = await client.getDraft(draft.id); }
120 catch (error) {
121 if (wixAuth.isRunInterruption(error)) throw error;
122 append(projectDir, key, create.draftId, draft.id, 'detected');
123 append(projectDir, key, create.draftId, draft.id, 'provenance',
124 { evidence: { status: 'inconsistent', tests: {}, readError: error.message } });
125 added.push({ sourceStableKey: key, extraDraftId: draft.id });
126 continue;
127 }
128 if (ledger.fingerprint(full) !== intent.fingerprint) continue;
129 append(projectDir, key, create.draftId, draft.id, 'detected');
130 added.push({ sourceStableKey: key, extraDraftId: draft.id });
131 await recordProvenance(projectDir, key, draft.id, client, crosswalk, full);
132 }
133 }
134 return added;
135}
136function ensureEvidence(pair, expected = 'consistent') {
137 if (!pair || pair.closed || pair.evidence?.status !== expected) throw new Error(`duplicate needs current ${expected} evidence`);
138}
139function confirmOwnership(projectDir, key, extraId, chosenBy, note) {
140 return ledger.withLock(projectDir, async () => {
141 const pair = currentPair(projectDir, key, extraId);
142 ensureEvidence(pair);
143 if (!String(chosenBy || '').trim() || !String(note || '').trim()) throw new Error('ownership needs named person and their explanation');
144 const row = append(projectDir, key, pair.recordedDraftId, extraId, 'ownership-confirmed', { chosenBy, note });
145 return { row, ...(await reports.refreshBlogHandoff(projectDir)) };
146 });
147}
148function keepOrNotImporter(projectDir, key, extraId, event, chosenBy, note) {
149 return ledger.withLock(projectDir, async () => {
150 const pair = currentPair(projectDir, key, extraId);
151 if (!pair || pair.closed) throw new Error('no open duplicate pair');
152 if (!['kept', 'not-importer'].includes(event)) throw new Error('invalid duplicate closure');
153 if (!String(note || '').trim()) throw new Error('the person’s note is required');
154 if (event === 'kept' && !String(chosenBy || '').trim()) throw new Error('chosenBy required');
155 const row = append(projectDir, key, pair.recordedDraftId, extraId, event, { chosenBy, note });
156 return { row, ...(await reports.refreshBlogHandoff(projectDir)) };
157 });
158}
159async function deleteConfirmedExtra(projectDir, key, extraId, chosenBy, note, client, crosswalk) {
160 return ledger.withLock(projectDir, async () => {
161 let pair = currentPair(projectDir, key, extraId);
162 ensureEvidence(pair);
163 if (!pair.confirmation || !String(chosenBy || '').trim() || !String(note || '').trim()) {
164 throw new Error('current ownership confirmation and a separate named delete choice required');
165 }
166 if (!pair.choice) append(projectDir, key, pair.recordedDraftId, extraId, 'delete-chosen', { chosenBy, note });
167 const draft = await readOrNull(value => client.getDraft(value), extraId);
168 if (!draft) {
169 const post = await readOrNull(value => client.getPost(value), extraId);
170 if (!post) {
171 append(projectDir, key, pair.recordedDraftId, extraId, 'removed');
172 return (await reports.refreshBlogHandoff(projectDir)).report;
173 }
174 append(projectDir, key, pair.recordedDraftId, extraId, 'unresolved',
175 { evidence: { status: 'inconsistent', tests: { unpublished: false } } });
176 return (await reports.refreshBlogHandoff(projectDir)).report;
177 }
178 const evidence = assessProvenance(ledger.fold(ledger.readRows(projectDir)), key, extraId, draft, crosswalk);
179 if (evidence.status !== 'consistent') {
180 append(projectDir, key, pair.recordedDraftId, extraId, 'unresolved', { evidence });
181 return (await reports.refreshBlogHandoff(projectDir)).report;
182 }
183 if (typeof client.deleteDraft !== 'function') throw new Error('client needs deleteDraft for chosen duplicate deletion');
184 await client.deleteDraft(extraId);
185 const draftAfter = await readOrNull(value => client.getDraft(value), extraId);
186 const postAfter = await readOrNull(value => client.getPost(value), extraId);
187 if (draftAfter || postAfter) throw new Error('extra draft deletion not confirmed on both surfaces');
188 pair = currentPair(projectDir, key, extraId);
189 append(projectDir, key, pair.recordedDraftId, extraId, 'removed');
190 return (await reports.refreshBlogHandoff(projectDir)).report;
191 });
192}
193
194function recheckProvenance(projectDir, key, extraId, client, crosswalk) {
195 return ledger.withLock(projectDir, async () => {
196 const evidence = await recordProvenance(projectDir, key, extraId, client, crosswalk);
197 return { evidence, ...(await reports.refreshBlogHandoff(projectDir)) };
198 });
199}
200
201module.exports = { pairKey, currentPair, assessProvenance,
202 finishPendingEvidence, detectExtraDrafts, confirmOwnership, keepOrNotImporter,
203 deleteConfirmedExtra, recheckProvenance };