@tiangong-lca/cli 0.0.6 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -30
- package/dist/src/cli.js +912 -113
- package/dist/src/cli.js.map +1 -1
- package/dist/src/lib/dataset-local.js +231 -0
- package/dist/src/lib/dataset-local.js.map +1 -0
- package/dist/src/lib/dataset-references-rewrite.js +214 -0
- package/dist/src/lib/dataset-references-rewrite.js.map +1 -0
- package/dist/src/lib/dataset-validate.js +191 -0
- package/dist/src/lib/dataset-validate.js.map +1 -0
- package/dist/src/lib/flow-regen-product.js +2 -10
- package/dist/src/lib/flow-regen-product.js.map +1 -1
- package/dist/src/lib/flow-remediate.js +4 -8
- package/dist/src/lib/flow-remediate.js.map +1 -1
- package/dist/src/lib/lifecyclemodel-auto-build.js +11 -8
- package/dist/src/lib/lifecyclemodel-auto-build.js.map +1 -1
- package/dist/src/lib/lifecyclemodel-graph.js +248 -0
- package/dist/src/lib/lifecyclemodel-graph.js.map +1 -0
- package/dist/src/lib/lifecyclemodel-publish-build.js +1 -1
- package/dist/src/lib/lifecyclemodel-publish-build.js.map +1 -1
- package/dist/src/lib/lifecyclemodel-save-draft-run.js +245 -0
- package/dist/src/lib/lifecyclemodel-save-draft-run.js.map +1 -0
- package/dist/src/lib/lifecyclemodel-validate-build.js +1 -1
- package/dist/src/lib/lifecyclemodel-validate-build.js.map +1 -1
- package/dist/src/lib/process-auto-build.js +11 -8
- package/dist/src/lib/process-auto-build.js.map +1 -1
- package/dist/src/lib/process-batch-build.js +6 -3
- package/dist/src/lib/process-batch-build.js.map +1 -1
- package/dist/src/lib/process-dedup-review.js +870 -0
- package/dist/src/lib/process-dedup-review.js.map +1 -0
- package/dist/src/lib/process-payload-validation.js +50 -0
- package/dist/src/lib/process-payload-validation.js.map +1 -0
- package/dist/src/lib/process-publish-build.js +4 -6
- package/dist/src/lib/process-publish-build.js.map +1 -1
- package/dist/src/lib/process-refresh-references.js +1028 -0
- package/dist/src/lib/process-refresh-references.js.map +1 -0
- package/dist/src/lib/process-resume-build.js +4 -6
- package/dist/src/lib/process-resume-build.js.map +1 -1
- package/dist/src/lib/process-save-draft-run.js +31 -12
- package/dist/src/lib/process-save-draft-run.js.map +1 -1
- package/dist/src/lib/process-scope-statistics.js +859 -0
- package/dist/src/lib/process-scope-statistics.js.map +1 -0
- package/dist/src/lib/process-verify-rows.js +250 -0
- package/dist/src/lib/process-verify-rows.js.map +1 -0
- package/dist/src/lib/publish.js +1 -1
- package/dist/src/lib/publish.js.map +1 -1
- package/dist/src/lib/remote.js +4 -4
- package/dist/src/lib/remote.js.map +1 -1
- package/dist/src/lib/review-lifecyclemodel.js +2 -2
- package/dist/src/lib/review-lifecyclemodel.js.map +1 -1
- package/dist/src/lib/tidas-sdk-package-validator.js +28 -9
- package/dist/src/lib/tidas-sdk-package-validator.js.map +1 -1
- package/dist/src/lib/tidas-sdk-validation.js +96 -0
- package/dist/src/lib/tidas-sdk-validation.js.map +1 -0
- package/dist/src/lib/user-api-key.js +1 -1
- package/dist/src/lib/user-api-key.js.map +1 -1
- package/package.json +4 -4
- /package/bin/{tiangong.d.ts → tiangong-lca.d.ts} +0 -0
- /package/bin/{tiangong.js → tiangong-lca.js} +0 -0
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { writeJsonArtifact } from './artifacts.js';
|
|
3
|
+
import { CliError } from './errors.js';
|
|
4
|
+
import { readJsonInput } from './io.js';
|
|
5
|
+
import { deriveSupabaseProjectBaseUrl, requireSupabaseRestRuntime } from './supabase-client.js';
|
|
6
|
+
import { resolveSupabaseUserSession } from './supabase-session.js';
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
8
|
+
const DEFAULT_MAX_RETRIES = 4;
|
|
9
|
+
const DEFAULT_PAGE_SIZE = 100;
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function trimText(value) {
|
|
14
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
15
|
+
}
|
|
16
|
+
function errorMessage(error) {
|
|
17
|
+
return error instanceof Error ? error.message : String(error);
|
|
18
|
+
}
|
|
19
|
+
function nowIso(now = new Date()) {
|
|
20
|
+
return now.toISOString();
|
|
21
|
+
}
|
|
22
|
+
function requiredNonEmpty(value, label, code) {
|
|
23
|
+
const normalized = value.trim();
|
|
24
|
+
if (!normalized) {
|
|
25
|
+
throw new CliError(`Missing required ${label}.`, {
|
|
26
|
+
code,
|
|
27
|
+
exitCode: 2,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
function toPositiveInteger(value, label, code) {
|
|
33
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
34
|
+
throw new CliError(`Expected ${label} to be a positive integer.`, {
|
|
35
|
+
code,
|
|
36
|
+
exitCode: 2,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function normalizeOptionalToken(value) {
|
|
42
|
+
const normalized = trimText(value);
|
|
43
|
+
return normalized ? normalized : null;
|
|
44
|
+
}
|
|
45
|
+
function asArray(value) {
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
if (value === null || value === undefined || value === '') {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
return [value];
|
|
53
|
+
}
|
|
54
|
+
function toGroupId(value, fallback) {
|
|
55
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
const normalized = trimText(value);
|
|
59
|
+
if (!normalized) {
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
if (/^\d+$/u.test(normalized)) {
|
|
63
|
+
return Number.parseInt(normalized, 10);
|
|
64
|
+
}
|
|
65
|
+
return normalized;
|
|
66
|
+
}
|
|
67
|
+
function normalizeExchangeCount(value) {
|
|
68
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
const normalized = trimText(value);
|
|
72
|
+
if (!normalized) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
76
|
+
return Number.isInteger(parsed) ? parsed : null;
|
|
77
|
+
}
|
|
78
|
+
function normalizeAmount(value) {
|
|
79
|
+
const normalized = trimText(value);
|
|
80
|
+
if (!normalized) {
|
|
81
|
+
return '';
|
|
82
|
+
}
|
|
83
|
+
if (/^[+-]?\d+(?:\.\d+)?$/u.test(normalized)) {
|
|
84
|
+
let result = normalized.replace(/^\+/u, '');
|
|
85
|
+
result = result.replace(/^(-?)0+(?=\d)/u, '$1');
|
|
86
|
+
if (result.includes('.')) {
|
|
87
|
+
result = result.replace(/0+$/u, '').replace(/\.$/u, '');
|
|
88
|
+
}
|
|
89
|
+
if (result === '' || result === '-' || result === '-0') {
|
|
90
|
+
return '0';
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
const parsed = Number(normalized);
|
|
95
|
+
if (Number.isFinite(parsed)) {
|
|
96
|
+
return parsed.toString();
|
|
97
|
+
}
|
|
98
|
+
return normalized;
|
|
99
|
+
}
|
|
100
|
+
function getLangList(value) {
|
|
101
|
+
if (value === null || value === undefined || value === '') {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(value)) {
|
|
105
|
+
return value.filter((entry) => isRecord(entry));
|
|
106
|
+
}
|
|
107
|
+
if (isRecord(value)) {
|
|
108
|
+
const langString = value['common:langString'];
|
|
109
|
+
if (Array.isArray(langString)) {
|
|
110
|
+
return langString.filter((entry) => isRecord(entry));
|
|
111
|
+
}
|
|
112
|
+
if (isRecord(langString)) {
|
|
113
|
+
return [langString];
|
|
114
|
+
}
|
|
115
|
+
if (value['#text'] !== undefined || value['@xml:lang'] !== undefined) {
|
|
116
|
+
return [value];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (typeof value === 'string') {
|
|
120
|
+
return [{ '@xml:lang': 'en', '#text': value }];
|
|
121
|
+
}
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
function getLangText(value, lang) {
|
|
125
|
+
const entries = getLangList(value);
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (trimText(entry['@xml:lang']).toLowerCase() === lang.toLowerCase() &&
|
|
128
|
+
trimText(entry['#text'])) {
|
|
129
|
+
return trimText(entry['#text']);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
if (trimText(entry['#text'])) {
|
|
134
|
+
return trimText(entry['#text']);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return '';
|
|
138
|
+
}
|
|
139
|
+
function normalizeExchangeSummaryRow(value) {
|
|
140
|
+
if (!isRecord(value)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const flowRef = isRecord(value.referenceToFlowDataSet) ? value.referenceToFlowDataSet : {};
|
|
144
|
+
const flowShortDescription = value.flow_short_description ?? flowRef['common:shortDescription'];
|
|
145
|
+
const normalized = {
|
|
146
|
+
exchange_internal_id: trimText(value.exchange_internal_id ?? value['@dataSetInternalID']),
|
|
147
|
+
flow_id: trimText(value.flow_id ?? flowRef['@refObjectId']),
|
|
148
|
+
flow_version: trimText(value.flow_version ?? flowRef['@version']),
|
|
149
|
+
direction: trimText(value.direction ?? value.exchangeDirection),
|
|
150
|
+
mean_amount: trimText(value.mean_amount ?? value.meanAmount),
|
|
151
|
+
resulting_amount: trimText(value.resulting_amount ?? value.resultingAmount),
|
|
152
|
+
flow_short_description_en: trimText(value.flow_short_description_en) || getLangText(flowShortDescription, 'en'),
|
|
153
|
+
flow_short_description_zh: trimText(value.flow_short_description_zh) || getLangText(flowShortDescription, 'zh'),
|
|
154
|
+
};
|
|
155
|
+
if (!normalized.exchange_internal_id &&
|
|
156
|
+
!normalized.flow_id &&
|
|
157
|
+
!normalized.direction &&
|
|
158
|
+
!normalized.mean_amount &&
|
|
159
|
+
!normalized.resulting_amount) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return normalized;
|
|
163
|
+
}
|
|
164
|
+
function normalizeExchangeRows(value) {
|
|
165
|
+
return asArray(value)
|
|
166
|
+
.map((entry) => normalizeExchangeSummaryRow(entry))
|
|
167
|
+
.filter((entry) => entry !== null);
|
|
168
|
+
}
|
|
169
|
+
function normalizeInputProcess(value) {
|
|
170
|
+
if (!isRecord(value)) {
|
|
171
|
+
throw new CliError('Each process dedup candidate must be a JSON object.', {
|
|
172
|
+
code: 'PROCESS_DEDUP_INPUT_INVALID_PROCESS',
|
|
173
|
+
exitCode: 2,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const processId = requiredNonEmpty(trimText(value.process_id ?? value.id), 'process_id', 'PROCESS_DEDUP_PROCESS_ID_REQUIRED');
|
|
177
|
+
return {
|
|
178
|
+
process_id: processId,
|
|
179
|
+
version: trimText(value.version),
|
|
180
|
+
name_en: trimText(value.name_en ?? value.remote_name_en),
|
|
181
|
+
name_zh: trimText(value.name_zh ?? value.remote_name_zh),
|
|
182
|
+
exchange_count: normalizeExchangeCount(value.exchange_count),
|
|
183
|
+
overview_fingerprint: trimText(value.overview_fingerprint),
|
|
184
|
+
sheet_exchange_rows: normalizeExchangeRows(value.sheet_exchange_rows ??
|
|
185
|
+
value.analysis_exchanges ??
|
|
186
|
+
value.remote_exchanges ??
|
|
187
|
+
value.exchanges),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function normalizeInputGroup(value, fallbackGroupId, index) {
|
|
191
|
+
if (!isRecord(value)) {
|
|
192
|
+
throw new CliError('Each process dedup group must be a JSON object.', {
|
|
193
|
+
code: 'PROCESS_DEDUP_INPUT_INVALID_GROUP',
|
|
194
|
+
exitCode: 2,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const processes = asArray(value.processes).map((entry) => normalizeInputProcess(entry));
|
|
198
|
+
if (processes.length === 0) {
|
|
199
|
+
throw new CliError(`Process dedup group ${index + 1} is missing processes.`, {
|
|
200
|
+
code: 'PROCESS_DEDUP_GROUP_PROCESSES_REQUIRED',
|
|
201
|
+
exitCode: 2,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
group_id: toGroupId(value.group_id, fallbackGroupId),
|
|
206
|
+
processes,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function normalizeInputDocument(value, resolvedInputPath) {
|
|
210
|
+
if (!isRecord(value)) {
|
|
211
|
+
throw new CliError('Process dedup review input must be a JSON object.', {
|
|
212
|
+
code: 'PROCESS_DEDUP_INPUT_INVALID',
|
|
213
|
+
exitCode: 2,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const sourceLabel = trimText(value.source_label ?? value.source_workbook ?? value.source_file) ||
|
|
217
|
+
path.basename(resolvedInputPath);
|
|
218
|
+
const rawGroups = value.groups;
|
|
219
|
+
let groups = [];
|
|
220
|
+
if (Array.isArray(rawGroups)) {
|
|
221
|
+
groups = rawGroups.map((entry, index) => normalizeInputGroup(entry, String(index + 1), index));
|
|
222
|
+
}
|
|
223
|
+
else if (isRecord(rawGroups)) {
|
|
224
|
+
groups = Object.entries(rawGroups).map(([groupId, entry], index) => normalizeInputGroup(entry, groupId, index));
|
|
225
|
+
}
|
|
226
|
+
if (groups.length === 0) {
|
|
227
|
+
throw new CliError('Process dedup review input must contain at least one group.', {
|
|
228
|
+
code: 'PROCESS_DEDUP_GROUPS_REQUIRED',
|
|
229
|
+
exitCode: 2,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
sourceLabel,
|
|
234
|
+
groups,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async function parseJsonResponse(response, label) {
|
|
238
|
+
const text = await response.text();
|
|
239
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
240
|
+
if (!text.trim()) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
if (!contentType.includes('application/json')) {
|
|
244
|
+
return text;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
return JSON.parse(text);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
throw new CliError(`${label} returned invalid JSON.`, {
|
|
251
|
+
code: 'PROCESS_DEDUP_REMOTE_INVALID_JSON',
|
|
252
|
+
exitCode: 1,
|
|
253
|
+
details: String(error),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function fetchJsonWithRetry(options) {
|
|
258
|
+
let lastError = null;
|
|
259
|
+
for (let attempt = 1; attempt <= options.maxRetries; attempt += 1) {
|
|
260
|
+
try {
|
|
261
|
+
const response = await options.fetchImpl(options.url, {
|
|
262
|
+
...options.init,
|
|
263
|
+
signal: AbortSignal.timeout(options.timeoutMs),
|
|
264
|
+
});
|
|
265
|
+
const body = await parseJsonResponse(response, options.label);
|
|
266
|
+
if (!response.ok) {
|
|
267
|
+
throw new CliError(`${options.label} failed with ${response.status}.`, {
|
|
268
|
+
code: 'PROCESS_DEDUP_REMOTE_REQUEST_FAILED',
|
|
269
|
+
exitCode: 1,
|
|
270
|
+
details: {
|
|
271
|
+
status: response.status,
|
|
272
|
+
body,
|
|
273
|
+
url: options.url,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
status: response.status,
|
|
279
|
+
headers: new Headers({
|
|
280
|
+
'content-range': response.headers.get('content-range') ?? '',
|
|
281
|
+
'content-type': response.headers.get('content-type') ?? '',
|
|
282
|
+
}),
|
|
283
|
+
body,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
lastError = error;
|
|
288
|
+
if (attempt >= options.maxRetries) {
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
await new Promise((resolve) => setTimeout(resolve, attempt * 1_500));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (lastError instanceof CliError) {
|
|
295
|
+
throw lastError;
|
|
296
|
+
}
|
|
297
|
+
throw new CliError(`${options.label} failed after ${options.maxRetries} attempt(s).`, {
|
|
298
|
+
code: 'PROCESS_DEDUP_REMOTE_REQUEST_FAILED',
|
|
299
|
+
exitCode: 1,
|
|
300
|
+
details: String(lastError),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
async function fetchCurrentUserId(options) {
|
|
304
|
+
const response = await fetchJsonWithRetry({
|
|
305
|
+
url: `${options.projectBaseUrl}/auth/v1/user`,
|
|
306
|
+
init: {
|
|
307
|
+
method: 'GET',
|
|
308
|
+
headers: {
|
|
309
|
+
apikey: options.publishableKey,
|
|
310
|
+
Authorization: `Bearer ${options.accessToken}`,
|
|
311
|
+
Accept: 'application/json',
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
label: 'supabase current-user lookup',
|
|
315
|
+
fetchImpl: options.fetchImpl,
|
|
316
|
+
timeoutMs: options.timeoutMs,
|
|
317
|
+
maxRetries: options.maxRetries,
|
|
318
|
+
});
|
|
319
|
+
if (!isRecord(response.body)) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
return normalizeOptionalToken(response.body.id);
|
|
323
|
+
}
|
|
324
|
+
async function resolveRemoteAuthContext(options) {
|
|
325
|
+
const runtime = requireSupabaseRestRuntime(options.env);
|
|
326
|
+
const session = await resolveSupabaseUserSession({
|
|
327
|
+
runtime,
|
|
328
|
+
fetchImpl: options.fetchImpl,
|
|
329
|
+
timeoutMs: options.timeoutMs,
|
|
330
|
+
now: options.now,
|
|
331
|
+
});
|
|
332
|
+
const projectBaseUrl = deriveSupabaseProjectBaseUrl(runtime.apiBaseUrl);
|
|
333
|
+
let userId;
|
|
334
|
+
try {
|
|
335
|
+
userId = await fetchCurrentUserId({
|
|
336
|
+
projectBaseUrl,
|
|
337
|
+
publishableKey: runtime.publishableKey,
|
|
338
|
+
accessToken: session.accessToken,
|
|
339
|
+
fetchImpl: options.fetchImpl,
|
|
340
|
+
timeoutMs: options.timeoutMs,
|
|
341
|
+
maxRetries: options.maxRetries,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
userId = null;
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
projectBaseUrl,
|
|
349
|
+
publishableKey: runtime.publishableKey,
|
|
350
|
+
accessToken: session.accessToken,
|
|
351
|
+
userId,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function summarizeRemoteExchanges(processJson) {
|
|
355
|
+
if (!isRecord(processJson)) {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
const processDataSet = isRecord(processJson.processDataSet) ? processJson.processDataSet : {};
|
|
359
|
+
const exchanges = isRecord(processDataSet.exchanges) ? processDataSet.exchanges.exchange : [];
|
|
360
|
+
return normalizeExchangeRows(exchanges);
|
|
361
|
+
}
|
|
362
|
+
async function fetchRemoteMetadata(options) {
|
|
363
|
+
if (options.processIds.length === 0) {
|
|
364
|
+
return {};
|
|
365
|
+
}
|
|
366
|
+
const url = new URL(`${options.auth.projectBaseUrl}/rest/v1/processes`);
|
|
367
|
+
url.searchParams.set('select', 'id,version,state_code,created_at,modified_at,user_id,team_id,model_id,json');
|
|
368
|
+
url.searchParams.set('id', `in.(${options.processIds.join(',')})`);
|
|
369
|
+
url.searchParams.set('order', 'id.asc');
|
|
370
|
+
const response = await fetchJsonWithRetry({
|
|
371
|
+
url: url.toString(),
|
|
372
|
+
init: {
|
|
373
|
+
method: 'GET',
|
|
374
|
+
headers: {
|
|
375
|
+
apikey: options.auth.publishableKey,
|
|
376
|
+
Authorization: `Bearer ${options.auth.accessToken}`,
|
|
377
|
+
Accept: 'application/json',
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
label: 'process dedup remote metadata fetch',
|
|
381
|
+
fetchImpl: options.fetchImpl,
|
|
382
|
+
timeoutMs: options.timeoutMs,
|
|
383
|
+
maxRetries: options.maxRetries,
|
|
384
|
+
});
|
|
385
|
+
const rows = Array.isArray(response.body) ? response.body : [];
|
|
386
|
+
const byId = {};
|
|
387
|
+
for (const row of rows) {
|
|
388
|
+
if (!isRecord(row)) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const processId = trimText(row.id);
|
|
392
|
+
if (!processId) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const payload = row.json;
|
|
396
|
+
const processDataSet = isRecord(payload) && isRecord(payload.processDataSet) ? payload.processDataSet : {};
|
|
397
|
+
const processInformation = isRecord(processDataSet.processInformation)
|
|
398
|
+
? processDataSet.processInformation
|
|
399
|
+
: {};
|
|
400
|
+
const dataSetInformation = isRecord(processInformation.dataSetInformation)
|
|
401
|
+
? processInformation.dataSetInformation
|
|
402
|
+
: {};
|
|
403
|
+
const name = isRecord(dataSetInformation.name) ? dataSetInformation.name : {};
|
|
404
|
+
byId[processId] = {
|
|
405
|
+
process_id: processId,
|
|
406
|
+
version: trimText(row.version),
|
|
407
|
+
state_code: typeof row.state_code === 'number' ? row.state_code : null,
|
|
408
|
+
created_at: normalizeOptionalToken(row.created_at),
|
|
409
|
+
modified_at: normalizeOptionalToken(row.modified_at),
|
|
410
|
+
user_id: normalizeOptionalToken(row.user_id),
|
|
411
|
+
team_id: normalizeOptionalToken(row.team_id),
|
|
412
|
+
model_id: normalizeOptionalToken(row.model_id),
|
|
413
|
+
remote_name_en: getLangText(name.baseName, 'en'),
|
|
414
|
+
remote_name_zh: getLangText(name.baseName, 'zh'),
|
|
415
|
+
remote_exchanges: summarizeRemoteExchanges(payload),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
return byId;
|
|
419
|
+
}
|
|
420
|
+
async function fetchCurrentUserRows(options) {
|
|
421
|
+
const rows = [];
|
|
422
|
+
let offset = 0;
|
|
423
|
+
let total = null;
|
|
424
|
+
while (total === null || offset < total) {
|
|
425
|
+
const url = new URL(`${options.auth.projectBaseUrl}/rest/v1/${options.tableName}`);
|
|
426
|
+
url.searchParams.set('select', options.select);
|
|
427
|
+
url.searchParams.set('user_id', `eq.${options.userId}`);
|
|
428
|
+
url.searchParams.set('order', 'id.asc');
|
|
429
|
+
url.searchParams.set('limit', String(DEFAULT_PAGE_SIZE));
|
|
430
|
+
url.searchParams.set('offset', String(offset));
|
|
431
|
+
const page = await fetchJsonWithRetry({
|
|
432
|
+
url: url.toString(),
|
|
433
|
+
init: {
|
|
434
|
+
method: 'GET',
|
|
435
|
+
headers: {
|
|
436
|
+
apikey: options.auth.publishableKey,
|
|
437
|
+
Authorization: `Bearer ${options.auth.accessToken}`,
|
|
438
|
+
Accept: 'application/json',
|
|
439
|
+
Prefer: total === null ? 'count=exact' : 'count=planned',
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
label: `${options.tableName} current-user reference scan`,
|
|
443
|
+
fetchImpl: options.fetchImpl,
|
|
444
|
+
timeoutMs: options.timeoutMs,
|
|
445
|
+
maxRetries: options.maxRetries,
|
|
446
|
+
});
|
|
447
|
+
const pageRows = Array.isArray(page.body) ? page.body : [];
|
|
448
|
+
rows.push(...pageRows);
|
|
449
|
+
if (total === null) {
|
|
450
|
+
const contentRange = page.headers.get('content-range');
|
|
451
|
+
const match = contentRange ? contentRange.match(/\/(\d+)$/u) : null;
|
|
452
|
+
total = match ? Number.parseInt(match[1], 10) : rows.length;
|
|
453
|
+
}
|
|
454
|
+
if (pageRows.length === 0) {
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
offset += DEFAULT_PAGE_SIZE;
|
|
458
|
+
}
|
|
459
|
+
return rows;
|
|
460
|
+
}
|
|
461
|
+
function collectReferenceHits(root, targetIds) {
|
|
462
|
+
const hits = [];
|
|
463
|
+
const visited = new Set();
|
|
464
|
+
const walk = (value) => {
|
|
465
|
+
if (value === null || value === undefined) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (Array.isArray(value)) {
|
|
469
|
+
if (visited.has(value)) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
visited.add(value);
|
|
473
|
+
value.forEach((entry) => walk(entry));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (!isRecord(value)) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (visited.has(value)) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
visited.add(value);
|
|
483
|
+
const refObjectId = trimText(value['@refObjectId']);
|
|
484
|
+
if (refObjectId && targetIds.has(refObjectId)) {
|
|
485
|
+
hits.push(refObjectId);
|
|
486
|
+
}
|
|
487
|
+
Object.values(value).forEach((entry) => walk(entry));
|
|
488
|
+
};
|
|
489
|
+
walk(root);
|
|
490
|
+
return hits;
|
|
491
|
+
}
|
|
492
|
+
function buildEmptyReferenceHits(processIds) {
|
|
493
|
+
return {
|
|
494
|
+
processes: Object.fromEntries(processIds.map((processId) => [processId, []])),
|
|
495
|
+
lifecyclemodels: Object.fromEntries(processIds.map((processId) => [processId, []])),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
async function fetchCurrentUserReferenceHits(options) {
|
|
499
|
+
const targetIds = new Set(options.targetProcessIds);
|
|
500
|
+
const results = buildEmptyReferenceHits(options.targetProcessIds);
|
|
501
|
+
const processRows = await fetchCurrentUserRows({
|
|
502
|
+
auth: options.auth,
|
|
503
|
+
tableName: 'processes',
|
|
504
|
+
userId: options.userId,
|
|
505
|
+
select: 'id,version,state_code,user_id,model_id,json',
|
|
506
|
+
fetchImpl: options.fetchImpl,
|
|
507
|
+
timeoutMs: options.timeoutMs,
|
|
508
|
+
maxRetries: options.maxRetries,
|
|
509
|
+
});
|
|
510
|
+
for (const row of processRows) {
|
|
511
|
+
if (!isRecord(row)) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const rowId = trimText(row.id);
|
|
515
|
+
const hits = collectReferenceHits(row.json, targetIds);
|
|
516
|
+
for (const hit of hits) {
|
|
517
|
+
if (rowId === hit) {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
results.processes[hit]?.push({
|
|
521
|
+
id: rowId,
|
|
522
|
+
version: trimText(row.version),
|
|
523
|
+
state_code: typeof row.state_code === 'number' ? row.state_code : null,
|
|
524
|
+
model_id: normalizeOptionalToken(row.model_id),
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const lifecyclemodelRows = await fetchCurrentUserRows({
|
|
529
|
+
auth: options.auth,
|
|
530
|
+
tableName: 'lifecyclemodels',
|
|
531
|
+
userId: options.userId,
|
|
532
|
+
select: 'id,version,state_code,user_id,json',
|
|
533
|
+
fetchImpl: options.fetchImpl,
|
|
534
|
+
timeoutMs: options.timeoutMs,
|
|
535
|
+
maxRetries: options.maxRetries,
|
|
536
|
+
});
|
|
537
|
+
for (const row of lifecyclemodelRows) {
|
|
538
|
+
if (!isRecord(row)) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const hits = collectReferenceHits(row.json, targetIds);
|
|
542
|
+
for (const hit of hits) {
|
|
543
|
+
results.lifecyclemodels[hit]?.push({
|
|
544
|
+
id: trimText(row.id),
|
|
545
|
+
version: trimText(row.version),
|
|
546
|
+
state_code: typeof row.state_code === 'number' ? row.state_code : null,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return results;
|
|
551
|
+
}
|
|
552
|
+
function normalizedSignature(exchanges) {
|
|
553
|
+
return [...exchanges]
|
|
554
|
+
.map((exchange) => [
|
|
555
|
+
trimText(exchange.flow_id),
|
|
556
|
+
trimText(exchange.direction),
|
|
557
|
+
normalizeAmount(exchange.mean_amount),
|
|
558
|
+
normalizeAmount(exchange.resulting_amount),
|
|
559
|
+
])
|
|
560
|
+
.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
561
|
+
}
|
|
562
|
+
function normalizedSignatureKey(exchanges) {
|
|
563
|
+
return JSON.stringify(normalizedSignature(exchanges));
|
|
564
|
+
}
|
|
565
|
+
function detectGroupPattern(processes) {
|
|
566
|
+
const sample = processes.find((process) => process.analysis_exchanges.length > 0)?.analysis_exchanges;
|
|
567
|
+
if (!sample || sample.length === 0) {
|
|
568
|
+
return 'unknown';
|
|
569
|
+
}
|
|
570
|
+
const inputExchanges = sample.filter((exchange) => trimText(exchange.direction).toLowerCase() === 'input');
|
|
571
|
+
const outputExchanges = sample.filter((exchange) => trimText(exchange.direction).toLowerCase() === 'output');
|
|
572
|
+
const inputFlowIds = new Set(inputExchanges.map((exchange) => trimText(exchange.flow_id)).filter(Boolean));
|
|
573
|
+
const outputFlowIds = new Set(outputExchanges.map((exchange) => trimText(exchange.flow_id)).filter(Boolean));
|
|
574
|
+
const hasTransportInput = inputExchanges.some((exchange) => {
|
|
575
|
+
const en = trimText(exchange.flow_short_description_en).toLowerCase();
|
|
576
|
+
const zh = trimText(exchange.flow_short_description_zh);
|
|
577
|
+
return en.includes('transport;') || zh.includes('运输;');
|
|
578
|
+
});
|
|
579
|
+
if (outputFlowIds.size > 0 && [...outputFlowIds].every((flowId) => inputFlowIds.has(flowId))) {
|
|
580
|
+
return hasTransportInput ? 'transport_pass_through' : 'same_flow_pass_through';
|
|
581
|
+
}
|
|
582
|
+
return 'other';
|
|
583
|
+
}
|
|
584
|
+
function scoreProcessName(process, groupPattern) {
|
|
585
|
+
const nameEn = trimText(process.remote_name_en ?? process.name_en);
|
|
586
|
+
const nameZh = trimText(process.remote_name_zh ?? process.name_zh);
|
|
587
|
+
const lowerEn = nameEn.toLowerCase();
|
|
588
|
+
const reasons = [];
|
|
589
|
+
let score = 0;
|
|
590
|
+
if (groupPattern === 'transport_pass_through') {
|
|
591
|
+
if (lowerEn.includes('transport') || nameZh.includes('运输')) {
|
|
592
|
+
score += 30;
|
|
593
|
+
reasons.push('name matches explicit transport-service input');
|
|
594
|
+
}
|
|
595
|
+
if (lowerEn.includes('logistics') || nameZh.includes('物流')) {
|
|
596
|
+
score -= 10;
|
|
597
|
+
reasons.push('name is broader than the observed transport-service input');
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (groupPattern === 'same_flow_pass_through') {
|
|
601
|
+
if (lowerEn.includes('processing') || nameZh.includes('加工')) {
|
|
602
|
+
score -= 20;
|
|
603
|
+
reasons.push('same-flow pass-through does not support a processing label');
|
|
604
|
+
}
|
|
605
|
+
if (lowerEn.includes('reception') || nameZh.includes('接收')) {
|
|
606
|
+
score += 20;
|
|
607
|
+
reasons.push('reception matches same-flow intake/output handling');
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (lowerEn.includes('collection') || nameZh.includes('收集')) {
|
|
611
|
+
score += 20;
|
|
612
|
+
reasons.push('collection is semantically consistent with a gather/sort process');
|
|
613
|
+
}
|
|
614
|
+
if (lowerEn.includes('wastepaper')) {
|
|
615
|
+
score -= 5;
|
|
616
|
+
reasons.push("compressed English form is less standardized than 'waste paper'");
|
|
617
|
+
}
|
|
618
|
+
if (lowerEn.includes('waste paper')) {
|
|
619
|
+
score += 3;
|
|
620
|
+
reasons.push('English wording is standardized');
|
|
621
|
+
}
|
|
622
|
+
if (nameEn.includes('/') || nameZh.includes('/')) {
|
|
623
|
+
score += 5;
|
|
624
|
+
reasons.push('name keeps the broader material scope explicit');
|
|
625
|
+
}
|
|
626
|
+
return { score, reasons };
|
|
627
|
+
}
|
|
628
|
+
function createdAtKey(process) {
|
|
629
|
+
return trimText(process.created_at) || '9999-12-31T23:59:59+00:00';
|
|
630
|
+
}
|
|
631
|
+
function compareGroupIds(left, right) {
|
|
632
|
+
return String(left).localeCompare(String(right), 'en', { numeric: true });
|
|
633
|
+
}
|
|
634
|
+
function analyzeGroups(groups, remoteById, referenceHits) {
|
|
635
|
+
const duplicateGroups = [];
|
|
636
|
+
const deletePlanGroups = [];
|
|
637
|
+
for (const group of [...groups].sort((left, right) => compareGroupIds(left.group_id, right.group_id))) {
|
|
638
|
+
const processes = group.processes.map((process) => {
|
|
639
|
+
const remote = remoteById[process.process_id];
|
|
640
|
+
const analysisExchanges = remote && remote.remote_exchanges.length > 0
|
|
641
|
+
? remote.remote_exchanges
|
|
642
|
+
: process.sheet_exchange_rows;
|
|
643
|
+
const initialScore = scoreProcessName({
|
|
644
|
+
name_en: process.name_en,
|
|
645
|
+
name_zh: process.name_zh,
|
|
646
|
+
remote_name_en: remote?.remote_name_en,
|
|
647
|
+
remote_name_zh: remote?.remote_name_zh,
|
|
648
|
+
}, 'unknown');
|
|
649
|
+
return {
|
|
650
|
+
...process,
|
|
651
|
+
...remote,
|
|
652
|
+
analysis_exchanges: analysisExchanges,
|
|
653
|
+
normalized_exchange_signature: normalizedSignature(analysisExchanges),
|
|
654
|
+
name_score: initialScore.score,
|
|
655
|
+
name_score_reasons: initialScore.reasons,
|
|
656
|
+
};
|
|
657
|
+
});
|
|
658
|
+
const exactDuplicate = processes.length > 1 &&
|
|
659
|
+
new Set(processes.map((process) => normalizedSignatureKey(process.analysis_exchanges)))
|
|
660
|
+
.size === 1;
|
|
661
|
+
const groupPattern = detectGroupPattern(processes);
|
|
662
|
+
for (const process of processes) {
|
|
663
|
+
const rescored = scoreProcessName(process, groupPattern);
|
|
664
|
+
process.name_score = rescored.score;
|
|
665
|
+
process.name_score_reasons = rescored.reasons;
|
|
666
|
+
}
|
|
667
|
+
const sortedProcesses = [...processes].sort((left, right) => right.name_score - left.name_score ||
|
|
668
|
+
createdAtKey(left).localeCompare(createdAtKey(right)) ||
|
|
669
|
+
left.process_id.localeCompare(right.process_id));
|
|
670
|
+
duplicateGroups.push({
|
|
671
|
+
group_id: group.group_id,
|
|
672
|
+
group_pattern: groupPattern,
|
|
673
|
+
exact_duplicate: exactDuplicate,
|
|
674
|
+
processes,
|
|
675
|
+
});
|
|
676
|
+
if (!exactDuplicate) {
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
const keep = sortedProcesses[0];
|
|
680
|
+
const deleteCandidates = sortedProcesses.slice(1);
|
|
681
|
+
const scoreGap = keep.name_score - deleteCandidates[0].name_score;
|
|
682
|
+
const confidence = scoreGap >= 15 ? 'high' : 'medium';
|
|
683
|
+
const keepProcessRefs = referenceHits.processes[keep.process_id];
|
|
684
|
+
const keepLifecyclemodelRefs = referenceHits.lifecyclemodels[keep.process_id];
|
|
685
|
+
const deleteRefs = Object.fromEntries(deleteCandidates.map((candidate) => [
|
|
686
|
+
candidate.process_id,
|
|
687
|
+
{
|
|
688
|
+
process_refs: referenceHits.processes[candidate.process_id].length,
|
|
689
|
+
lifecyclemodel_refs: referenceHits.lifecyclemodels[candidate.process_id].length,
|
|
690
|
+
},
|
|
691
|
+
]));
|
|
692
|
+
const notes = ['exact duplicate confirmed by normalized exchange signature'];
|
|
693
|
+
if (Object.values(deleteRefs).every((entry) => entry.process_refs === 0 && entry.lifecyclemodel_refs === 0)) {
|
|
694
|
+
notes.push('current-user reference scan found no downstream hits for delete candidates');
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
notes.push('current-user reference scan found downstream hits; delete requires reference cleanup');
|
|
698
|
+
}
|
|
699
|
+
notes.push('global or shared-team reference verification is outside this command');
|
|
700
|
+
deletePlanGroups.push({
|
|
701
|
+
group_id: group.group_id,
|
|
702
|
+
status: 'priority_delete_candidates',
|
|
703
|
+
confidence,
|
|
704
|
+
current_user_reference_hits: {
|
|
705
|
+
keep_process_refs: keepProcessRefs.length,
|
|
706
|
+
keep_lifecyclemodel_refs: keepLifecyclemodelRefs.length,
|
|
707
|
+
delete_refs: deleteRefs,
|
|
708
|
+
},
|
|
709
|
+
keep: {
|
|
710
|
+
process_id: keep.process_id,
|
|
711
|
+
name_en: trimText(keep.remote_name_en ?? keep.name_en),
|
|
712
|
+
name_zh: trimText(keep.remote_name_zh ?? keep.name_zh),
|
|
713
|
+
score: keep.name_score,
|
|
714
|
+
reasons: keep.name_score_reasons,
|
|
715
|
+
},
|
|
716
|
+
delete: deleteCandidates.map((candidate) => ({
|
|
717
|
+
process_id: candidate.process_id,
|
|
718
|
+
name_en: trimText(candidate.remote_name_en ?? candidate.name_en),
|
|
719
|
+
name_zh: trimText(candidate.remote_name_zh ?? candidate.name_zh),
|
|
720
|
+
score: candidate.name_score,
|
|
721
|
+
reasons: candidate.name_score_reasons,
|
|
722
|
+
})),
|
|
723
|
+
notes,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
duplicateGroups,
|
|
728
|
+
deletePlanGroups,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
export async function runProcessDedupReview(options) {
|
|
732
|
+
const inputPath = requiredNonEmpty(options.inputPath, '--input', 'PROCESS_DEDUP_INPUT_REQUIRED');
|
|
733
|
+
const outDir = requiredNonEmpty(options.outDir, '--out-dir', 'PROCESS_DEDUP_OUT_DIR_REQUIRED');
|
|
734
|
+
const resolvedInputPath = path.resolve(inputPath);
|
|
735
|
+
const resolvedOutDir = path.resolve(outDir);
|
|
736
|
+
const timeoutMs = toPositiveInteger(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, '--timeout-ms', 'PROCESS_DEDUP_TIMEOUT_INVALID');
|
|
737
|
+
const maxRetries = toPositiveInteger(options.maxRetries ?? DEFAULT_MAX_RETRIES, '--max-retries', 'PROCESS_DEDUP_MAX_RETRIES_INVALID');
|
|
738
|
+
const generatedAtUtc = nowIso(options.now);
|
|
739
|
+
const document = normalizeInputDocument(readJsonInput(resolvedInputPath), resolvedInputPath);
|
|
740
|
+
const processIds = [
|
|
741
|
+
...new Set(document.groups.flatMap((group) => group.processes.map((process) => process.process_id))),
|
|
742
|
+
];
|
|
743
|
+
const inputManifestPath = path.join(resolvedOutDir, 'inputs', 'dedup-input.manifest.json');
|
|
744
|
+
writeJsonArtifact(inputManifestPath, {
|
|
745
|
+
schema_version: 1,
|
|
746
|
+
generated_at_utc: generatedAtUtc,
|
|
747
|
+
input_file: resolvedInputPath,
|
|
748
|
+
source_label: document.sourceLabel,
|
|
749
|
+
group_count: document.groups.length,
|
|
750
|
+
process_count: processIds.length,
|
|
751
|
+
});
|
|
752
|
+
const remoteStatus = {
|
|
753
|
+
enabled: false,
|
|
754
|
+
loaded: 0,
|
|
755
|
+
error: null,
|
|
756
|
+
reference_scan: options.skipRemote ? 'skipped_by_flag' : 'not_run',
|
|
757
|
+
};
|
|
758
|
+
let remoteMetadataPath = null;
|
|
759
|
+
let referenceScanPath = null;
|
|
760
|
+
let remoteById = {};
|
|
761
|
+
let referenceHits = buildEmptyReferenceHits(processIds);
|
|
762
|
+
if (!options.skipRemote) {
|
|
763
|
+
try {
|
|
764
|
+
const env = options.env ?? process.env;
|
|
765
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
766
|
+
const auth = await resolveRemoteAuthContext({
|
|
767
|
+
env,
|
|
768
|
+
fetchImpl,
|
|
769
|
+
timeoutMs,
|
|
770
|
+
maxRetries,
|
|
771
|
+
now: options.now ?? new Date(),
|
|
772
|
+
});
|
|
773
|
+
remoteById = await fetchRemoteMetadata({
|
|
774
|
+
processIds,
|
|
775
|
+
auth,
|
|
776
|
+
fetchImpl,
|
|
777
|
+
timeoutMs,
|
|
778
|
+
maxRetries,
|
|
779
|
+
});
|
|
780
|
+
remoteStatus.enabled = true;
|
|
781
|
+
remoteStatus.loaded = Object.keys(remoteById).length;
|
|
782
|
+
remoteMetadataPath = path.join(resolvedOutDir, 'inputs', 'processes.remote-metadata.json');
|
|
783
|
+
writeJsonArtifact(remoteMetadataPath, remoteById);
|
|
784
|
+
if (auth.userId) {
|
|
785
|
+
try {
|
|
786
|
+
referenceHits = await fetchCurrentUserReferenceHits({
|
|
787
|
+
auth,
|
|
788
|
+
userId: auth.userId,
|
|
789
|
+
targetProcessIds: processIds,
|
|
790
|
+
fetchImpl,
|
|
791
|
+
timeoutMs,
|
|
792
|
+
maxRetries,
|
|
793
|
+
});
|
|
794
|
+
referenceScanPath = path.join(resolvedOutDir, 'outputs', 'current-user-reference-scan.json');
|
|
795
|
+
writeJsonArtifact(referenceScanPath, referenceHits);
|
|
796
|
+
remoteStatus.reference_scan = 'current_user_completed';
|
|
797
|
+
}
|
|
798
|
+
catch (error) {
|
|
799
|
+
remoteStatus.reference_scan = 'failed';
|
|
800
|
+
remoteStatus.error = errorMessage(error);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
remoteStatus.reference_scan = 'skipped_missing_user_id';
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
remoteStatus.error = errorMessage(error);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const analysis = analyzeGroups(document.groups, remoteById, referenceHits);
|
|
812
|
+
const duplicateGroupsPath = path.join(resolvedOutDir, 'outputs', 'duplicate-groups.json');
|
|
813
|
+
const deletePlanPath = path.join(resolvedOutDir, 'outputs', 'delete-plan.json');
|
|
814
|
+
writeJsonArtifact(duplicateGroupsPath, {
|
|
815
|
+
schema_version: 1,
|
|
816
|
+
generated_at_utc: generatedAtUtc,
|
|
817
|
+
input_file: resolvedInputPath,
|
|
818
|
+
source_label: document.sourceLabel,
|
|
819
|
+
remote_status: remoteStatus,
|
|
820
|
+
groups: analysis.duplicateGroups,
|
|
821
|
+
});
|
|
822
|
+
writeJsonArtifact(deletePlanPath, {
|
|
823
|
+
schema_version: 1,
|
|
824
|
+
generated_at_utc: generatedAtUtc,
|
|
825
|
+
input_file: resolvedInputPath,
|
|
826
|
+
source_label: document.sourceLabel,
|
|
827
|
+
remote_status: remoteStatus,
|
|
828
|
+
groups: analysis.deletePlanGroups,
|
|
829
|
+
});
|
|
830
|
+
return {
|
|
831
|
+
schema_version: 1,
|
|
832
|
+
generated_at_utc: generatedAtUtc,
|
|
833
|
+
status: 'completed_process_dedup_review',
|
|
834
|
+
input_file: resolvedInputPath,
|
|
835
|
+
out_dir: resolvedOutDir,
|
|
836
|
+
source_label: document.sourceLabel,
|
|
837
|
+
group_count: analysis.duplicateGroups.length,
|
|
838
|
+
exact_duplicate_group_count: analysis.duplicateGroups.filter((group) => group.exact_duplicate)
|
|
839
|
+
.length,
|
|
840
|
+
remote_status: remoteStatus,
|
|
841
|
+
files: {
|
|
842
|
+
input_manifest: inputManifestPath,
|
|
843
|
+
remote_metadata: remoteMetadataPath,
|
|
844
|
+
duplicate_groups: duplicateGroupsPath,
|
|
845
|
+
delete_plan: deletePlanPath,
|
|
846
|
+
current_user_reference_scan: referenceScanPath,
|
|
847
|
+
},
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
export const __testInternals = {
|
|
851
|
+
analyzeGroups,
|
|
852
|
+
collectReferenceHits,
|
|
853
|
+
detectGroupPattern,
|
|
854
|
+
errorMessage,
|
|
855
|
+
fetchCurrentUserId,
|
|
856
|
+
fetchCurrentUserReferenceHits,
|
|
857
|
+
fetchCurrentUserRows,
|
|
858
|
+
fetchJsonWithRetry,
|
|
859
|
+
fetchRemoteMetadata,
|
|
860
|
+
getLangList,
|
|
861
|
+
getLangText,
|
|
862
|
+
normalizeAmount,
|
|
863
|
+
normalizeExchangeRows,
|
|
864
|
+
normalizeInputDocument,
|
|
865
|
+
normalizedSignature,
|
|
866
|
+
parseJsonResponse,
|
|
867
|
+
resolveRemoteAuthContext,
|
|
868
|
+
scoreProcessName,
|
|
869
|
+
};
|
|
870
|
+
//# sourceMappingURL=process-dedup-review.js.map
|