@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.
Files changed (58) hide show
  1. package/README.md +73 -30
  2. package/dist/src/cli.js +912 -113
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/lib/dataset-local.js +231 -0
  5. package/dist/src/lib/dataset-local.js.map +1 -0
  6. package/dist/src/lib/dataset-references-rewrite.js +214 -0
  7. package/dist/src/lib/dataset-references-rewrite.js.map +1 -0
  8. package/dist/src/lib/dataset-validate.js +191 -0
  9. package/dist/src/lib/dataset-validate.js.map +1 -0
  10. package/dist/src/lib/flow-regen-product.js +2 -10
  11. package/dist/src/lib/flow-regen-product.js.map +1 -1
  12. package/dist/src/lib/flow-remediate.js +4 -8
  13. package/dist/src/lib/flow-remediate.js.map +1 -1
  14. package/dist/src/lib/lifecyclemodel-auto-build.js +11 -8
  15. package/dist/src/lib/lifecyclemodel-auto-build.js.map +1 -1
  16. package/dist/src/lib/lifecyclemodel-graph.js +248 -0
  17. package/dist/src/lib/lifecyclemodel-graph.js.map +1 -0
  18. package/dist/src/lib/lifecyclemodel-publish-build.js +1 -1
  19. package/dist/src/lib/lifecyclemodel-publish-build.js.map +1 -1
  20. package/dist/src/lib/lifecyclemodel-save-draft-run.js +245 -0
  21. package/dist/src/lib/lifecyclemodel-save-draft-run.js.map +1 -0
  22. package/dist/src/lib/lifecyclemodel-validate-build.js +1 -1
  23. package/dist/src/lib/lifecyclemodel-validate-build.js.map +1 -1
  24. package/dist/src/lib/process-auto-build.js +11 -8
  25. package/dist/src/lib/process-auto-build.js.map +1 -1
  26. package/dist/src/lib/process-batch-build.js +6 -3
  27. package/dist/src/lib/process-batch-build.js.map +1 -1
  28. package/dist/src/lib/process-dedup-review.js +870 -0
  29. package/dist/src/lib/process-dedup-review.js.map +1 -0
  30. package/dist/src/lib/process-payload-validation.js +50 -0
  31. package/dist/src/lib/process-payload-validation.js.map +1 -0
  32. package/dist/src/lib/process-publish-build.js +4 -6
  33. package/dist/src/lib/process-publish-build.js.map +1 -1
  34. package/dist/src/lib/process-refresh-references.js +1028 -0
  35. package/dist/src/lib/process-refresh-references.js.map +1 -0
  36. package/dist/src/lib/process-resume-build.js +4 -6
  37. package/dist/src/lib/process-resume-build.js.map +1 -1
  38. package/dist/src/lib/process-save-draft-run.js +31 -12
  39. package/dist/src/lib/process-save-draft-run.js.map +1 -1
  40. package/dist/src/lib/process-scope-statistics.js +859 -0
  41. package/dist/src/lib/process-scope-statistics.js.map +1 -0
  42. package/dist/src/lib/process-verify-rows.js +250 -0
  43. package/dist/src/lib/process-verify-rows.js.map +1 -0
  44. package/dist/src/lib/publish.js +1 -1
  45. package/dist/src/lib/publish.js.map +1 -1
  46. package/dist/src/lib/remote.js +4 -4
  47. package/dist/src/lib/remote.js.map +1 -1
  48. package/dist/src/lib/review-lifecyclemodel.js +2 -2
  49. package/dist/src/lib/review-lifecyclemodel.js.map +1 -1
  50. package/dist/src/lib/tidas-sdk-package-validator.js +28 -9
  51. package/dist/src/lib/tidas-sdk-package-validator.js.map +1 -1
  52. package/dist/src/lib/tidas-sdk-validation.js +96 -0
  53. package/dist/src/lib/tidas-sdk-validation.js.map +1 -0
  54. package/dist/src/lib/user-api-key.js +1 -1
  55. package/dist/src/lib/user-api-key.js.map +1 -1
  56. package/package.json +4 -4
  57. /package/bin/{tiangong.d.ts → tiangong-lca.d.ts} +0 -0
  58. /package/bin/{tiangong.js → tiangong-lca.js} +0 -0
