@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,1028 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { writeJsonArtifact, writeJsonLinesArtifact, writeTextArtifact } from './artifacts.js';
|
|
4
|
+
import { CliError } from './errors.js';
|
|
5
|
+
import { syncStateAwareProcessRecord, } from './process-save-draft.js';
|
|
6
|
+
import { validateProcessPayload, } from './process-payload-validation.js';
|
|
7
|
+
import { deriveSupabaseProjectBaseUrl, requireSupabaseRestRuntime } from './supabase-client.js';
|
|
8
|
+
import { resolveSupabaseUserSession } from './supabase-session.js';
|
|
9
|
+
import { redactEmail, requireUserApiKeyCredentials } from './user-api-key.js';
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
11
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
12
|
+
const DEFAULT_PAGE_SIZE = 500;
|
|
13
|
+
const DEFAULT_CONCURRENCY = 1;
|
|
14
|
+
const MAX_PAGE_SIZE = 1_000;
|
|
15
|
+
const MAX_CONCURRENCY = 8;
|
|
16
|
+
const TABLE_BY_TYPE = new Map([
|
|
17
|
+
['contact data set', 'contacts'],
|
|
18
|
+
['source data set', 'sources'],
|
|
19
|
+
['unit group data set', 'unitgroups'],
|
|
20
|
+
['flow property data set', 'flowproperties'],
|
|
21
|
+
['flow data set', 'flows'],
|
|
22
|
+
['process data set', 'processes'],
|
|
23
|
+
['lifeCycleModel data set', 'lifecyclemodels'],
|
|
24
|
+
['LCIA method data set', 'lciamethods'],
|
|
25
|
+
]);
|
|
26
|
+
function isRecord(value) {
|
|
27
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
function trimText(value) {
|
|
30
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
31
|
+
}
|
|
32
|
+
function requiredNonEmpty(value, label, code) {
|
|
33
|
+
const normalized = value.trim();
|
|
34
|
+
if (!normalized) {
|
|
35
|
+
throw new CliError(`Missing required ${label}.`, {
|
|
36
|
+
code,
|
|
37
|
+
exitCode: 2,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
42
|
+
function toPositiveInteger(value, label, code, max) {
|
|
43
|
+
if (!Number.isInteger(value) || value <= 0 || (max !== undefined && value > max)) {
|
|
44
|
+
throw new CliError(max === undefined
|
|
45
|
+
? `Expected ${label} to be a positive integer.`
|
|
46
|
+
: `Expected ${label} to be a positive integer not greater than ${max}.`, {
|
|
47
|
+
code,
|
|
48
|
+
exitCode: 2,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
function toOptionalPositiveInteger(value, label, code) {
|
|
54
|
+
if (value === null || value === undefined) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return toPositiveInteger(value, label, code);
|
|
58
|
+
}
|
|
59
|
+
function nowIso(now = new Date()) {
|
|
60
|
+
return now.toISOString();
|
|
61
|
+
}
|
|
62
|
+
function recordKey(row) {
|
|
63
|
+
return `${row.id}:${row.version}`;
|
|
64
|
+
}
|
|
65
|
+
function parseContentRangeTotal(value) {
|
|
66
|
+
if (!value) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const match = value.match(/\/(\d+|\*)$/u);
|
|
70
|
+
if (!match || match[1] === '*') {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return Number.parseInt(match[1], 10);
|
|
74
|
+
}
|
|
75
|
+
function normalizeVersion(version) {
|
|
76
|
+
return version.trim();
|
|
77
|
+
}
|
|
78
|
+
function compareVersions(left, right) {
|
|
79
|
+
const leftParts = normalizeVersion(left)
|
|
80
|
+
.split('.')
|
|
81
|
+
.map((part) => Number(part));
|
|
82
|
+
const rightParts = normalizeVersion(right)
|
|
83
|
+
.split('.')
|
|
84
|
+
.map((part) => Number(part));
|
|
85
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
86
|
+
for (let index = 0; index < length; index += 1) {
|
|
87
|
+
const leftValue = Number.isFinite(leftParts[index]) ? leftParts[index] : 0;
|
|
88
|
+
const rightValue = Number.isFinite(rightParts[index]) ? rightParts[index] : 0;
|
|
89
|
+
if (leftValue !== rightValue) {
|
|
90
|
+
return leftValue - rightValue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return normalizeVersion(left).localeCompare(normalizeVersion(right));
|
|
94
|
+
}
|
|
95
|
+
function jsonToList(value) {
|
|
96
|
+
if (value === null || value === undefined || value === '') {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
return Array.isArray(value) ? value : [value];
|
|
100
|
+
}
|
|
101
|
+
function getLangList(value) {
|
|
102
|
+
if (value === null || value === undefined || value === '') {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
if (Array.isArray(value)) {
|
|
106
|
+
return value.filter((entry) => isRecord(entry));
|
|
107
|
+
}
|
|
108
|
+
if (isRecord(value)) {
|
|
109
|
+
const langString = value['common:langString'];
|
|
110
|
+
if (Array.isArray(langString)) {
|
|
111
|
+
return langString.filter((entry) => isRecord(entry));
|
|
112
|
+
}
|
|
113
|
+
if (isRecord(langString)) {
|
|
114
|
+
return [langString];
|
|
115
|
+
}
|
|
116
|
+
if (value['#text'] !== undefined || value['@xml:lang'] !== undefined) {
|
|
117
|
+
return [value];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (typeof value === 'string') {
|
|
121
|
+
return [{ '@xml:lang': 'en', '#text': value }];
|
|
122
|
+
}
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
function getLangText(value, lang) {
|
|
126
|
+
const entries = getLangList(value);
|
|
127
|
+
if (lang) {
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
if (trimText(entry['@xml:lang']).toLowerCase() === lang.toLowerCase() &&
|
|
130
|
+
trimText(entry['#text'])) {
|
|
131
|
+
return trimText(entry['#text']);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const entry of entries) {
|
|
136
|
+
if (trimText(entry['#text'])) {
|
|
137
|
+
return trimText(entry['#text']);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return '';
|
|
141
|
+
}
|
|
142
|
+
function genFlowName(name, lang) {
|
|
143
|
+
if (!name) {
|
|
144
|
+
return '';
|
|
145
|
+
}
|
|
146
|
+
const parts = [
|
|
147
|
+
getLangText(name.baseName, lang),
|
|
148
|
+
getLangText(name.treatmentStandardsRoutes, lang),
|
|
149
|
+
getLangText(name.mixAndLocationTypes, lang),
|
|
150
|
+
getLangText(name.flowProperties, lang),
|
|
151
|
+
].filter(Boolean);
|
|
152
|
+
return parts.join('; ');
|
|
153
|
+
}
|
|
154
|
+
function genFlowNameJson(name) {
|
|
155
|
+
if (!name) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
const results = [];
|
|
159
|
+
for (const item of jsonToList(name.baseName)) {
|
|
160
|
+
const entry = isRecord(item) ? item : {};
|
|
161
|
+
const lang = trimText(entry['@xml:lang']);
|
|
162
|
+
const text = lang ? genFlowName(name, lang) : '';
|
|
163
|
+
if (lang && text) {
|
|
164
|
+
results.push({ '@xml:lang': lang, '#text': text });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return results;
|
|
168
|
+
}
|
|
169
|
+
function genProcessName(name, lang) {
|
|
170
|
+
if (!name) {
|
|
171
|
+
return '';
|
|
172
|
+
}
|
|
173
|
+
const parts = [
|
|
174
|
+
getLangText(name.baseName, lang),
|
|
175
|
+
getLangText(name.treatmentStandardsRoutes, lang),
|
|
176
|
+
getLangText(name.mixAndLocationTypes, lang),
|
|
177
|
+
getLangText(name.functionalUnitFlowProperties, lang),
|
|
178
|
+
].filter(Boolean);
|
|
179
|
+
return parts.join('; ');
|
|
180
|
+
}
|
|
181
|
+
function genProcessNameJson(name) {
|
|
182
|
+
if (!name) {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
const results = [];
|
|
186
|
+
for (const item of jsonToList(name.baseName)) {
|
|
187
|
+
const entry = isRecord(item) ? item : {};
|
|
188
|
+
const lang = trimText(entry['@xml:lang']);
|
|
189
|
+
const text = lang ? genProcessName(name, lang) : '';
|
|
190
|
+
if (lang && text) {
|
|
191
|
+
results.push({ '@xml:lang': lang, '#text': text });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return results;
|
|
195
|
+
}
|
|
196
|
+
function normalizeDatasetPayload(payload, label) {
|
|
197
|
+
if (typeof payload === 'string') {
|
|
198
|
+
try {
|
|
199
|
+
const parsed = JSON.parse(payload);
|
|
200
|
+
if (!isRecord(parsed)) {
|
|
201
|
+
throw new CliError(`Remote dataset payload was not a JSON object for ${label}.`, {
|
|
202
|
+
code: 'PROCESS_REFRESH_REMOTE_PAYLOAD_INVALID',
|
|
203
|
+
exitCode: 1,
|
|
204
|
+
details: parsed,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return parsed;
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (error instanceof CliError) {
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
throw new CliError(`Remote dataset payload was not valid JSON for ${label}.`, {
|
|
214
|
+
code: 'PROCESS_REFRESH_REMOTE_PAYLOAD_INVALID_JSON',
|
|
215
|
+
exitCode: 1,
|
|
216
|
+
details: String(error),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (!isRecord(payload)) {
|
|
221
|
+
throw new CliError(`Remote dataset payload was missing json for ${label}.`, {
|
|
222
|
+
code: 'PROCESS_REFRESH_REMOTE_PAYLOAD_MISSING',
|
|
223
|
+
exitCode: 1,
|
|
224
|
+
details: payload,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return payload;
|
|
228
|
+
}
|
|
229
|
+
function getShortDescription(payload, type) {
|
|
230
|
+
if (type === 'flow data set') {
|
|
231
|
+
const flowDataSet = isRecord(payload.flowDataSet) ? payload.flowDataSet : {};
|
|
232
|
+
const info = isRecord(flowDataSet.flowInformation) ? flowDataSet.flowInformation : {};
|
|
233
|
+
const dataSetInformation = isRecord(info.dataSetInformation) ? info.dataSetInformation : {};
|
|
234
|
+
const name = isRecord(dataSetInformation.name) ? dataSetInformation.name : null;
|
|
235
|
+
return genFlowNameJson(name);
|
|
236
|
+
}
|
|
237
|
+
if (type === 'process data set') {
|
|
238
|
+
const processDataSet = isRecord(payload.processDataSet) ? payload.processDataSet : {};
|
|
239
|
+
const info = isRecord(processDataSet.processInformation)
|
|
240
|
+
? processDataSet.processInformation
|
|
241
|
+
: {};
|
|
242
|
+
const dataSetInformation = isRecord(info.dataSetInformation) ? info.dataSetInformation : {};
|
|
243
|
+
const name = isRecord(dataSetInformation.name) ? dataSetInformation.name : null;
|
|
244
|
+
return genProcessNameJson(name);
|
|
245
|
+
}
|
|
246
|
+
if (type === 'contact data set') {
|
|
247
|
+
const contactDataSet = isRecord(payload.contactDataSet) ? payload.contactDataSet : {};
|
|
248
|
+
const info = isRecord(contactDataSet.contactInformation)
|
|
249
|
+
? contactDataSet.contactInformation
|
|
250
|
+
: {};
|
|
251
|
+
const dataSetInformation = isRecord(info.dataSetInformation) ? info.dataSetInformation : {};
|
|
252
|
+
return getLangList(dataSetInformation['common:shortName']);
|
|
253
|
+
}
|
|
254
|
+
if (type === 'source data set') {
|
|
255
|
+
const sourceDataSet = isRecord(payload.sourceDataSet) ? payload.sourceDataSet : {};
|
|
256
|
+
const info = isRecord(sourceDataSet.sourceInformation) ? sourceDataSet.sourceInformation : {};
|
|
257
|
+
const dataSetInformation = isRecord(info.dataSetInformation) ? info.dataSetInformation : {};
|
|
258
|
+
return getLangList(dataSetInformation['common:shortName']);
|
|
259
|
+
}
|
|
260
|
+
if (type === 'flow property data set') {
|
|
261
|
+
const flowPropertyDataSet = isRecord(payload.flowPropertyDataSet)
|
|
262
|
+
? payload.flowPropertyDataSet
|
|
263
|
+
: {};
|
|
264
|
+
const info = isRecord(flowPropertyDataSet.flowPropertyInformation)
|
|
265
|
+
? flowPropertyDataSet.flowPropertyInformation
|
|
266
|
+
: {};
|
|
267
|
+
const dataSetInformation = isRecord(info.dataSetInformation) ? info.dataSetInformation : {};
|
|
268
|
+
return getLangList(dataSetInformation['common:shortName']);
|
|
269
|
+
}
|
|
270
|
+
if (type === 'unit group data set') {
|
|
271
|
+
const unitGroupDataSet = isRecord(payload.unitGroupDataSet) ? payload.unitGroupDataSet : {};
|
|
272
|
+
const info = isRecord(unitGroupDataSet.unitGroupInformation)
|
|
273
|
+
? unitGroupDataSet.unitGroupInformation
|
|
274
|
+
: {};
|
|
275
|
+
const dataSetInformation = isRecord(info.dataSetInformation) ? info.dataSetInformation : {};
|
|
276
|
+
return getLangList(dataSetInformation['common:shortName']);
|
|
277
|
+
}
|
|
278
|
+
if (type === 'LCIA method data set') {
|
|
279
|
+
const lciaMethodDataSet = isRecord(payload.lciaMethodDataSet) ? payload.lciaMethodDataSet : {};
|
|
280
|
+
const info = isRecord(lciaMethodDataSet.LCIAMethodInformation)
|
|
281
|
+
? lciaMethodDataSet.LCIAMethodInformation
|
|
282
|
+
: {};
|
|
283
|
+
const dataSetInformation = isRecord(info.dataSetInformation) ? info.dataSetInformation : {};
|
|
284
|
+
return getLangList(dataSetInformation['common:shortName']);
|
|
285
|
+
}
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
async function parseJsonResponse(response, label) {
|
|
289
|
+
const text = await response.text();
|
|
290
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
291
|
+
if (!text.trim()) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
if (!contentType.includes('application/json')) {
|
|
295
|
+
return text;
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
return JSON.parse(text);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
throw new CliError(`${label} returned invalid JSON.`, {
|
|
302
|
+
code: 'PROCESS_REFRESH_REMOTE_INVALID_JSON',
|
|
303
|
+
exitCode: 1,
|
|
304
|
+
details: String(error),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async function fetchJsonWithRetry(options) {
|
|
309
|
+
let lastError = null;
|
|
310
|
+
for (let attempt = 1; attempt <= options.maxRetries; attempt += 1) {
|
|
311
|
+
try {
|
|
312
|
+
const response = await options.fetchImpl(options.url, {
|
|
313
|
+
...options.init,
|
|
314
|
+
signal: AbortSignal.timeout(options.timeoutMs),
|
|
315
|
+
});
|
|
316
|
+
const body = await parseJsonResponse(response, options.label);
|
|
317
|
+
if (!response.ok) {
|
|
318
|
+
throw new CliError(`${options.label} failed with ${response.status}.`, {
|
|
319
|
+
code: 'PROCESS_REFRESH_REMOTE_REQUEST_FAILED',
|
|
320
|
+
exitCode: 1,
|
|
321
|
+
details: {
|
|
322
|
+
status: response.status,
|
|
323
|
+
body,
|
|
324
|
+
url: options.url,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
status: response.status,
|
|
330
|
+
headers: new Headers({
|
|
331
|
+
'content-range': response.headers.get('content-range') ?? '',
|
|
332
|
+
'content-type': response.headers.get('content-type') ?? '',
|
|
333
|
+
}),
|
|
334
|
+
body,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
lastError = error;
|
|
339
|
+
if (attempt >= options.maxRetries) {
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
await new Promise((resolve) => setTimeout(resolve, attempt * 1_500));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (lastError instanceof CliError) {
|
|
346
|
+
throw lastError;
|
|
347
|
+
}
|
|
348
|
+
throw new CliError(`${options.label} failed after ${options.maxRetries} attempt(s).`, {
|
|
349
|
+
code: 'PROCESS_REFRESH_REMOTE_REQUEST_FAILED',
|
|
350
|
+
exitCode: 1,
|
|
351
|
+
details: String(lastError),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
async function resolveRemoteAuth(options) {
|
|
355
|
+
const runtime = requireSupabaseRestRuntime(options.env);
|
|
356
|
+
const session = await resolveSupabaseUserSession({
|
|
357
|
+
runtime,
|
|
358
|
+
fetchImpl: options.fetchImpl,
|
|
359
|
+
timeoutMs: options.timeoutMs,
|
|
360
|
+
now: options.now,
|
|
361
|
+
});
|
|
362
|
+
const userResponse = await fetchJsonWithRetry({
|
|
363
|
+
url: `${deriveSupabaseProjectBaseUrl(runtime.apiBaseUrl)}/auth/v1/user`,
|
|
364
|
+
init: {
|
|
365
|
+
method: 'GET',
|
|
366
|
+
headers: {
|
|
367
|
+
apikey: runtime.publishableKey,
|
|
368
|
+
Authorization: `Bearer ${session.accessToken}`,
|
|
369
|
+
Accept: 'application/json',
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
label: 'supabase current-user lookup',
|
|
373
|
+
fetchImpl: options.fetchImpl,
|
|
374
|
+
timeoutMs: options.timeoutMs,
|
|
375
|
+
maxRetries: options.maxRetries,
|
|
376
|
+
});
|
|
377
|
+
const userId = isRecord(userResponse.body) ? trimText(userResponse.body.id) : '';
|
|
378
|
+
if (!userId) {
|
|
379
|
+
throw new CliError('Supabase current-user lookup succeeded without a user id.', {
|
|
380
|
+
code: 'PROCESS_REFRESH_CURRENT_USER_ID_MISSING',
|
|
381
|
+
exitCode: 1,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
projectBaseUrl: deriveSupabaseProjectBaseUrl(runtime.apiBaseUrl),
|
|
386
|
+
publishableKey: runtime.publishableKey,
|
|
387
|
+
accessToken: session.accessToken,
|
|
388
|
+
userId,
|
|
389
|
+
maskedUserEmail: redactEmail(requireUserApiKeyCredentials(runtime.userApiKey).email),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function normalizeManifestRow(value) {
|
|
393
|
+
if (!isRecord(value)) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
const id = trimText(value.id);
|
|
397
|
+
const version = trimText(value.version);
|
|
398
|
+
if (!id || !version) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
id,
|
|
403
|
+
version,
|
|
404
|
+
modified_at: trimText(value.modified_at) || null,
|
|
405
|
+
state_code: typeof value.state_code === 'number' ? value.state_code : null,
|
|
406
|
+
model_id: trimText(value.model_id) || null,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function readManifest(filePath) {
|
|
410
|
+
if (!existsSync(filePath)) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
415
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.rows)) {
|
|
416
|
+
throw new Error('manifest rows missing');
|
|
417
|
+
}
|
|
418
|
+
const rows = parsed.rows
|
|
419
|
+
.map((row) => normalizeManifestRow(row))
|
|
420
|
+
.filter((row) => row !== null);
|
|
421
|
+
const userId = trimText(parsed.user_id);
|
|
422
|
+
const maskedUserEmail = trimText(parsed.masked_user_email);
|
|
423
|
+
if (!userId || !maskedUserEmail) {
|
|
424
|
+
throw new Error('manifest user metadata missing');
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
schema_version: 1,
|
|
428
|
+
generated_at_utc: trimText(parsed.generated_at_utc) || nowIso(),
|
|
429
|
+
user_id: userId,
|
|
430
|
+
masked_user_email: maskedUserEmail,
|
|
431
|
+
source: 'current_user_processes',
|
|
432
|
+
order: 'modified_at.desc,id.asc',
|
|
433
|
+
page_size: typeof parsed.page_size === 'number' && Number.isInteger(parsed.page_size)
|
|
434
|
+
? parsed.page_size
|
|
435
|
+
: DEFAULT_PAGE_SIZE,
|
|
436
|
+
count: typeof parsed.count === 'number' && Number.isInteger(parsed.count)
|
|
437
|
+
? parsed.count
|
|
438
|
+
: rows.length,
|
|
439
|
+
rows,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
throw new CliError(`Manifest file is not valid JSON: ${filePath}`, {
|
|
444
|
+
code: 'PROCESS_REFRESH_MANIFEST_INVALID',
|
|
445
|
+
exitCode: 2,
|
|
446
|
+
details: String(error),
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
async function snapshotProcesses(options) {
|
|
451
|
+
const rows = [];
|
|
452
|
+
let total = null;
|
|
453
|
+
for (let offset = 0; total === null || offset < total; offset += options.pageSize) {
|
|
454
|
+
const url = new URL(`${options.projectBaseUrl}/rest/v1/processes`);
|
|
455
|
+
url.searchParams.set('select', 'id,version,modified_at,state_code,model_id');
|
|
456
|
+
url.searchParams.set('user_id', `eq.${options.userId}`);
|
|
457
|
+
url.searchParams.set('order', 'modified_at.desc,id.asc');
|
|
458
|
+
url.searchParams.set('limit', String(options.pageSize));
|
|
459
|
+
url.searchParams.set('offset', String(offset));
|
|
460
|
+
const page = await fetchJsonWithRetry({
|
|
461
|
+
url: url.toString(),
|
|
462
|
+
init: {
|
|
463
|
+
method: 'GET',
|
|
464
|
+
headers: {
|
|
465
|
+
apikey: options.publishableKey,
|
|
466
|
+
Authorization: `Bearer ${options.accessToken}`,
|
|
467
|
+
Accept: 'application/json',
|
|
468
|
+
Prefer: 'count=exact',
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
label: `process refresh snapshot page fetch (offset=${offset})`,
|
|
472
|
+
fetchImpl: options.fetchImpl,
|
|
473
|
+
timeoutMs: options.timeoutMs,
|
|
474
|
+
maxRetries: options.maxRetries,
|
|
475
|
+
});
|
|
476
|
+
const pageRows = Array.isArray(page.body) ? page.body : [];
|
|
477
|
+
const normalizedRows = pageRows
|
|
478
|
+
.map((row) => normalizeManifestRow(row))
|
|
479
|
+
.filter((row) => row !== null);
|
|
480
|
+
rows.push(...normalizedRows);
|
|
481
|
+
total = parseContentRangeTotal(page.headers.get('content-range')) ?? rows.length;
|
|
482
|
+
if (normalizedRows.length === 0) {
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const deduped = [];
|
|
487
|
+
const seen = new Set();
|
|
488
|
+
for (const row of rows) {
|
|
489
|
+
const key = recordKey(row);
|
|
490
|
+
if (seen.has(key)) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
seen.add(key);
|
|
494
|
+
deduped.push(row);
|
|
495
|
+
}
|
|
496
|
+
return deduped;
|
|
497
|
+
}
|
|
498
|
+
function readCompleted(progressFile, applyMode) {
|
|
499
|
+
const completed = new Set();
|
|
500
|
+
if (!existsSync(progressFile)) {
|
|
501
|
+
return completed;
|
|
502
|
+
}
|
|
503
|
+
const doneStatuses = applyMode
|
|
504
|
+
? new Set(['saved', 'skipped', 'validation_blocked'])
|
|
505
|
+
: new Set(['dry_run', 'skipped', 'validation_blocked']);
|
|
506
|
+
const lines = readFileSync(progressFile, 'utf8').split(/\r?\n/u).filter(Boolean);
|
|
507
|
+
for (const line of lines) {
|
|
508
|
+
try {
|
|
509
|
+
const record = JSON.parse(line);
|
|
510
|
+
if (!isRecord(record)) {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
const key = trimText(record.key);
|
|
514
|
+
const status = trimText(record.status);
|
|
515
|
+
if (key && doneStatuses.has(status)) {
|
|
516
|
+
completed.add(key);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
// Ignore corrupt progress rows and continue resuming from the valid ones.
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return completed;
|
|
524
|
+
}
|
|
525
|
+
async function fetchProcessDetail(options) {
|
|
526
|
+
const url = new URL(`${options.projectBaseUrl}/rest/v1/processes`);
|
|
527
|
+
url.searchParams.set('select', 'id,version,json,modified_at,state_code,model_id,user_id');
|
|
528
|
+
url.searchParams.set('id', `eq.${options.row.id}`);
|
|
529
|
+
url.searchParams.set('version', `eq.${options.row.version}`);
|
|
530
|
+
url.searchParams.set('limit', '1');
|
|
531
|
+
const response = await fetchJsonWithRetry({
|
|
532
|
+
url: url.toString(),
|
|
533
|
+
init: {
|
|
534
|
+
method: 'GET',
|
|
535
|
+
headers: {
|
|
536
|
+
apikey: options.publishableKey,
|
|
537
|
+
Authorization: `Bearer ${options.accessToken}`,
|
|
538
|
+
Accept: 'application/json',
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
label: `process refresh detail fetch (${recordKey(options.row)})`,
|
|
542
|
+
fetchImpl: options.fetchImpl,
|
|
543
|
+
timeoutMs: options.timeoutMs,
|
|
544
|
+
maxRetries: options.maxRetries,
|
|
545
|
+
});
|
|
546
|
+
const rows = Array.isArray(response.body) ? response.body : [];
|
|
547
|
+
const firstRow = rows[0];
|
|
548
|
+
if (!isRecord(firstRow)) {
|
|
549
|
+
throw new CliError(`Process not found for ${recordKey(options.row)}.`, {
|
|
550
|
+
code: 'PROCESS_REFRESH_DETAIL_NOT_FOUND',
|
|
551
|
+
exitCode: 1,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
id: trimText(firstRow.id) || options.row.id,
|
|
556
|
+
version: trimText(firstRow.version) || options.row.version,
|
|
557
|
+
modified_at: trimText(firstRow.modified_at) || null,
|
|
558
|
+
state_code: typeof firstRow.state_code === 'number' ? firstRow.state_code : null,
|
|
559
|
+
model_id: trimText(firstRow.model_id) || null,
|
|
560
|
+
user_id: trimText(firstRow.user_id) || null,
|
|
561
|
+
json: normalizeDatasetPayload(firstRow.json, recordKey(options.row)),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function collectRefs(root) {
|
|
565
|
+
const refs = [];
|
|
566
|
+
const visited = new WeakSet();
|
|
567
|
+
const walk = (current, pathParts) => {
|
|
568
|
+
if (!isRecord(current) && !Array.isArray(current)) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (visited.has(current)) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
visited.add(current);
|
|
575
|
+
if (isRecord(current) &&
|
|
576
|
+
typeof current['@refObjectId'] === 'string' &&
|
|
577
|
+
typeof current['@version'] === 'string' &&
|
|
578
|
+
typeof current['@type'] === 'string' &&
|
|
579
|
+
TABLE_BY_TYPE.has(current['@type'])) {
|
|
580
|
+
refs.push({
|
|
581
|
+
node: current,
|
|
582
|
+
path: pathParts.join('.'),
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
if (Array.isArray(current)) {
|
|
586
|
+
current.forEach((item, index) => walk(item, [...pathParts, String(index)]));
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
Object.entries(current).forEach(([key, value]) => walk(value, [...pathParts, key]));
|
|
590
|
+
};
|
|
591
|
+
walk(root, []);
|
|
592
|
+
return refs;
|
|
593
|
+
}
|
|
594
|
+
async function fetchLatestRefs(options) {
|
|
595
|
+
const missingByTable = new Map();
|
|
596
|
+
for (const ref of options.refs) {
|
|
597
|
+
const table = TABLE_BY_TYPE.get(ref.node['@type']);
|
|
598
|
+
const id = ref.node['@refObjectId'];
|
|
599
|
+
const cacheKey = table ? `${table}:${id}` : '';
|
|
600
|
+
if (!table || options.cache.has(cacheKey)) {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (!missingByTable.has(table)) {
|
|
604
|
+
missingByTable.set(table, new Set());
|
|
605
|
+
}
|
|
606
|
+
missingByTable.get(table)?.add(id);
|
|
607
|
+
}
|
|
608
|
+
for (const [table, idSet] of missingByTable.entries()) {
|
|
609
|
+
const ids = Array.from(idSet);
|
|
610
|
+
for (let offset = 0; offset < ids.length; offset += 50) {
|
|
611
|
+
const chunk = ids.slice(offset, offset + 50);
|
|
612
|
+
const url = new URL(`${options.projectBaseUrl}/rest/v1/${table}`);
|
|
613
|
+
url.searchParams.set('select', 'id,version,json,modified_at,state_code,user_id,team_id');
|
|
614
|
+
url.searchParams.set('id', `in.(${chunk.join(',')})`);
|
|
615
|
+
url.searchParams.set('order', 'version.desc');
|
|
616
|
+
try {
|
|
617
|
+
const response = await fetchJsonWithRetry({
|
|
618
|
+
url: url.toString(),
|
|
619
|
+
init: {
|
|
620
|
+
method: 'GET',
|
|
621
|
+
headers: {
|
|
622
|
+
apikey: options.publishableKey,
|
|
623
|
+
Authorization: `Bearer ${options.accessToken}`,
|
|
624
|
+
Accept: 'application/json',
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
label: `reference refresh ${table} lookup`,
|
|
628
|
+
fetchImpl: options.fetchImpl,
|
|
629
|
+
timeoutMs: options.timeoutMs,
|
|
630
|
+
maxRetries: options.maxRetries,
|
|
631
|
+
});
|
|
632
|
+
const rows = Array.isArray(response.body) ? response.body : [];
|
|
633
|
+
const byId = new Map();
|
|
634
|
+
for (const rawRow of rows) {
|
|
635
|
+
if (!isRecord(rawRow)) {
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
const id = trimText(rawRow.id);
|
|
639
|
+
const version = trimText(rawRow.version);
|
|
640
|
+
if (!id || !version) {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
const normalized = {
|
|
644
|
+
id,
|
|
645
|
+
version,
|
|
646
|
+
json: normalizeDatasetPayload(rawRow.json, `${table}:${id}@${version}`),
|
|
647
|
+
modified_at: trimText(rawRow.modified_at) || null,
|
|
648
|
+
state_code: typeof rawRow.state_code === 'number' ? rawRow.state_code : null,
|
|
649
|
+
user_id: trimText(rawRow.user_id) || null,
|
|
650
|
+
team_id: trimText(rawRow.team_id) || null,
|
|
651
|
+
};
|
|
652
|
+
const existing = byId.get(id) ?? [];
|
|
653
|
+
existing.push(normalized);
|
|
654
|
+
byId.set(id, existing);
|
|
655
|
+
}
|
|
656
|
+
for (const id of chunk) {
|
|
657
|
+
const versions = byId.get(id) ?? [];
|
|
658
|
+
versions.sort((left, right) => compareVersions(right.version, left.version));
|
|
659
|
+
options.cache.set(`${table}:${id}`, {
|
|
660
|
+
row: versions[0] ?? null,
|
|
661
|
+
count: versions.length,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
const message = String(error);
|
|
667
|
+
for (const id of chunk) {
|
|
668
|
+
options.cache.set(`${table}:${id}`, {
|
|
669
|
+
row: null,
|
|
670
|
+
count: 0,
|
|
671
|
+
error: message,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function updateProcessJson(payload, refs, cache) {
|
|
679
|
+
let versionUpdates = 0;
|
|
680
|
+
let descriptionUpdates = 0;
|
|
681
|
+
const touchedRefs = [];
|
|
682
|
+
const unresolvedRefs = [];
|
|
683
|
+
for (const ref of refs) {
|
|
684
|
+
const table = TABLE_BY_TYPE.get(ref.node['@type']);
|
|
685
|
+
const cacheKey = table ? `${table}:${ref.node['@refObjectId']}` : '';
|
|
686
|
+
const latest = cacheKey ? cache.get(cacheKey) : null;
|
|
687
|
+
if (!latest?.row) {
|
|
688
|
+
unresolvedRefs.push({
|
|
689
|
+
id: ref.node['@refObjectId'],
|
|
690
|
+
type: ref.node['@type'],
|
|
691
|
+
version: ref.node['@version'],
|
|
692
|
+
reason: latest?.error ?? 'no accessible version',
|
|
693
|
+
});
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
const currentVersion = ref.node['@version'];
|
|
697
|
+
const latestVersion = latest.row.version;
|
|
698
|
+
const description = getShortDescription(latest.row.json, ref.node['@type']);
|
|
699
|
+
const beforeDescription = JSON.stringify(ref.node['common:shortDescription'] ?? null);
|
|
700
|
+
const afterDescription = description.length ? JSON.stringify(description) : beforeDescription;
|
|
701
|
+
if (compareVersions(latestVersion, currentVersion) > 0) {
|
|
702
|
+
ref.node['@version'] = latestVersion;
|
|
703
|
+
versionUpdates += 1;
|
|
704
|
+
}
|
|
705
|
+
if (description.length && beforeDescription !== afterDescription) {
|
|
706
|
+
ref.node['common:shortDescription'] = description;
|
|
707
|
+
descriptionUpdates += 1;
|
|
708
|
+
}
|
|
709
|
+
if (ref.node['@version'] !== currentVersion || beforeDescription !== afterDescription) {
|
|
710
|
+
touchedRefs.push({
|
|
711
|
+
id: ref.node['@refObjectId'],
|
|
712
|
+
type: ref.node['@type'],
|
|
713
|
+
from_version: currentVersion,
|
|
714
|
+
to_version: ref.node['@version'],
|
|
715
|
+
path: ref.path,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
void payload;
|
|
720
|
+
return {
|
|
721
|
+
version_updates: versionUpdates,
|
|
722
|
+
description_updates: descriptionUpdates,
|
|
723
|
+
touched_refs: touchedRefs,
|
|
724
|
+
unresolved_refs: unresolvedRefs,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function appendReportHeader(reportFile, context) {
|
|
728
|
+
if (existsSync(reportFile)) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
writeTextArtifact(reportFile, [
|
|
732
|
+
'# TianGong Process Reference Refresh',
|
|
733
|
+
'',
|
|
734
|
+
`- mode: ${context.mode}`,
|
|
735
|
+
`- generated_at_utc: ${context.generatedAtUtc}`,
|
|
736
|
+
`- manifest_count: ${context.manifestCount}`,
|
|
737
|
+
'',
|
|
738
|
+
'| time | status | key | version updates | description updates | refs | note |',
|
|
739
|
+
'| --- | --- | --- | ---: | ---: | ---: | --- |',
|
|
740
|
+
'',
|
|
741
|
+
].join('\n'));
|
|
742
|
+
}
|
|
743
|
+
function appendReportRow(reportFile, record) {
|
|
744
|
+
const note = trimText(record.note ?? record.error)
|
|
745
|
+
.replace(/\|/gu, '/')
|
|
746
|
+
.slice(0, 180);
|
|
747
|
+
appendFileSync(reportFile, `| ${record.time} | ${record.status} | ${record.key} | ${record.version_updates ?? 0} | ${record.description_updates ?? 0} | ${record.ref_count ?? 0} | ${note} |\n`, 'utf8');
|
|
748
|
+
}
|
|
749
|
+
async function processOne(options) {
|
|
750
|
+
const time = new Date().toISOString();
|
|
751
|
+
const key = recordKey(options.row);
|
|
752
|
+
try {
|
|
753
|
+
if (typeof options.row.state_code === 'number' && options.row.state_code >= 20) {
|
|
754
|
+
const record = {
|
|
755
|
+
time,
|
|
756
|
+
key,
|
|
757
|
+
status: 'skipped',
|
|
758
|
+
note: `state_code=${options.row.state_code}`,
|
|
759
|
+
};
|
|
760
|
+
writeJsonLinesArtifact(options.files.progress_jsonl, record, { append: true });
|
|
761
|
+
appendReportRow(options.files.report_md, record);
|
|
762
|
+
return record;
|
|
763
|
+
}
|
|
764
|
+
const detail = await fetchProcessDetail({
|
|
765
|
+
projectBaseUrl: options.auth.projectBaseUrl,
|
|
766
|
+
publishableKey: options.auth.publishableKey,
|
|
767
|
+
accessToken: options.auth.accessToken,
|
|
768
|
+
row: options.row,
|
|
769
|
+
fetchImpl: options.fetchImpl,
|
|
770
|
+
timeoutMs: options.timeoutMs,
|
|
771
|
+
maxRetries: options.maxRetries,
|
|
772
|
+
});
|
|
773
|
+
const payload = JSON.parse(JSON.stringify(detail.json));
|
|
774
|
+
const refs = collectRefs(payload);
|
|
775
|
+
await fetchLatestRefs({
|
|
776
|
+
projectBaseUrl: options.auth.projectBaseUrl,
|
|
777
|
+
publishableKey: options.auth.publishableKey,
|
|
778
|
+
accessToken: options.auth.accessToken,
|
|
779
|
+
refs,
|
|
780
|
+
cache: options.refCache,
|
|
781
|
+
fetchImpl: options.fetchImpl,
|
|
782
|
+
timeoutMs: options.timeoutMs,
|
|
783
|
+
maxRetries: options.maxRetries,
|
|
784
|
+
});
|
|
785
|
+
const update = updateProcessJson(payload, refs, options.refCache);
|
|
786
|
+
const validation = options.validateProcessPayloadImpl(payload);
|
|
787
|
+
if (!validation.ok || update.unresolved_refs.length > 0) {
|
|
788
|
+
const noteParts = [];
|
|
789
|
+
if (!validation.ok) {
|
|
790
|
+
noteParts.push(`schema_issue_count=${validation.issue_count}`);
|
|
791
|
+
}
|
|
792
|
+
if (update.unresolved_refs.length > 0) {
|
|
793
|
+
noteParts.push(`unresolved_refs=${update.unresolved_refs.length}`);
|
|
794
|
+
}
|
|
795
|
+
const record = {
|
|
796
|
+
time,
|
|
797
|
+
key,
|
|
798
|
+
id: options.row.id,
|
|
799
|
+
version: options.row.version,
|
|
800
|
+
status: 'validation_blocked',
|
|
801
|
+
ref_count: refs.length,
|
|
802
|
+
version_updates: update.version_updates,
|
|
803
|
+
description_updates: update.description_updates,
|
|
804
|
+
unresolved_count: update.unresolved_refs.length,
|
|
805
|
+
changed_ref_count: update.touched_refs.length,
|
|
806
|
+
schema_validator: validation.validator,
|
|
807
|
+
schema_issue_count: validation.issue_count,
|
|
808
|
+
schema_issues: validation.issues.slice(0, 20),
|
|
809
|
+
touched_refs: update.touched_refs.slice(0, 50),
|
|
810
|
+
unresolved_refs: update.unresolved_refs.slice(0, 50),
|
|
811
|
+
note: noteParts.join('; '),
|
|
812
|
+
};
|
|
813
|
+
writeJsonLinesArtifact(options.files.validation_blockers_jsonl, record, { append: true });
|
|
814
|
+
writeJsonLinesArtifact(options.files.progress_jsonl, record, { append: true });
|
|
815
|
+
appendReportRow(options.files.report_md, record);
|
|
816
|
+
return record;
|
|
817
|
+
}
|
|
818
|
+
let writeResult = null;
|
|
819
|
+
if (options.apply) {
|
|
820
|
+
writeResult = await options.syncStateAwareProcessRecordImpl({
|
|
821
|
+
id: detail.id,
|
|
822
|
+
version: detail.version,
|
|
823
|
+
payload,
|
|
824
|
+
env: options.env,
|
|
825
|
+
fetchImpl: options.fetchImpl,
|
|
826
|
+
timeoutMs: options.timeoutMs,
|
|
827
|
+
audit: {
|
|
828
|
+
command: 'process_refresh_references',
|
|
829
|
+
source: 'tiangong-lca process refresh-references',
|
|
830
|
+
},
|
|
831
|
+
modelId: detail.model_id,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
const record = {
|
|
835
|
+
time,
|
|
836
|
+
key,
|
|
837
|
+
id: options.row.id,
|
|
838
|
+
version: options.row.version,
|
|
839
|
+
status: options.apply ? 'saved' : 'dry_run',
|
|
840
|
+
ref_count: refs.length,
|
|
841
|
+
version_updates: update.version_updates,
|
|
842
|
+
description_updates: update.description_updates,
|
|
843
|
+
unresolved_count: update.unresolved_refs.length,
|
|
844
|
+
changed_ref_count: update.touched_refs.length,
|
|
845
|
+
schema_validator: validation.validator,
|
|
846
|
+
schema_issue_count: validation.issue_count,
|
|
847
|
+
touched_refs: update.touched_refs.slice(0, 50),
|
|
848
|
+
unresolved_refs: update.unresolved_refs.slice(0, 50),
|
|
849
|
+
write_path: writeResult && 'write_path' in writeResult ? writeResult.write_path : undefined,
|
|
850
|
+
write_operation: writeResult?.operation,
|
|
851
|
+
};
|
|
852
|
+
writeJsonLinesArtifact(options.files.progress_jsonl, record, { append: true });
|
|
853
|
+
appendReportRow(options.files.report_md, record);
|
|
854
|
+
return record;
|
|
855
|
+
}
|
|
856
|
+
catch (error) {
|
|
857
|
+
const record = {
|
|
858
|
+
time,
|
|
859
|
+
key,
|
|
860
|
+
id: options.row.id,
|
|
861
|
+
version: options.row.version,
|
|
862
|
+
status: 'error',
|
|
863
|
+
error: error instanceof Error ? error.message : String(error),
|
|
864
|
+
};
|
|
865
|
+
writeJsonLinesArtifact(options.files.errors_jsonl, record, { append: true });
|
|
866
|
+
writeJsonLinesArtifact(options.files.progress_jsonl, record, { append: true });
|
|
867
|
+
appendReportRow(options.files.report_md, record);
|
|
868
|
+
return record;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
async function workerPool(items, concurrency, worker) {
|
|
872
|
+
let cursor = 0;
|
|
873
|
+
const results = [];
|
|
874
|
+
async function runWorker() {
|
|
875
|
+
while (cursor < items.length) {
|
|
876
|
+
const currentIndex = cursor;
|
|
877
|
+
cursor += 1;
|
|
878
|
+
results[currentIndex] = await worker(items[currentIndex], currentIndex);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
await Promise.all(Array.from({ length: concurrency }, () => runWorker()));
|
|
882
|
+
return results;
|
|
883
|
+
}
|
|
884
|
+
export async function runProcessRefreshReferences(options) {
|
|
885
|
+
const outDir = path.resolve(requiredNonEmpty(options.outDir, '--out-dir', 'PROCESS_REFRESH_OUT_DIR_REQUIRED'));
|
|
886
|
+
const apply = Boolean(options.apply);
|
|
887
|
+
const reuseManifest = Boolean(options.reuseManifest);
|
|
888
|
+
const limit = toOptionalPositiveInteger(options.limit ?? null, '--limit', 'PROCESS_REFRESH_LIMIT_INVALID');
|
|
889
|
+
const pageSize = toPositiveInteger(options.pageSize ?? DEFAULT_PAGE_SIZE, '--page-size', 'PROCESS_REFRESH_PAGE_SIZE_INVALID', MAX_PAGE_SIZE);
|
|
890
|
+
const concurrency = toPositiveInteger(options.concurrency ?? DEFAULT_CONCURRENCY, '--concurrency', 'PROCESS_REFRESH_CONCURRENCY_INVALID', MAX_CONCURRENCY);
|
|
891
|
+
const timeoutMs = toPositiveInteger(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, 'timeout', 'PROCESS_REFRESH_TIMEOUT_INVALID');
|
|
892
|
+
const maxRetries = toPositiveInteger(options.maxRetries ?? DEFAULT_MAX_RETRIES, 'retry count', 'PROCESS_REFRESH_MAX_RETRIES_INVALID');
|
|
893
|
+
const env = options.env ?? process.env;
|
|
894
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
895
|
+
const generatedAtUtc = nowIso(options.now);
|
|
896
|
+
const validateProcessPayloadImpl = options.validateProcessPayloadImpl ?? validateProcessPayload;
|
|
897
|
+
const syncStateAwareProcessRecordImpl = options.syncStateAwareProcessRecordImpl ?? syncStateAwareProcessRecord;
|
|
898
|
+
mkdirSync(outDir, { recursive: true });
|
|
899
|
+
const files = {
|
|
900
|
+
manifest: path.join(outDir, 'inputs', 'processes.manifest.json'),
|
|
901
|
+
progress_jsonl: path.join(outDir, 'outputs', 'progress.jsonl'),
|
|
902
|
+
errors_jsonl: path.join(outDir, 'outputs', 'errors.jsonl'),
|
|
903
|
+
validation_blockers_jsonl: path.join(outDir, 'outputs', 'validation-blockers.jsonl'),
|
|
904
|
+
summary_json: path.join(outDir, 'outputs', 'summary.json'),
|
|
905
|
+
report_md: path.join(outDir, 'reports', 'process-refresh-references.md'),
|
|
906
|
+
};
|
|
907
|
+
const auth = await resolveRemoteAuth({
|
|
908
|
+
env,
|
|
909
|
+
fetchImpl,
|
|
910
|
+
timeoutMs,
|
|
911
|
+
now: options.now ?? new Date(),
|
|
912
|
+
maxRetries,
|
|
913
|
+
});
|
|
914
|
+
let manifest = readManifest(files.manifest);
|
|
915
|
+
if (!manifest || !reuseManifest) {
|
|
916
|
+
const rows = await snapshotProcesses({
|
|
917
|
+
projectBaseUrl: auth.projectBaseUrl,
|
|
918
|
+
publishableKey: auth.publishableKey,
|
|
919
|
+
accessToken: auth.accessToken,
|
|
920
|
+
userId: auth.userId,
|
|
921
|
+
pageSize,
|
|
922
|
+
fetchImpl,
|
|
923
|
+
timeoutMs,
|
|
924
|
+
maxRetries,
|
|
925
|
+
});
|
|
926
|
+
manifest = {
|
|
927
|
+
schema_version: 1,
|
|
928
|
+
generated_at_utc: generatedAtUtc,
|
|
929
|
+
user_id: auth.userId,
|
|
930
|
+
masked_user_email: auth.maskedUserEmail,
|
|
931
|
+
source: 'current_user_processes',
|
|
932
|
+
order: 'modified_at.desc,id.asc',
|
|
933
|
+
page_size: pageSize,
|
|
934
|
+
count: rows.length,
|
|
935
|
+
rows,
|
|
936
|
+
};
|
|
937
|
+
writeJsonArtifact(files.manifest, manifest);
|
|
938
|
+
}
|
|
939
|
+
appendReportHeader(files.report_md, {
|
|
940
|
+
mode: apply ? 'apply' : 'dry_run',
|
|
941
|
+
generatedAtUtc,
|
|
942
|
+
manifestCount: manifest.rows.length,
|
|
943
|
+
});
|
|
944
|
+
const completed = readCompleted(files.progress_jsonl, apply);
|
|
945
|
+
const selectedRows = manifest.rows.slice(0, limit ?? manifest.rows.length);
|
|
946
|
+
const pendingRows = selectedRows.filter((row) => !completed.has(recordKey(row)));
|
|
947
|
+
const refCache = new Map();
|
|
948
|
+
const counts = {
|
|
949
|
+
manifest: manifest.rows.length,
|
|
950
|
+
selected: selectedRows.length,
|
|
951
|
+
already_completed: selectedRows.length - pendingRows.length,
|
|
952
|
+
pending: pendingRows.length,
|
|
953
|
+
saved: 0,
|
|
954
|
+
dry_run: 0,
|
|
955
|
+
skipped: 0,
|
|
956
|
+
validation_blocked: 0,
|
|
957
|
+
errors: 0,
|
|
958
|
+
};
|
|
959
|
+
const results = await workerPool(pendingRows, concurrency, (row) => processOne({
|
|
960
|
+
row,
|
|
961
|
+
apply,
|
|
962
|
+
files,
|
|
963
|
+
auth,
|
|
964
|
+
fetchImpl,
|
|
965
|
+
timeoutMs,
|
|
966
|
+
maxRetries,
|
|
967
|
+
env,
|
|
968
|
+
validateProcessPayloadImpl,
|
|
969
|
+
syncStateAwareProcessRecordImpl,
|
|
970
|
+
refCache,
|
|
971
|
+
}));
|
|
972
|
+
for (const record of results) {
|
|
973
|
+
if (record.status === 'saved') {
|
|
974
|
+
counts.saved += 1;
|
|
975
|
+
}
|
|
976
|
+
else if (record.status === 'dry_run') {
|
|
977
|
+
counts.dry_run += 1;
|
|
978
|
+
}
|
|
979
|
+
else if (record.status === 'skipped') {
|
|
980
|
+
counts.skipped += 1;
|
|
981
|
+
}
|
|
982
|
+
else if (record.status === 'validation_blocked') {
|
|
983
|
+
counts.validation_blocked += 1;
|
|
984
|
+
}
|
|
985
|
+
else if (record.status === 'error') {
|
|
986
|
+
counts.errors += 1;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
const report = {
|
|
990
|
+
schema_version: 1,
|
|
991
|
+
generated_at_utc: generatedAtUtc,
|
|
992
|
+
status: counts.errors > 0
|
|
993
|
+
? 'completed_process_reference_refresh_with_errors'
|
|
994
|
+
: 'completed_process_reference_refresh',
|
|
995
|
+
out_dir: outDir,
|
|
996
|
+
mode: apply ? 'apply' : 'dry_run',
|
|
997
|
+
user_id: manifest.user_id,
|
|
998
|
+
masked_user_email: manifest.masked_user_email,
|
|
999
|
+
counts,
|
|
1000
|
+
files,
|
|
1001
|
+
};
|
|
1002
|
+
writeJsonArtifact(files.summary_json, report);
|
|
1003
|
+
appendFileSync(files.report_md, `\n## Summary\n\n\`\`\`json\n${JSON.stringify(report, null, 2)}\n\`\`\`\n`, 'utf8');
|
|
1004
|
+
return report;
|
|
1005
|
+
}
|
|
1006
|
+
export const __testInternals = {
|
|
1007
|
+
appendReportHeader,
|
|
1008
|
+
collectRefs,
|
|
1009
|
+
compareVersions,
|
|
1010
|
+
fetchLatestRefs,
|
|
1011
|
+
fetchJsonWithRetry,
|
|
1012
|
+
genFlowName,
|
|
1013
|
+
genFlowNameJson,
|
|
1014
|
+
genProcessName,
|
|
1015
|
+
genProcessNameJson,
|
|
1016
|
+
getLangList,
|
|
1017
|
+
getLangText,
|
|
1018
|
+
getShortDescription,
|
|
1019
|
+
normalizeDatasetPayload,
|
|
1020
|
+
normalizeManifestRow,
|
|
1021
|
+
parseJsonResponse,
|
|
1022
|
+
parseContentRangeTotal,
|
|
1023
|
+
readCompleted,
|
|
1024
|
+
readManifest,
|
|
1025
|
+
recordKey,
|
|
1026
|
+
updateProcessJson,
|
|
1027
|
+
};
|
|
1028
|
+
//# sourceMappingURL=process-refresh-references.js.map
|