@@ -0,0 +1,859 @@
1
+ import path from 'node:path';
2
+ import { readJsonArtifact, readJsonLinesArtifact, writeJsonArtifact, writeJsonLinesArtifact, writeTextArtifact, } from './artifacts.js';
3
+ import { CliError } from './errors.js';
4
+ import { deriveSupabaseProjectBaseUrl, requireSupabaseRestRuntime } from './supabase-client.js';
5
+ import { resolveSupabaseUserSession } from './supabase-session.js';
6
+ import { redactEmail, requireUserApiKeyCredentials } from './user-api-key.js';
7
+ const DEFAULT_STATE_CODES = [0, 100];
8
+ const DEFAULT_PAGE_SIZE = 200;
9
+ const DEFAULT_TIMEOUT_MS = 30_000;
10
+ const DEFAULT_MAX_RETRIES = 4;
11
+ function isRecord(value) {
12
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
13
+ }
14
+ function trimText(value) {
15
+ return typeof value === 'string' ? value.trim() : '';
16
+ }
17
+ function normalizeOptionalToken(value) {
18
+ const trimmed = trimText(value);
19
+ return trimmed ? trimmed : null;
20
+ }
21
+ function nowIso(now = new Date()) {
22
+ return now.toISOString();
23
+ }
24
+ function toPositiveInteger(value, label, code) {
25
+ if (!Number.isInteger(value) || value <= 0) {
26
+ throw new CliError(`Expected ${label} to be a positive integer.`, {
27
+ code,
28
+ exitCode: 2,
29
+ });
30
+ }
31
+ return value;
32
+ }
33
+ function normalizeStateCodes(value) {
34
+ if (!Array.isArray(value) || value.length === 0) {
35
+ return [...DEFAULT_STATE_CODES];
36
+ }
37
+ const seen = new Set();
38
+ const normalized = [];
39
+ for (const entry of value) {
40
+ if (!Number.isInteger(entry) || entry < 0) {
41
+ throw new CliError('Expected --state-codes to contain only non-negative integers.', {
42
+ code: 'PROCESS_SCOPE_STATE_CODES_INVALID',
43
+ exitCode: 2,
44
+ });
45
+ }
46
+ if (!seen.has(entry)) {
47
+ normalized.push(entry);
48
+ seen.add(entry);
49
+ }
50
+ }
51
+ if (normalized.length === 0) {
52
+ throw new CliError('--state-codes must contain at least one state code.', {
53
+ code: 'PROCESS_SCOPE_STATE_CODES_REQUIRED',
54
+ exitCode: 2,
55
+ });
56
+ }
57
+ return normalized;
58
+ }
59
+ function ensureScope(value) {
60
+ if (value === undefined || value === 'visible') {
61
+ return 'visible';
62
+ }
63
+ if (value === 'current-user') {
64
+ return 'current-user';
65
+ }
66
+ throw new CliError("Expected --scope to be either 'visible' or 'current-user'.", {
67
+ code: 'PROCESS_SCOPE_SCOPE_INVALID',
68
+ exitCode: 2,
69
+ });
70
+ }
71
+ async function parseJsonResponse(response, label) {
72
+ const text = await response.text();
73
+ const contentType = response.headers.get('content-type') ?? '';
74
+ if (!text.trim()) {
75
+ return null;
76
+ }
77
+ if (!contentType.includes('application/json')) {
78
+ return text;
79
+ }
80
+ try {
81
+ return JSON.parse(text);
82
+ }
83
+ catch (error) {
84
+ throw new CliError(`${label} returned invalid JSON.`, {
85
+ code: 'PROCESS_SCOPE_REMOTE_INVALID_JSON',
86
+ exitCode: 1,
87
+ details: String(error),
88
+ });
89
+ }
90
+ }
91
+ async function fetchJsonWithRetry(options) {
92
+ let lastError = null;
93
+ for (let attempt = 1; attempt <= options.maxRetries; attempt += 1) {
94
+ try {
95
+ const response = await options.fetchImpl(options.url, {
96
+ ...options.init,
97
+ signal: AbortSignal.timeout(options.timeoutMs),
98
+ });
99
+ const body = await parseJsonResponse(response, options.label);
100
+ if (!response.ok) {
101
+ throw new CliError(`${options.label} failed with ${response.status}.`, {
102
+ code: 'PROCESS_SCOPE_REMOTE_REQUEST_FAILED',
103
+ exitCode: 1,
104
+ details: {
105
+ status: response.status,
106
+ body,
107
+ url: options.url,
108
+ },
109
+ });
110
+ }
111
+ return {
112
+ status: response.status,
113
+ headers: new Headers({
114
+ 'content-range': response.headers.get('content-range') ?? '',
115
+ 'content-type': response.headers.get('content-type') ?? '',
116
+ }),
117
+ body,
118
+ };
119
+ }
120
+ catch (error) {
121
+ lastError = error;
122
+ if (attempt >= options.maxRetries) {
123
+ break;
124
+ }
125
+ await new Promise((resolve) => setTimeout(resolve, attempt * 1_500));
126
+ }
127
+ }
128
+ if (lastError instanceof CliError) {
129
+ throw lastError;
130
+ }
131
+ throw new CliError(`${options.label} failed after ${options.maxRetries} attempt(s).`, {
132
+ code: 'PROCESS_SCOPE_REMOTE_REQUEST_FAILED',
133
+ exitCode: 1,
134
+ details: String(lastError),
135
+ });
136
+ }
137
+ async function resolveCurrentUserId(options) {
138
+ const runtime = requireSupabaseRestRuntime(options.env);
139
+ const session = await resolveSupabaseUserSession({
140
+ runtime,
141
+ fetchImpl: options.fetchImpl,
142
+ timeoutMs: options.timeoutMs,
143
+ now: options.now,
144
+ });
145
+ const body = await fetchJsonWithRetry({
146
+ url: `${deriveSupabaseProjectBaseUrl(runtime.apiBaseUrl)}/auth/v1/user`,
147
+ init: {
148
+ method: 'GET',
149
+ headers: {
150
+ apikey: runtime.publishableKey,
151
+ Authorization: `Bearer ${session.accessToken}`,
152
+ Accept: 'application/json',
153
+ },
154
+ },
155
+ label: 'supabase current-user lookup',
156
+ fetchImpl: options.fetchImpl,
157
+ timeoutMs: options.timeoutMs,
158
+ maxRetries: options.maxRetries,
159
+ });
160
+ const userId = isRecord(body.body) ? normalizeOptionalToken(body.body.id) : null;
161
+ if (!userId) {
162
+ throw new CliError('Supabase current-user lookup succeeded without a user id.', {
163
+ code: 'PROCESS_SCOPE_CURRENT_USER_ID_MISSING',
164
+ exitCode: 1,
165
+ });
166
+ }
167
+ return userId;
168
+ }
169
+ async function fetchProcessRows(options) {
170
+ const runtime = requireSupabaseRestRuntime(options.env);
171
+ const session = await resolveSupabaseUserSession({
172
+ runtime,
173
+ fetchImpl: options.fetchImpl,
174
+ timeoutMs: options.timeoutMs,
175
+ });
176
+ const projectBaseUrl = deriveSupabaseProjectBaseUrl(runtime.apiBaseUrl);
177
+ const rows = [];
178
+ let total = 0;
179
+ for (const stateCode of options.stateCodes) {
180
+ let stateTotal = null;
181
+ let cursorId = '';
182
+ while (true) {
183
+ const url = new URL(`${projectBaseUrl}/rest/v1/processes`);
184
+ url.searchParams.set('select', 'id,version,state_code,user_id,modified_at,model_id,json');
185
+ url.searchParams.set('state_code', `eq.${stateCode}`);
186
+ if (options.scope === 'current-user') {
187
+ url.searchParams.set('user_id', `eq.${options.userId ?? ''}`);
188
+ }
189
+ if (cursorId) {
190
+ url.searchParams.set('id', `gt.${cursorId}`);
191
+ }
192
+ url.searchParams.set('order', 'id.asc');
193
+ url.searchParams.set('limit', String(options.pageSize));
194
+ const page = await fetchJsonWithRetry({
195
+ url: url.toString(),
196
+ init: {
197
+ method: 'GET',
198
+ headers: {
199
+ apikey: runtime.publishableKey,
200
+ Authorization: `Bearer ${session.accessToken}`,
201
+ Accept: 'application/json',
202
+ ...(stateTotal === null ? { Prefer: 'count=exact' } : {}),
203
+ },
204
+ },
205
+ label: `process scope statistics page fetch (state_code=${stateCode})`,
206
+ fetchImpl: options.fetchImpl,
207
+ timeoutMs: options.timeoutMs,
208
+ maxRetries: options.maxRetries,
209
+ });
210
+ const pageRows = Array.isArray(page.body) ? page.body : [];
211
+ const normalizedRows = pageRows
212
+ .map((row) => normalizeSnapshotRow(row))
213
+ .filter((row) => row !== null);
214
+ rows.push(...normalizedRows);
215
+ if (stateTotal === null) {
216
+ const match = page.headers.get('content-range')?.match(/\/(\d+)$/u) ?? null;
217
+ stateTotal = match ? Number.parseInt(match[1], 10) : null;
218
+ if (stateTotal !== null) {
219
+ total += stateTotal;
220
+ }
221
+ }
222
+ if (normalizedRows.length < options.pageSize) {
223
+ break;
224
+ }
225
+ const lastRow = normalizedRows[normalizedRows.length - 1];
226
+ cursorId = lastRow.id;
227
+ }
228
+ }
229
+ return {
230
+ rows,
231
+ total: total || null,
232
+ };
233
+ }
234
+ function normalizeSnapshotRow(value) {
235
+ if (!isRecord(value)) {
236
+ return null;
237
+ }
238
+ const id = normalizeOptionalToken(value.id);
239
+ const version = normalizeOptionalToken(value.version);
240
+ const payload = normalizePayload(value.json);
241
+ if (!id || !version || !payload) {
242
+ return null;
243
+ }
244
+ return {
245
+ id,
246
+ version,
247
+ state_code: typeof value.state_code === 'number' ? value.state_code : null,
248
+ user_id: normalizeOptionalToken(value.user_id),
249
+ modified_at: normalizeOptionalToken(value.modified_at),
250
+ model_id: normalizeOptionalToken(value.model_id),
251
+ json: payload,
252
+ };
253
+ }
254
+ function normalizePayload(value) {
255
+ if (isRecord(value)) {
256
+ return value;
257
+ }
258
+ if (typeof value === 'string' && value.trim()) {
259
+ try {
260
+ const parsed = JSON.parse(value);
261
+ return isRecord(parsed) ? parsed : null;
262
+ }
263
+ catch {
264
+ return null;
265
+ }
266
+ }
267
+ return null;
268
+ }
269
+ function getLangList(value) {
270
+ if (value === null || value === undefined || value === '') {
271
+ return [];
272
+ }
273
+ if (Array.isArray(value)) {
274
+ return value.filter((item) => isRecord(item));
275
+ }
276
+ if (isRecord(value)) {
277
+ const langString = value['common:langString'];
278
+ if (Array.isArray(langString)) {
279
+ return langString.filter((item) => isRecord(item));
280
+ }
281
+ if (isRecord(langString)) {
282
+ return [langString];
283
+ }
284
+ if (typeof value['#text'] === 'string' || typeof value['@xml:lang'] === 'string') {
285
+ return [value];
286
+ }
287
+ }
288
+ if (typeof value === 'string') {
289
+ return [{ '@xml:lang': 'en', '#text': value }];
290
+ }
291
+ return [];
292
+ }
293
+ function getLangText(value, lang) {
294
+ const list = getLangList(value);
295
+ const exact = lang === null
296
+ ? null
297
+ : list.find((item) => trimText(item['@xml:lang']).toLowerCase() === lang.toLowerCase() &&
298
+ trimText(item['#text']));
299
+ const fallback = list.find((item) => trimText(item['#text']));
300
+ return trimText((exact ?? fallback)?.['#text']);
301
+ }
302
+ function getAnyLangText(value) {
303
+ return getLangText(value, null);
304
+ }
305
+ function asArray(value) {
306
+ if (value === null || value === undefined || value === '') {
307
+ return [];
308
+ }
309
+ return Array.isArray(value) ? value : [value];
310
+ }
311
+ function extractClassificationEntries(dataSetInformation) {
312
+ const classes = asArray(dataSetInformation.classificationInformation?.['common:classification'] &&
313
+ dataSetInformation.classificationInformation['common:classification']?.['common:class']);
314
+ return classes
315
+ .map((entry) => {
316
+ if (!isRecord(entry)) {
317
+ return null;
318
+ }
319
+ const level = Number.parseInt(trimText(entry['@level']) || '0', 10);
320
+ const text = trimText(entry['#text']);
321
+ if (!text) {
322
+ return null;
323
+ }
324
+ return { level, text };
325
+ })
326
+ .filter((entry) => entry !== null)
327
+ .sort((left, right) => left.level - right.level);
328
+ }
329
+ function normalizeSignature(text) {
330
+ return trimText(text)
331
+ .normalize('NFKC')
332
+ .toLowerCase()
333
+ .replace(/[\u3000\u00a0]/gu, ' ')
334
+ .replace(/[,,;;::()()[\]{}<>|/\\]+/gu, ' ')
335
+ .replace(/\s+/gu, ' ')
336
+ .trim();
337
+ }
338
+ function firstClause(text) {
339
+ const normalized = trimText(text);
340
+ if (!normalized) {
341
+ return '';
342
+ }
343
+ const [first] = normalized.split(/[.;;。]/u);
344
+ return first.trim();
345
+ }
346
+ function renderProcessName(name) {
347
+ const parts = [
348
+ getLangText(name.baseName, 'en') ||
349
+ getLangText(name.baseName, 'zh') ||
350
+ getAnyLangText(name.baseName),
351
+ getLangText(name.treatmentStandardsRoutes, 'en') ||
352
+ getLangText(name.treatmentStandardsRoutes, 'zh') ||
353
+ getAnyLangText(name.treatmentStandardsRoutes),
354
+ getLangText(name.mixAndLocationTypes, 'en') ||
355
+ getLangText(name.mixAndLocationTypes, 'zh') ||
356
+ getAnyLangText(name.mixAndLocationTypes),
357
+ getLangText(name.functionalUnitFlowProperties, 'en') ||
358
+ getLangText(name.functionalUnitFlowProperties, 'zh') ||
359
+ getAnyLangText(name.functionalUnitFlowProperties),
360
+ ].filter((part) => trimText(part));
361
+ return parts.join('; ');
362
+ }
363
+ function extractCraftCandidate(dataSetInformation, technology) {
364
+ const name = isRecord(dataSetInformation.name) ? dataSetInformation.name : {};
365
+ const candidates = [
366
+ {
367
+ source_kind: 'treatmentStandardsRoutes',
368
+ label: getLangText(name.treatmentStandardsRoutes, 'en') ||
369
+ getLangText(name.treatmentStandardsRoutes, 'zh') ||
370
+ getAnyLangText(name.treatmentStandardsRoutes),
371
+ },
372
+ {
373
+ source_kind: 'technologyDescriptionAndIncludedProcesses',
374
+ label: firstClause(getLangText(technology.technologyDescriptionAndIncludedProcesses, 'en')) ||
375
+ firstClause(getLangText(technology.technologyDescriptionAndIncludedProcesses, 'zh')) ||
376
+ firstClause(getAnyLangText(technology.technologyDescriptionAndIncludedProcesses)),
377
+ },
378
+ {
379
+ source_kind: 'baseName',
380
+ label: getLangText(name.baseName, 'en') ||
381
+ getLangText(name.baseName, 'zh') ||
382
+ getAnyLangText(name.baseName),
383
+ },
384
+ ];
385
+ for (const candidate of candidates) {
386
+ if (trimText(candidate.label)) {
387
+ return {
388
+ ...candidate,
389
+ signature: normalizeSignature(candidate.label),
390
+ };
391
+ }
392
+ }
393
+ return {
394
+ source_kind: 'missing',
395
+ label: '',
396
+ signature: '',
397
+ };
398
+ }
399
+ function extractReferenceProduct(processDataSet, dataSetInformation) {
400
+ const processInformation = isRecord(processDataSet.processInformation)
401
+ ? processDataSet.processInformation
402
+ : {};
403
+ const quantRef = isRecord(processInformation.quantitativeReference)
404
+ ? processInformation.quantitativeReference
405
+ : {};
406
+ const refInternalId = trimText(quantRef.referenceToReferenceFlow);
407
+ const exchangesBlock = isRecord(processDataSet.exchanges) ? processDataSet.exchanges : {};
408
+ const exchanges = asArray(exchangesBlock.exchange);
409
+ const refExchange = exchanges.find((exchange) => isRecord(exchange) && trimText(exchange['@dataSetInternalID']) === refInternalId);
410
+ const exchange = isRecord(refExchange) ? refExchange : null;
411
+ const flowRef = exchange && isRecord(exchange.referenceToFlowDataSet) ? exchange.referenceToFlowDataSet : {};
412
+ const flowRefId = trimText(flowRef['@refObjectId']);
413
+ const flowRefVersion = trimText(flowRef['@version']);
414
+ const shortDescription = getLangText(flowRef['common:shortDescription'], 'en') ||
415
+ getLangText(flowRef['common:shortDescription'], 'zh') ||
416
+ getAnyLangText(flowRef['common:shortDescription']);
417
+ const name = isRecord(dataSetInformation.name) ? dataSetInformation.name : {};
418
+ const fallbackBaseName = getLangText(name.baseName, 'en') ||
419
+ getLangText(name.baseName, 'zh') ||
420
+ getAnyLangText(name.baseName);
421
+ const fallbackProcessName = renderProcessName(name);
422
+ if (flowRefId) {
423
+ return {
424
+ key: `flow:${flowRefId}`,
425
+ stable_flow_id: flowRefId,
426
+ stable_flow_version: flowRefVersion,
427
+ label: shortDescription || fallbackProcessName || flowRefId,
428
+ source_kind: 'reference_flow_id',
429
+ missing_reference_exchange: exchange === null,
430
+ };
431
+ }
432
+ if (shortDescription) {
433
+ return {
434
+ key: `label:${normalizeSignature(shortDescription)}`,
435
+ stable_flow_id: '',
436
+ stable_flow_version: '',
437
+ label: shortDescription,
438
+ source_kind: 'reference_flow_short_description',
439
+ missing_reference_exchange: exchange === null,
440
+ };
441
+ }
442
+ if (fallbackBaseName) {
443
+ return {
444
+ key: `base:${normalizeSignature(fallbackBaseName)}`,
445
+ stable_flow_id: '',
446
+ stable_flow_version: '',
447
+ label: fallbackBaseName,
448
+ source_kind: 'process_base_name',
449
+ missing_reference_exchange: exchange === null,
450
+ };
451
+ }
452
+ return {
453
+ key: '',
454
+ stable_flow_id: '',
455
+ stable_flow_version: '',
456
+ label: '',
457
+ source_kind: 'missing',
458
+ missing_reference_exchange: exchange === null,
459
+ };
460
+ }
461
+ function incrementCount(map, key, seed = {}) {
462
+ const existing = map.get(key) ?? {
463
+ count: 0,
464
+ ...seed,
465
+ };
466
+ existing.count += 1;
467
+ map.set(key, existing);
468
+ }
469
+ function toSortedArray(map) {
470
+ return [...map.entries()]
471
+ .map(([key, value]) => ({
472
+ key,
473
+ ...value,
474
+ }))
475
+ .sort((left, right) => right.count - left.count || left.key.localeCompare(right.key));
476
+ }
477
+ function calculateStatistics(rows, options, metadata, generatedAtUtc) {
478
+ const stateCodeCounts = new Map();
479
+ const typeOfDataSetCounts = new Map();
480
+ const domainPrimaryCounts = new Map();
481
+ const domainLeafCounts = new Map();
482
+ const craftCounts = new Map();
483
+ const craftSourceCounts = new Map();
484
+ const productCounts = new Map();
485
+ const productSourceCounts = new Map();
486
+ const ownerCounts = new Map();
487
+ let rowsMissingClassification = 0;
488
+ let rowsMissingCraft = 0;
489
+ let rowsMissingProduct = 0;
490
+ let rowsMissingReferenceExchange = 0;
491
+ let unitProcessRows = 0;
492
+ for (const row of rows) {
493
+ incrementCount(stateCodeCounts, String(row.state_code ?? 'null'));
494
+ incrementCount(ownerCounts, row.user_id ?? 'missing');
495
+ const processDataSet = isRecord(row.json.processDataSet) ? row.json.processDataSet : {};
496
+ const processInformation = isRecord(processDataSet.processInformation)
497
+ ? processDataSet.processInformation
498
+ : {};
499
+ const dataSetInformation = isRecord(processInformation.dataSetInformation)
500
+ ? processInformation.dataSetInformation
501
+ : {};
502
+ const technology = isRecord(processInformation.technology) ? processInformation.technology : {};
503
+ const modellingAndValidation = isRecord(processDataSet.modellingAndValidation)
504
+ ? processDataSet.modellingAndValidation
505
+ : {};
506
+ const lciMethod = isRecord(modellingAndValidation.LCIMethodAndAllocation)
507
+ ? modellingAndValidation.LCIMethodAndAllocation
508
+ : {};
509
+ const typeOfDataSet = trimText(lciMethod.typeOfDataSet) || 'missing';
510
+ incrementCount(typeOfDataSetCounts, typeOfDataSet);
511
+ if (typeOfDataSet.toLowerCase().includes('unit process')) {
512
+ unitProcessRows += 1;
513
+ }
514
+ const classificationEntries = extractClassificationEntries(dataSetInformation);
515
+ const primaryDomain = classificationEntries.find((entry) => entry.level === 1)?.text ??
516
+ classificationEntries[classificationEntries.length - 1]?.text ??
517
+ '';
518
+ const leafDomain = classificationEntries[classificationEntries.length - 1]?.text ?? '';
519
+ if (primaryDomain) {
520
+ incrementCount(domainPrimaryCounts, primaryDomain, {
521
+ sample_path: classificationEntries.map((entry) => entry.text).join(' > '),
522
+ });
523
+ }
524
+ else {
525
+ rowsMissingClassification += 1;
526
+ }
527
+ if (leafDomain) {
528
+ incrementCount(domainLeafCounts, leafDomain, {
529
+ sample_path: classificationEntries.map((entry) => entry.text).join(' > '),
530
+ });
531
+ }
532
+ const craft = extractCraftCandidate(dataSetInformation, technology);
533
+ if (craft.signature) {
534
+ incrementCount(craftCounts, craft.signature, {
535
+ label: craft.label,
536
+ source_kind: craft.source_kind,
537
+ });
538
+ incrementCount(craftSourceCounts, craft.source_kind);
539
+ }
540
+ else {
541
+ rowsMissingCraft += 1;
542
+ }
543
+ const product = extractReferenceProduct(processDataSet, dataSetInformation);
544
+ if (product.key) {
545
+ incrementCount(productCounts, product.key, {
546
+ label: product.label,
547
+ stable_flow_id: product.stable_flow_id,
548
+ stable_flow_version: product.stable_flow_version,
549
+ source_kind: product.source_kind,
550
+ });
551
+ incrementCount(productSourceCounts, product.source_kind);
552
+ }
553
+ else {
554
+ rowsMissingProduct += 1;
555
+ }
556
+ if (product.missing_reference_exchange) {
557
+ rowsMissingReferenceExchange += 1;
558
+ }
559
+ }
560
+ const domainPrimarySummary = toSortedArray(domainPrimaryCounts).map((item) => ({
561
+ domain: item.key,
562
+ count: item.count,
563
+ sample_path: item.sample_path,
564
+ }));
565
+ const domainLeafSummary = toSortedArray(domainLeafCounts).map((item) => ({
566
+ domain: item.key,
567
+ count: item.count,
568
+ sample_path: item.sample_path,
569
+ }));
570
+ const craftSummary = toSortedArray(craftCounts).map((item) => ({
571
+ craft_signature: item.key,
572
+ label: item.label,
573
+ count: item.count,
574
+ source_kind: item.source_kind,
575
+ }));
576
+ const productSummary = toSortedArray(productCounts).map((item) => ({
577
+ product_key: item.key,
578
+ label: item.label,
579
+ count: item.count,
580
+ stable_flow_id: item.stable_flow_id,
581
+ stable_flow_version: item.stable_flow_version,
582
+ source_kind: item.source_kind,
583
+ }));
584
+ const typeOfDataSetSummary = toSortedArray(typeOfDataSetCounts).map((item) => ({
585
+ type_of_dataset: item.key,
586
+ count: item.count,
587
+ }));
588
+ return {
589
+ summary: {
590
+ schema_version: 1,
591
+ generated_at_utc: generatedAtUtc,
592
+ scope: options.scope,
593
+ state_codes: options.stateCodes,
594
+ user_id: metadata.userId,
595
+ masked_user_email: metadata.maskedUserEmail,
596
+ total_process_rows: rows.length,
597
+ total_rows_reported_by_remote: metadata.totalRowsReportedByRemote,
598
+ rows_by_state_code: Object.fromEntries([...stateCodeCounts.entries()].map(([key, value]) => [key, value.count])),
599
+ distinct_visible_owner_user_ids: ownerCounts.size,
600
+ domain_count_primary: domainPrimaryCounts.size,
601
+ domain_count_leaf: domainLeafCounts.size,
602
+ craft_count: craftCounts.size,
603
+ unit_process_rows: unitProcessRows,
604
+ unit_process_share: rows.length === 0 ? 0 : unitProcessRows / rows.length,
605
+ product_count: productCounts.size,
606
+ products_with_flow_id: productSummary.filter((item) => item.stable_flow_id).length,
607
+ products_without_flow_id: productSummary.filter((item) => !item.stable_flow_id).length,
608
+ rows_missing_classification: rowsMissingClassification,
609
+ rows_missing_craft: rowsMissingCraft,
610
+ rows_missing_product: rowsMissingProduct,
611
+ rows_missing_reference_exchange: rowsMissingReferenceExchange,
612
+ metric_definitions: {
613
+ domain_count_primary: 'Unique level-1 classification text; fallback to the deepest available classification when level 1 is missing.',
614
+ craft_count: 'Unique normalized craft or route signatures derived from treatmentStandardsRoutes, or fallback technology/base-name text.',
615
+ unit_process_rows: 'Rows whose typeOfDataSet contains the phrase "Unit process".',
616
+ product_count: 'Unique reference products, keyed by reference flow ID when present and by normalized label fallback otherwise.',
617
+ },
618
+ },
619
+ domainPrimarySummary,
620
+ domainLeafSummary,
621
+ craftSummary,
622
+ productSummary,
623
+ typeOfDataSetSummary,
624
+ craftSourceSummary: toSortedArray(craftSourceCounts).map((item) => ({
625
+ source_kind: item.key,
626
+ count: item.count,
627
+ })),
628
+ productSourceSummary: toSortedArray(productSourceCounts).map((item) => ({
629
+ source_kind: item.key,
630
+ count: item.count,
631
+ })),
632
+ };
633
+ }
634
+ function escapePipe(value) {
635
+ return String(value ?? '').replace(/\|/gu, '\\|');
636
+ }
637
+ function renderMarkdownReport(statistics, reportLang) {
638
+ const isZh = reportLang === 'zh';
639
+ const summary = statistics.summary;
640
+ const lines = [];
641
+ lines.push(isZh ? '# Process 覆盖统计' : '# Process Scope Statistics');
642
+ lines.push('');
643
+ lines.push(isZh ? `统计时间:${summary.generated_at_utc}` : `Generated at: ${summary.generated_at_utc}`);
644
+ lines.push(isZh
645
+ ? `范围:\`scope=${summary.scope}\`, \`state_codes=${summary.state_codes.join(',')}\``
646
+ : `Scope: \`scope=${summary.scope}\`, \`state_codes=${summary.state_codes.join(',')}\``);
647
+ lines.push('');
648
+ lines.push(isZh ? '## 核心结果' : '## Headline Metrics');
649
+ lines.push('');
650
+ lines.push(`- ${isZh ? '总 process 行数' : 'Total process rows'}: \`${summary.total_process_rows}\``);
651
+ lines.push(`- ${isZh ? '可见 owner 数' : 'Distinct visible owners'}: \`${summary.distinct_visible_owner_user_ids}\``);
652
+ lines.push(`- ${isZh ? '领域数量(一级分类口径)' : 'Domain count (primary definition)'}: \`${summary.domain_count_primary}\``);
653
+ lines.push(`- ${isZh ? '领域数量(叶子分类口径)' : 'Domain count (leaf classification)'}: \`${summary.domain_count_leaf}\``);
654
+ lines.push(`- ${isZh ? '工艺/路线数量' : 'Craft / route count'}: \`${summary.craft_count}\``);
655
+ lines.push(`- ${isZh ? '单元过程行数' : 'Unit-process rows'}: \`${summary.unit_process_rows}\``);
656
+ lines.push(`- ${isZh ? '产品数量' : 'Product count'}: \`${summary.product_count}\``);
657
+ lines.push('');
658
+ lines.push(isZh ? '## 状态分布' : '## State-code Distribution');
659
+ lines.push('');
660
+ Object.entries(summary.rows_by_state_code).forEach(([stateCode, count]) => {
661
+ lines.push(`- \`state_code=${stateCode}\`: \`${count}\``);
662
+ });
663
+ lines.push('');
664
+ lines.push(isZh ? '## 统计口径' : '## Metric Definitions');
665
+ lines.push('');
666
+ lines.push(`- ${isZh ? '领域' : 'Domain'}: ${isZh
667
+ ? '优先使用 classification level=1;若缺失,则回退到最深层分类。'
668
+ : 'Prefer classification level 1; fall back to the deepest available classification when level 1 is missing.'}`);
669
+ lines.push(`- ${isZh ? '工艺' : 'Craft'}: ${isZh
670
+ ? '优先使用 `treatmentStandardsRoutes`;为空时回退到技术说明首句,再回退到 `baseName`。'
671
+ : 'Prefer `treatmentStandardsRoutes`; fall back to the first clause of technology text, then to `baseName`.'}`);
672
+ lines.push(`- ${isZh ? '单元过程' : 'Unit process'}: ${isZh
673
+ ? '以 `typeOfDataSet` 是否包含 `Unit process` 为准。'
674
+ : 'Rows are counted when `typeOfDataSet` contains `Unit process`.'}`);
675
+ lines.push(`- ${isZh ? '产品' : 'Product'}: ${isZh
676
+ ? '优先按 reference flow UUID 聚合;缺失时回退到 reference flow 短描述,再回退到 process base name。'
677
+ : 'Prefer reference-flow UUIDs; fall back to reference-flow short descriptions, then to process base names.'}`);
678
+ lines.push('');
679
+ lines.push(isZh ? '## Top 领域' : '## Top Domains');
680
+ lines.push('');
681
+ lines.push(`| ${isZh ? '领域' : 'Domain'} | ${isZh ? '数量' : 'Count'} |`);
682
+ lines.push('| --- | --- |');
683
+ statistics.domainPrimarySummary.slice(0, 15).forEach((item) => {
684
+ lines.push(`| ${escapePipe(item.domain)} | ${item.count} |`);
685
+ });
686
+ lines.push('');
687
+ lines.push(isZh ? '## Top 工艺/路线' : '## Top Crafts / Routes');
688
+ lines.push('');
689
+ lines.push(`| ${isZh ? '工艺/路线' : 'Craft / Route'} | ${isZh ? '数量' : 'Count'} | ${isZh ? '来源' : 'Source'} |`);
690
+ lines.push('| --- | --- | --- |');
691
+ statistics.craftSummary.slice(0, 15).forEach((item) => {
692
+ lines.push(`| ${escapePipe(item.label)} | ${item.count} | ${item.source_kind} |`);
693
+ });
694
+ lines.push('');
695
+ lines.push(isZh ? '## Top 产品' : '## Top Products');
696
+ lines.push('');
697
+ lines.push(`| ${isZh ? '产品' : 'Product'} | ${isZh ? '数量' : 'Count'} | ${isZh ? '标识来源' : 'Key source'} |`);
698
+ lines.push('| --- | --- | --- |');
699
+ statistics.productSummary.slice(0, 15).forEach((item) => {
700
+ lines.push(`| ${escapePipe(item.label)} | ${item.count} | ${item.source_kind} |`);
701
+ });
702
+ lines.push('');
703
+ return `${lines.join('\n')}\n`;
704
+ }
705
+ function readSnapshotRows(filePath) {
706
+ return readJsonLinesArtifact(filePath)
707
+ .map((row) => normalizeSnapshotRow(row))
708
+ .filter((row) => row !== null);
709
+ }
710
+ function readSnapshotManifest(filePath) {
711
+ const value = readJsonArtifact(filePath);
712
+ return isRecord(value) ? value : null;
713
+ }
714
+ export async function runProcessScopeStatistics(options) {
715
+ const outDir = trimText(options.outDir);
716
+ if (!outDir) {
717
+ throw new CliError('Missing required --out-dir value.', {
718
+ code: 'PROCESS_SCOPE_OUT_DIR_REQUIRED',
719
+ exitCode: 2,
720
+ });
721
+ }
722
+ const scope = ensureScope(options.scope);
723
+ const stateCodes = normalizeStateCodes(options.stateCodes);
724
+ const pageSize = toPositiveInteger(options.pageSize ?? DEFAULT_PAGE_SIZE, '--page-size', 'PROCESS_SCOPE_PAGE_SIZE_INVALID');
725
+ const timeoutMs = toPositiveInteger(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, '--timeout-ms', 'PROCESS_SCOPE_TIMEOUT_INVALID');
726
+ const maxRetries = toPositiveInteger(options.maxRetries ?? DEFAULT_MAX_RETRIES, 'process scope statistics retry count', 'PROCESS_SCOPE_MAX_RETRIES_INVALID');
727
+ const resolvedOutDir = path.resolve(outDir);
728
+ const snapshotRowsPath = path.join(resolvedOutDir, 'inputs', 'processes.snapshot.rows.jsonl');
729
+ const snapshotManifestPath = path.join(resolvedOutDir, 'inputs', 'processes.snapshot.manifest.json');
730
+ const generatedAtUtc = nowIso(options.now);
731
+ let rows;
732
+ let metadata;
733
+ if (options.reuseSnapshot) {
734
+ rows = readSnapshotRows(snapshotRowsPath);
735
+ const manifest = readSnapshotManifest(snapshotManifestPath);
736
+ metadata = {
737
+ userId: normalizeOptionalToken(manifest?.user_id),
738
+ maskedUserEmail: normalizeOptionalToken(manifest?.masked_user_email),
739
+ totalRowsReportedByRemote: typeof manifest?.total_rows === 'number' ? manifest.total_rows : rows.length,
740
+ };
741
+ }
742
+ else {
743
+ const env = options.env ?? process.env;
744
+ const fetchImpl = options.fetchImpl ?? fetch;
745
+ const restRuntime = requireSupabaseRestRuntime(env);
746
+ const userApiKeyCredentials = requireUserApiKeyCredentials(restRuntime.userApiKey);
747
+ const userId = scope === 'current-user'
748
+ ? await resolveCurrentUserId({
749
+ env,
750
+ fetchImpl,
751
+ timeoutMs,
752
+ now: options.now ?? new Date(),
753
+ maxRetries,
754
+ })
755
+ : null;
756
+ const snapshot = await fetchProcessRows({
757
+ env,
758
+ fetchImpl,
759
+ timeoutMs,
760
+ maxRetries,
761
+ scope,
762
+ stateCodes,
763
+ pageSize,
764
+ userId,
765
+ });
766
+ rows = snapshot.rows;
767
+ metadata = {
768
+ userId,
769
+ maskedUserEmail: redactEmail(userApiKeyCredentials.email),
770
+ totalRowsReportedByRemote: snapshot.total ?? rows.length,
771
+ };
772
+ writeJsonLinesArtifact(snapshotRowsPath, rows);
773
+ writeJsonArtifact(snapshotManifestPath, {
774
+ schema_version: 1,
775
+ generated_at_utc: generatedAtUtc,
776
+ masked_user_email: metadata.maskedUserEmail,
777
+ user_id: metadata.userId,
778
+ scope,
779
+ state_codes: stateCodes,
780
+ page_size: pageSize,
781
+ total_rows: metadata.totalRowsReportedByRemote,
782
+ });
783
+ }
784
+ const statistics = calculateStatistics(rows, {
785
+ scope,
786
+ stateCodes,
787
+ }, metadata, generatedAtUtc);
788
+ const summaryPath = path.join(resolvedOutDir, 'outputs', 'process-scope-summary.json');
789
+ const domainPath = path.join(resolvedOutDir, 'outputs', 'domain-summary.json');
790
+ const craftPath = path.join(resolvedOutDir, 'outputs', 'craft-summary.json');
791
+ const productPath = path.join(resolvedOutDir, 'outputs', 'product-summary.json');
792
+ const datasetTypePath = path.join(resolvedOutDir, 'outputs', 'type-of-dataset-summary.json');
793
+ const reportPath = path.join(resolvedOutDir, 'reports', 'process-scope-statistics.md');
794
+ const reportZhPath = path.join(resolvedOutDir, 'reports', 'process-scope-statistics.zh-CN.md');
795
+ writeJsonArtifact(summaryPath, statistics.summary);
796
+ writeJsonArtifact(domainPath, {
797
+ primary: statistics.domainPrimarySummary,
798
+ leaf: statistics.domainLeafSummary,
799
+ });
800
+ writeJsonArtifact(craftPath, {
801
+ craft_summary: statistics.craftSummary,
802
+ source_summary: statistics.craftSourceSummary,
803
+ });
804
+ writeJsonArtifact(productPath, {
805
+ product_summary: statistics.productSummary,
806
+ source_summary: statistics.productSourceSummary,
807
+ });
808
+ writeJsonArtifact(datasetTypePath, statistics.typeOfDataSetSummary);
809
+ writeTextArtifact(reportPath, renderMarkdownReport(statistics, 'en'));
810
+ writeTextArtifact(reportZhPath, renderMarkdownReport(statistics, 'zh'));
811
+ return {
812
+ schema_version: 1,
813
+ generated_at_utc: generatedAtUtc,
814
+ status: 'completed_process_scope_statistics',
815
+ out_dir: resolvedOutDir,
816
+ scope,
817
+ state_codes: stateCodes,
818
+ total_process_rows: statistics.summary.total_process_rows,
819
+ domain_count_primary: statistics.summary.domain_count_primary,
820
+ domain_count_leaf: statistics.summary.domain_count_leaf,
821
+ craft_count: statistics.summary.craft_count,
822
+ unit_process_rows: statistics.summary.unit_process_rows,
823
+ product_count: statistics.summary.product_count,
824
+ files: {
825
+ snapshot_manifest: snapshotManifestPath,
826
+ snapshot_rows: snapshotRowsPath,
827
+ process_scope_summary: summaryPath,
828
+ domain_summary: domainPath,
829
+ craft_summary: craftPath,
830
+ product_summary: productPath,
831
+ type_of_dataset_summary: datasetTypePath,
832
+ report: reportPath,
833
+ report_zh: reportZhPath,
834
+ },
835
+ };
836
+ }
837
+ export const __testInternals = {
838
+ calculateStatistics,
839
+ ensureScope,
840
+ escapePipe,
841
+ extractClassificationEntries,
842
+ extractCraftCandidate,
843
+ extractReferenceProduct,
844
+ fetchJsonWithRetry,
845
+ fetchProcessRows,
846
+ getAnyLangText,
847
+ getLangList,
848
+ getLangText,
849
+ normalizePayload,
850
+ normalizeSignature,
851
+ normalizeSnapshotRow,
852
+ normalizeStateCodes,
853
+ parseJsonResponse,
854
+ readSnapshotManifest,
855
+ resolveCurrentUserId,
856
+ renderMarkdownReport,
857
+ toPositiveInteger,
858
+ };
859
+ //# sourceMappingURL=process-scope-statistics.js.map