@tiangong-lca/cli 0.0.10 → 0.0.12

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 (56) hide show
  1. package/README.md +18 -4
  2. package/assets/tidas-schemas/tidas_contacts.json +312 -0
  3. package/assets/tidas-schemas/tidas_contacts_category.json +126 -0
  4. package/assets/tidas-schemas/tidas_data_types.json +359 -0
  5. package/assets/tidas-schemas/tidas_flowproperties.json +303 -0
  6. package/assets/tidas-schemas/tidas_flowproperties_category.json +61 -0
  7. package/assets/tidas-schemas/tidas_flows.json +809 -0
  8. package/assets/tidas-schemas/tidas_flows_elementary_category.json +720 -0
  9. package/assets/tidas-schemas/tidas_flows_product_category.json +59623 -0
  10. package/assets/tidas-schemas/tidas_lciamethods.json +1326 -0
  11. package/assets/tidas-schemas/tidas_lciamethods_category.json +685 -0
  12. package/assets/tidas-schemas/tidas_lifecyclemodels.json +1374 -0
  13. package/assets/tidas-schemas/tidas_locations_category.json +2593 -0
  14. package/assets/tidas-schemas/tidas_processes.json +1646 -0
  15. package/assets/tidas-schemas/tidas_processes_category.json +10795 -0
  16. package/assets/tidas-schemas/tidas_sources.json +228 -0
  17. package/assets/tidas-schemas/tidas_sources_category.json +100 -0
  18. package/assets/tidas-schemas/tidas_unitgroups.json +338 -0
  19. package/assets/tidas-schemas/tidas_unitgroups_category.json +61 -0
  20. package/dist/src/cli.js +948 -8
  21. package/dist/src/cli.js.map +1 -1
  22. package/dist/src/lib/dataset-classification.js +947 -0
  23. package/dist/src/lib/dataset-classification.js.map +1 -0
  24. package/dist/src/lib/dataset-command.js +11 -0
  25. package/dist/src/lib/dataset-command.js.map +1 -1
  26. package/dist/src/lib/dataset-contract.js +2 -0
  27. package/dist/src/lib/dataset-contract.js.map +1 -1
  28. package/dist/src/lib/dataset-curation-queue.js +768 -0
  29. package/dist/src/lib/dataset-curation-queue.js.map +1 -0
  30. package/dist/src/lib/dataset-import-lca.js +18 -0
  31. package/dist/src/lib/dataset-import-lca.js.map +1 -1
  32. package/dist/src/lib/dataset-local.js +94 -1
  33. package/dist/src/lib/dataset-local.js.map +1 -1
  34. package/dist/src/lib/dataset-maintenance-clear-account.js +506 -0
  35. package/dist/src/lib/dataset-maintenance-clear-account.js.map +1 -0
  36. package/dist/src/lib/dataset-patch.js +797 -0
  37. package/dist/src/lib/dataset-patch.js.map +1 -0
  38. package/dist/src/lib/dataset-remote-verify.js +188 -3
  39. package/dist/src/lib/dataset-remote-verify.js.map +1 -1
  40. package/dist/src/lib/dataset-save-draft-run.js +725 -0
  41. package/dist/src/lib/dataset-save-draft-run.js.map +1 -0
  42. package/dist/src/lib/dataset-validate.js +32 -2
  43. package/dist/src/lib/dataset-validate.js.map +1 -1
  44. package/dist/src/lib/flow-publish-version.js +7 -2
  45. package/dist/src/lib/flow-publish-version.js.map +1 -1
  46. package/dist/src/lib/flow-qa.js +8 -0
  47. package/dist/src/lib/flow-qa.js.map +1 -1
  48. package/dist/src/lib/identity-preflight.js +895 -117
  49. package/dist/src/lib/identity-preflight.js.map +1 -1
  50. package/dist/src/lib/lifecyclemodel-qa.js +159 -34
  51. package/dist/src/lib/lifecyclemodel-qa.js.map +1 -1
  52. package/dist/src/lib/process-required-fields.js +62 -12
  53. package/dist/src/lib/process-required-fields.js.map +1 -1
  54. package/dist/src/lib/supabase-client.js +19 -8
  55. package/dist/src/lib/supabase-client.js.map +1 -1
  56. package/package.json +2 -1
@@ -0,0 +1,768 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { CliError } from './errors.js';
5
+ import { firstNonEmpty, isRecord, materializeDatasetRows, } from './dataset-local.js';
6
+ const DEFAULT_VERSION = 'unversioned';
7
+ export async function runDatasetCurationQueueBuild(options) {
8
+ const outDir = requirePath(options.outDir, '--out-dir');
9
+ const processesPath = requireExistingPath(options.processesPath, '--processes');
10
+ const flowsPath = options.flowsPath ? requireExistingPath(options.flowsPath, '--flows') : null;
11
+ const supportPaths = (options.supportPaths ?? []).map((inputPath) => requireExistingPath(inputPath, '--support'));
12
+ const externalFlowRefPaths = (options.externalFlowRefPaths ?? []).map((inputPath) => requireExistingPath(inputPath, '--external-flow-ref'));
13
+ const processLimit = normalizeProcessLimit(options.processLimit);
14
+ const excludedProcessIds = new Set((options.excludeProcessIds ?? []).map((id) => id.trim()));
15
+ const supportRows = supportPaths.flatMap((inputPath) => readQueueRows(inputPath, 'support'));
16
+ const flowRows = flowsPath ? readQueueRows(flowsPath, 'flow') : [];
17
+ const processRows = readQueueRows(processesPath, 'process')
18
+ .filter((row) => !excludedProcessIds.has(row.id))
19
+ .slice(0, processLimit ?? undefined);
20
+ const externalFlowRefs = externalFlowRefPaths.flatMap((inputPath) => readExternalFlowRefs(inputPath));
21
+ mkdirSync(outDir, { recursive: true });
22
+ const localFlowTasks = new Map();
23
+ const flowRowsById = new Map();
24
+ for (const row of flowRows) {
25
+ const taskId = taskIdFor(row.entityType, row.id, row.version);
26
+ localFlowTasks.set(row.id, taskId);
27
+ flowRowsById.set(row.id, row);
28
+ }
29
+ const externalFlowIds = new Set(externalFlowRefs.map((ref) => ref.id));
30
+ const blockers = [];
31
+ const supportTasks = supportRows.map((row) => buildTask(outDir, row, []));
32
+ const flowTasks = flowRows.map((row) => buildTask(outDir, row, []));
33
+ const processTasks = processRows.map((row) => {
34
+ const refs = extractProcessFlowRefs(row.payload);
35
+ const deferredRefs = extractDeferredProcessFlowRefs(row.payload);
36
+ const deferredRefKeys = deferredProcessFlowRefKeys(deferredRefs);
37
+ const dependsOn = new Set();
38
+ const missingRefs = [];
39
+ for (const ref of refs) {
40
+ const taskId = localFlowTasks.get(ref.id);
41
+ if (taskId) {
42
+ dependsOn.add(taskId);
43
+ }
44
+ else if (!externalFlowIds.has(ref.id) && !deferredRefKeys.has(flowRefKey(ref))) {
45
+ missingRefs.push(ref);
46
+ }
47
+ }
48
+ if (missingRefs.length > 0) {
49
+ blockers.push({
50
+ schema_version: 1,
51
+ code: 'process_flow_reference_unresolved',
52
+ severity: 'blocker',
53
+ entity_type: 'process',
54
+ entity_id: row.id,
55
+ version: row.version,
56
+ message: 'Process references flows that are neither present in local flow rows nor declared as external flow refs.',
57
+ details: { missing_flow_refs: missingRefs },
58
+ });
59
+ }
60
+ return buildTask(outDir, row, [...dependsOn].sort());
61
+ });
62
+ const tasks = [...supportTasks, ...flowTasks, ...processTasks];
63
+ const taskRows = [
64
+ ...supportRows.map((row, index) => ({ row, task: supportTasks[index] })),
65
+ ...flowRows.map((row, index) => ({ row, task: flowTasks[index] })),
66
+ ...processRows.map((row, index) => ({ row, task: processTasks[index] })),
67
+ ];
68
+ for (const { row, task } of taskRows) {
69
+ writeEntityArtifacts({
70
+ row,
71
+ task,
72
+ flowRefs: row.entityType === 'process' ? extractProcessFlowRefs(row.payload) : [],
73
+ deferredFlowRefs: row.entityType === 'process' ? extractDeferredProcessFlowRefs(row.payload) : [],
74
+ externalFlowIds,
75
+ flowRowsById,
76
+ });
77
+ }
78
+ const outputsDir = path.join(outDir, 'outputs');
79
+ mkdirSync(outputsDir, { recursive: true });
80
+ const manifestPath = path.join(outputsDir, 'curation-queue-manifest.json');
81
+ const tasksPath = path.join(outputsDir, 'curation-queue-tasks.jsonl');
82
+ const locksPath = path.join(outputsDir, 'curation-queue-locks.json');
83
+ const blockersPath = path.join(outputsDir, 'curation-queue-blockers.jsonl');
84
+ const inputHashes = buildInputHashes([
85
+ processesPath,
86
+ ...(flowsPath ? [flowsPath] : []),
87
+ ...supportPaths,
88
+ ...externalFlowRefPaths,
89
+ ]);
90
+ const locks = {
91
+ schema_version: 1,
92
+ generated_at_utc: new Date().toISOString(),
93
+ locks: tasks.map((task) => ({
94
+ lock_key: task.lock_key,
95
+ task_id: task.task_id,
96
+ entity_type: task.entity_type,
97
+ entity_id: task.entity_id,
98
+ version: task.version,
99
+ status: 'available',
100
+ })),
101
+ };
102
+ const report = {
103
+ schema_version: 1,
104
+ generated_at_utc: new Date().toISOString(),
105
+ status: blockers.length > 0 ? 'blocked' : 'ready',
106
+ out_dir: path.resolve(outDir),
107
+ inputs: {
108
+ processes: path.resolve(processesPath),
109
+ flows: flowsPath ? path.resolve(flowsPath) : null,
110
+ support: supportPaths.map((inputPath) => path.resolve(inputPath)),
111
+ external_flow_refs: externalFlowRefPaths.map((inputPath) => path.resolve(inputPath)),
112
+ },
113
+ counts: {
114
+ support_rows: supportRows.length,
115
+ flow_rows: flowRows.length,
116
+ process_rows: processRows.length,
117
+ external_flow_refs: externalFlowRefs.length,
118
+ tasks: tasks.length,
119
+ blockers: blockers.length,
120
+ },
121
+ hashes: {
122
+ inputs: inputHashes,
123
+ task_order: sha256(tasks.map((task) => task.task_id).join('\n')),
124
+ },
125
+ files: {
126
+ manifest: manifestPath,
127
+ tasks: tasksPath,
128
+ locks: locksPath,
129
+ blockers: blockersPath,
130
+ },
131
+ tasks,
132
+ blockers,
133
+ };
134
+ writeJson(manifestPath, report);
135
+ writeText(tasksPath, jsonLines(tasks));
136
+ writeJson(locksPath, locks);
137
+ writeText(blockersPath, jsonLines(blockers));
138
+ return report;
139
+ }
140
+ export async function runDatasetCurationQueueNext(options) {
141
+ const queue = readQueueRuntime(options.queueDir);
142
+ const scope = normalizeQueueScope(options);
143
+ const taskStates = buildTaskStates(queue.tasks).filter((state) => taskMatchesScope(state, scope));
144
+ const scopedTasks = queue.tasks.filter((task) => taskMatchesScope(task, scope));
145
+ const stateByTaskId = new Map(taskStates.map((state) => [state.task_id, state]));
146
+ const runnable = scopedTasks.filter((task) => stateByTaskId.get(task.task_id)?.status === 'pending');
147
+ const counts = countTaskStates(taskStates);
148
+ const status = queue.blockers.length > 0 ||
149
+ (taskStates.length > 0 && runnable.length === 0 && counts.complete < taskStates.length)
150
+ ? 'blocked'
151
+ : counts.complete === taskStates.length
152
+ ? 'complete'
153
+ : 'ready';
154
+ const nextTask = status === 'ready' && runnable[0] ? withQueueAction(runnable[0]) : null;
155
+ return {
156
+ schema_version: 1,
157
+ generated_at_utc: new Date().toISOString(),
158
+ status,
159
+ queue_dir: queue.queueDir,
160
+ scope,
161
+ counts: {
162
+ ...counts,
163
+ runnable: runnable.length,
164
+ },
165
+ next_task: nextTask,
166
+ task_states: taskStates,
167
+ blockers: queue.blockers,
168
+ };
169
+ }
170
+ export async function runDatasetCurationQueueVerify(options) {
171
+ const queue = readQueueRuntime(options.queueDir);
172
+ const scope = normalizeQueueScope(options);
173
+ const taskStates = buildTaskStates(queue.tasks).filter((state) => taskMatchesScope(state, scope));
174
+ const counts = countTaskStates(taskStates);
175
+ return {
176
+ schema_version: 1,
177
+ generated_at_utc: new Date().toISOString(),
178
+ status: queue.blockers.length === 0 && counts.complete === taskStates.length ? 'passed' : 'blocked',
179
+ queue_dir: queue.queueDir,
180
+ scope,
181
+ counts,
182
+ task_states: taskStates,
183
+ blockers: queue.blockers,
184
+ };
185
+ }
186
+ function readQueueRuntime(queueDirInput) {
187
+ const queueDir = requireExistingPath(queueDirInput, '--queue-dir');
188
+ const outputsDir = path.join(queueDir, 'outputs');
189
+ const tasksPath = path.join(outputsDir, 'curation-queue-tasks.jsonl');
190
+ const blockersPath = path.join(outputsDir, 'curation-queue-blockers.jsonl');
191
+ if (!existsSync(tasksPath)) {
192
+ throw new CliError(`curation queue tasks file does not exist: ${tasksPath}`, {
193
+ code: 'CURATION_QUEUE_TASKS_NOT_FOUND',
194
+ exitCode: 2,
195
+ });
196
+ }
197
+ if (!existsSync(blockersPath)) {
198
+ throw new CliError(`curation queue blockers file does not exist: ${blockersPath}`, {
199
+ code: 'CURATION_QUEUE_BLOCKERS_NOT_FOUND',
200
+ exitCode: 2,
201
+ });
202
+ }
203
+ return {
204
+ queueDir: path.resolve(queueDir),
205
+ tasks: readJsonlFile(tasksPath).map((value, index) => parseQueueTask(value, tasksPath, index)),
206
+ blockers: readJsonlFile(blockersPath).map((value, index) => parseQueueBlocker(value, blockersPath, index)),
207
+ };
208
+ }
209
+ function normalizeQueueScope(options) {
210
+ return {
211
+ entity_type: options.entityType ?? null,
212
+ task_id: options.taskId?.trim() || null,
213
+ };
214
+ }
215
+ function buildTaskStates(tasks) {
216
+ const completeTaskIds = new Set(tasks
217
+ .filter((task) => isCompleteCheckpointStatus(readCheckpointStatus(task.checkpoint_file)))
218
+ .map((task) => task.task_id));
219
+ return tasks.map((task) => {
220
+ const checkpointStatus = readCheckpointStatus(task.checkpoint_file);
221
+ const incompleteDependencies = task.depends_on.filter((taskId) => !completeTaskIds.has(taskId));
222
+ const status = taskRuntimeStatus(checkpointStatus, incompleteDependencies);
223
+ return {
224
+ task_id: task.task_id,
225
+ entity_type: task.entity_type,
226
+ entity_id: task.entity_id,
227
+ version: task.version,
228
+ status,
229
+ checkpoint_status: checkpointStatus,
230
+ checkpoint_file: task.checkpoint_file,
231
+ depends_on: task.depends_on,
232
+ incomplete_dependencies: incompleteDependencies,
233
+ reason: status === 'waiting_dependencies'
234
+ ? 'dependency checkpoints are not complete'
235
+ : status === 'blocked'
236
+ ? 'checkpoint status is blocked or failed'
237
+ : null,
238
+ };
239
+ });
240
+ }
241
+ function taskRuntimeStatus(checkpointStatus, incompleteDependencies) {
242
+ if (isCompleteCheckpointStatus(checkpointStatus)) {
243
+ return 'complete';
244
+ }
245
+ if (checkpointStatus === 'blocked' || checkpointStatus === 'failed') {
246
+ return 'blocked';
247
+ }
248
+ if (incompleteDependencies.length > 0) {
249
+ return 'waiting_dependencies';
250
+ }
251
+ return 'pending';
252
+ }
253
+ function isCompleteCheckpointStatus(status) {
254
+ return (status === 'passed' || status === 'complete' || status === 'completed' || status === 'waived');
255
+ }
256
+ function readCheckpointStatus(checkpointFile) {
257
+ const resolvedPath = path.resolve(checkpointFile);
258
+ if (!existsSync(resolvedPath)) {
259
+ return null;
260
+ }
261
+ try {
262
+ const value = JSON.parse(readFileSync(resolvedPath, 'utf8'));
263
+ if (!isRecord(value)) {
264
+ return 'blocked';
265
+ }
266
+ const status = firstNonEmpty(value.status, value.state);
267
+ return status ? status.toLowerCase() : 'blocked';
268
+ }
269
+ catch {
270
+ return 'blocked';
271
+ }
272
+ }
273
+ function taskMatchesScope(task, scope) {
274
+ if (scope.entity_type && task.entity_type !== scope.entity_type) {
275
+ return false;
276
+ }
277
+ if (scope.task_id && task.task_id !== scope.task_id) {
278
+ return false;
279
+ }
280
+ return true;
281
+ }
282
+ function countTaskStates(taskStates) {
283
+ return {
284
+ total: taskStates.length,
285
+ complete: taskStates.filter((state) => state.status === 'complete').length,
286
+ pending: taskStates.filter((state) => state.status === 'pending').length,
287
+ waiting_dependencies: taskStates.filter((state) => state.status === 'waiting_dependencies')
288
+ .length,
289
+ blocked: taskStates.filter((state) => state.status === 'blocked').length,
290
+ };
291
+ }
292
+ function withQueueAction(task) {
293
+ return {
294
+ ...task,
295
+ action: {
296
+ kind: 'entity_curation_task',
297
+ input_artifact: task.input_rows_file,
298
+ output_artifacts: [task.checkpoint_file],
299
+ },
300
+ };
301
+ }
302
+ function readJsonlFile(inputPath) {
303
+ const text = readFileSync(inputPath, 'utf8').trim();
304
+ if (!text) {
305
+ return [];
306
+ }
307
+ return text.split(/\r?\n/u).map((line, index) => {
308
+ try {
309
+ return JSON.parse(line);
310
+ }
311
+ catch (error) {
312
+ throw new CliError(`Invalid JSONL in ${inputPath} at line ${index + 1}: ${String(error)}`, {
313
+ code: 'CURATION_QUEUE_JSONL_INVALID',
314
+ exitCode: 2,
315
+ });
316
+ }
317
+ });
318
+ }
319
+ function parseQueueTask(value, inputPath, index) {
320
+ if (!isRecord(value)) {
321
+ throw new CliError(`Invalid queue task in ${inputPath} at line ${index + 1}.`, {
322
+ code: 'CURATION_QUEUE_TASK_INVALID',
323
+ exitCode: 2,
324
+ });
325
+ }
326
+ const entityType = value.entity_type;
327
+ if (entityType !== 'support' && entityType !== 'flow' && entityType !== 'process') {
328
+ throw new CliError(`Invalid queue task entity_type in ${inputPath} at line ${index + 1}.`, {
329
+ code: 'CURATION_QUEUE_TASK_INVALID',
330
+ exitCode: 2,
331
+ });
332
+ }
333
+ return {
334
+ schema_version: 1,
335
+ entity_type: entityType,
336
+ task_id: requireString(value.task_id, inputPath, index, 'task_id'),
337
+ entity_id: requireString(value.entity_id, inputPath, index, 'entity_id'),
338
+ version: requireString(value.version, inputPath, index, 'version'),
339
+ lock_key: requireString(value.lock_key, inputPath, index, 'lock_key'),
340
+ depends_on: Array.isArray(value.depends_on)
341
+ ? value.depends_on.filter((item) => typeof item === 'string')
342
+ : [],
343
+ input_rows_file: requireString(value.input_rows_file, inputPath, index, 'input_rows_file'),
344
+ work_dir: requireString(value.work_dir, inputPath, index, 'work_dir'),
345
+ checkpoint_file: requireString(value.checkpoint_file, inputPath, index, 'checkpoint_file'),
346
+ run_plan_file: requireString(value.run_plan_file, inputPath, index, 'run_plan_file'),
347
+ closure_file: requireString(value.closure_file, inputPath, index, 'closure_file'),
348
+ };
349
+ }
350
+ function parseQueueBlocker(value, inputPath, index) {
351
+ if (!isRecord(value)) {
352
+ throw new CliError(`Invalid queue blocker in ${inputPath} at line ${index + 1}.`, {
353
+ code: 'CURATION_QUEUE_BLOCKER_INVALID',
354
+ exitCode: 2,
355
+ });
356
+ }
357
+ const entityType = value.entity_type;
358
+ if (entityType !== 'support' && entityType !== 'flow' && entityType !== 'process') {
359
+ throw new CliError(`Invalid queue blocker entity_type in ${inputPath} at line ${index + 1}.`, {
360
+ code: 'CURATION_QUEUE_BLOCKER_INVALID',
361
+ exitCode: 2,
362
+ });
363
+ }
364
+ return {
365
+ schema_version: 1,
366
+ code: requireString(value.code, inputPath, index, 'code'),
367
+ severity: 'blocker',
368
+ entity_type: entityType,
369
+ entity_id: typeof value.entity_id === 'string' ? value.entity_id : null,
370
+ version: typeof value.version === 'string' ? value.version : null,
371
+ message: requireString(value.message, inputPath, index, 'message'),
372
+ details: value.details,
373
+ };
374
+ }
375
+ function requireString(value, inputPath, index, field) {
376
+ if (typeof value === 'string' && value.length > 0) {
377
+ return value;
378
+ }
379
+ throw new CliError(`Invalid or missing ${field} in ${inputPath} at line ${index + 1}.`, {
380
+ code: 'CURATION_QUEUE_TASK_INVALID',
381
+ exitCode: 2,
382
+ });
383
+ }
384
+ function readQueueRows(inputPath, entityType) {
385
+ const rows = materializeDatasetRows(inputPath);
386
+ return rows.map((row) => queueRowFromDatasetRow(inputPath, entityType, row));
387
+ }
388
+ function queueRowFromDatasetRow(inputPath, entityType, row) {
389
+ const tidasIdentity = genericTidasDatasetIdentity(row.payload);
390
+ const id = firstNonEmpty(row.id, row.row.id, row.row.dataset_id, row.row.uuid, tidasIdentity.id);
391
+ if (!id) {
392
+ throw new CliError(`${entityType} row is missing a stable id in ${inputPath} at index ${row.index}.`, {
393
+ code: 'CURATION_QUEUE_ROW_ID_MISSING',
394
+ exitCode: 2,
395
+ });
396
+ }
397
+ return {
398
+ entityType,
399
+ sourcePath: inputPath,
400
+ sourceIndex: row.index,
401
+ row: row.row,
402
+ payload: row.payload,
403
+ id,
404
+ version: firstNonEmpty(row.version, row.row.version, tidasIdentity.version) ?? DEFAULT_VERSION,
405
+ };
406
+ }
407
+ function genericTidasDatasetIdentity(payload) {
408
+ const root = genericTidasDatasetRoot(payload);
409
+ const information = Object.values(root).find((value) => isRecord(value) && isRecord(value.dataSetInformation));
410
+ const dataSetInformation = isRecord(information) && isRecord(information.dataSetInformation)
411
+ ? information.dataSetInformation
412
+ : {};
413
+ const administrativeInformation = isRecord(root.administrativeInformation)
414
+ ? root.administrativeInformation
415
+ : {};
416
+ const publicationAndOwnership = isRecord(administrativeInformation.publicationAndOwnership)
417
+ ? administrativeInformation.publicationAndOwnership
418
+ : {};
419
+ return {
420
+ id: firstNonEmpty(dataSetInformation['common:UUID']),
421
+ version: firstNonEmpty(publicationAndOwnership['common:dataSetVersion']),
422
+ };
423
+ }
424
+ function genericTidasDatasetRoot(payload) {
425
+ const datasetRoots = [];
426
+ for (const [key, value] of Object.entries(payload)) {
427
+ if (key.endsWith('DataSet') && isRecord(value)) {
428
+ datasetRoots.push(value);
429
+ }
430
+ }
431
+ return datasetRoots.length === 1 ? datasetRoots[0] : payload;
432
+ }
433
+ function buildTask(outDir, row, dependsOn) {
434
+ const taskId = taskIdFor(row.entityType, row.id, row.version);
435
+ const workDir = path.join(outDir, 'entities', entityDirPlural(row.entityType), entityDirName(row.id, row.version));
436
+ return {
437
+ schema_version: 1,
438
+ entity_type: row.entityType,
439
+ task_id: taskId,
440
+ entity_id: row.id,
441
+ version: row.version,
442
+ lock_key: taskId,
443
+ depends_on: dependsOn,
444
+ input_rows_file: path.join(workDir, 'input.jsonl'),
445
+ work_dir: workDir,
446
+ checkpoint_file: path.join(workDir, 'checkpoint.json'),
447
+ run_plan_file: path.join(workDir, 'entity-run-plan.json'),
448
+ closure_file: path.join(workDir, 'closure.json'),
449
+ };
450
+ }
451
+ function writeEntityArtifacts(options) {
452
+ mkdirSync(path.join(options.task.work_dir, 'checkpoints'), { recursive: true });
453
+ writeText(options.task.input_rows_file, jsonLines([options.row.row]));
454
+ writeJson(options.task.closure_file, {
455
+ schema_version: 1,
456
+ entity_type: options.row.entityType,
457
+ entity_id: options.row.id,
458
+ version: options.row.version,
459
+ source: {
460
+ file: path.resolve(options.row.sourcePath),
461
+ index: options.row.sourceIndex,
462
+ },
463
+ dependencies: buildDependencyClosure(options),
464
+ });
465
+ writeJson(options.task.run_plan_file, buildRunPlan(options.task));
466
+ }
467
+ function buildDependencyClosure(options) {
468
+ if (options.row.entityType !== 'process') {
469
+ return {
470
+ local_tasks: [],
471
+ external_refs: [],
472
+ deferred_refs: [],
473
+ unresolved_refs: [],
474
+ };
475
+ }
476
+ const deferredRefKeys = deferredProcessFlowRefKeys(options.deferredFlowRefs);
477
+ return {
478
+ local_tasks: options.flowRefs
479
+ .map((ref) => ({ ref, row: options.flowRowsById.get(ref.id) }))
480
+ .filter((item) => Boolean(item.row))
481
+ .map(({ ref, row }) => ({
482
+ entity_type: 'flow',
483
+ entity_id: ref.id,
484
+ version: row.version,
485
+ task_id: taskIdFor('flow', ref.id, row.version),
486
+ ref_path: ref.path,
487
+ })),
488
+ external_refs: options.flowRefs
489
+ .filter((ref) => !options.flowRowsById.has(ref.id) && options.externalFlowIds.has(ref.id))
490
+ .map((ref) => ({
491
+ entity_type: 'flow',
492
+ entity_id: ref.id,
493
+ version: ref.version,
494
+ ref_path: ref.path,
495
+ })),
496
+ deferred_refs: options.flowRefs
497
+ .filter((ref) => !options.flowRowsById.has(ref.id) &&
498
+ !options.externalFlowIds.has(ref.id) &&
499
+ deferredRefKeys.has(flowRefKey(ref)))
500
+ .map((ref) => {
501
+ const deferredRef = deferredRefKeys.get(flowRefKey(ref));
502
+ return {
503
+ entity_type: 'flow',
504
+ entity_id: ref.id,
505
+ version: ref.version,
506
+ ref_path: ref.path,
507
+ action_item_code: deferredRef.actionItemCode,
508
+ reason: deferredRef.reason ?? null,
509
+ };
510
+ }),
511
+ unresolved_refs: options.flowRefs
512
+ .filter((ref) => !options.flowRowsById.has(ref.id) &&
513
+ !options.externalFlowIds.has(ref.id) &&
514
+ !deferredRefKeys.has(flowRefKey(ref)))
515
+ .map((ref) => ({
516
+ entity_type: 'flow',
517
+ entity_id: ref.id,
518
+ version: ref.version,
519
+ ref_path: ref.path,
520
+ })),
521
+ };
522
+ }
523
+ function buildRunPlan(task) {
524
+ const stagesByType = {
525
+ support: ['identity', 'schema', 'qa_or_profile', 'checkpoint'],
526
+ flow: ['identity', 'name_plan', 'schema', 'qa', 'checkpoint'],
527
+ process: [
528
+ 'dependency_closure',
529
+ 'reference_refresh',
530
+ 'required_fields',
531
+ 'schema',
532
+ 'qa',
533
+ 'curation',
534
+ 'remote_dry_run',
535
+ 'readback',
536
+ ],
537
+ };
538
+ return {
539
+ schema_version: 1,
540
+ task_id: task.task_id,
541
+ entity_type: task.entity_type,
542
+ entity_id: task.entity_id,
543
+ version: task.version,
544
+ input_rows_file: task.input_rows_file,
545
+ checkpoint_file: task.checkpoint_file,
546
+ stages: stagesByType[task.entity_type].map((stage) => ({
547
+ id: stage,
548
+ status: 'pending',
549
+ checkpoint_file: path.join(task.work_dir, 'checkpoints', `${stage}.json`),
550
+ })),
551
+ ai_authoring_policy: {
552
+ output_only: 'structured_patch_or_build_plan',
553
+ deterministic_apply_required: true,
554
+ remote_write_allowed: false,
555
+ },
556
+ };
557
+ }
558
+ function extractDeferredProcessFlowRefs(payload) {
559
+ const refs = new Map();
560
+ scanForDeferredProcessFlowRefs(payload, refs);
561
+ return [...refs.values()].sort((a, b) => `${a.id}@${a.version ?? ''}@${a.path}`.localeCompare(`${b.id}@${b.version ?? ''}@${b.path}`));
562
+ }
563
+ function scanForDeferredProcessFlowRefs(value, refs) {
564
+ if (Array.isArray(value)) {
565
+ value.forEach((item) => scanForDeferredProcessFlowRefs(item, refs));
566
+ return;
567
+ }
568
+ if (!isRecord(value)) {
569
+ return;
570
+ }
571
+ const traces = asList(value['tiangongfoundry:unresolvedTrace']);
572
+ for (const trace of traces) {
573
+ if (!isRecord(trace)) {
574
+ continue;
575
+ }
576
+ const actionItemCode = firstNonEmpty(trace.action_item_code, trace.actionItemCode);
577
+ if (actionItemCode !== 'elementary_flow_identity_manual_review') {
578
+ continue;
579
+ }
580
+ const id = firstNonEmpty(trace.reference_id, trace.referenceId, trace.ref_object_id);
581
+ const pathValue = firstNonEmpty(trace.blocked_path, trace.blockedPath, trace.path);
582
+ if (!id || !pathValue) {
583
+ continue;
584
+ }
585
+ const version = firstNonEmpty(trace.reference_version, trace.referenceVersion, trace.version);
586
+ const ref = {
587
+ id,
588
+ version,
589
+ path: normalizeReferencePath(pathValue),
590
+ actionItemCode,
591
+ reason: firstNonEmpty(trace.reason),
592
+ };
593
+ refs.set(flowRefKey(ref), ref);
594
+ }
595
+ for (const nested of Object.values(value)) {
596
+ scanForDeferredProcessFlowRefs(nested, refs);
597
+ }
598
+ }
599
+ function deferredProcessFlowRefKeys(refs) {
600
+ const keys = new Map();
601
+ for (const ref of refs) {
602
+ keys.set(flowRefKey(ref), ref);
603
+ }
604
+ return keys;
605
+ }
606
+ function flowRefKey(ref) {
607
+ return `${ref.id}@${ref.version ?? ''}@${normalizeReferencePath(ref.path)}`;
608
+ }
609
+ function normalizeReferencePath(value) {
610
+ return value
611
+ .replace(/^\/+/u, '')
612
+ .replace(/\/+/gu, '.')
613
+ .replace(/^\.+|\.+$/gu, '');
614
+ }
615
+ function isFoundryTracePathParts(pathParts) {
616
+ return (pathParts.includes('common:other') &&
617
+ pathParts.some((part) => part.startsWith('tiangongfoundry:') && part.toLowerCase().includes('trace')));
618
+ }
619
+ function extractProcessFlowRefs(payload) {
620
+ const refs = new Map();
621
+ scanForFlowRefs(payload, [], refs);
622
+ return [...refs.values()].sort((a, b) => `${a.id}@${a.version ?? ''}`.localeCompare(`${b.id}@${b.version ?? ''}`));
623
+ }
624
+ function scanForFlowRefs(value, pathParts, refs) {
625
+ if (isFoundryTracePathParts(pathParts)) {
626
+ return;
627
+ }
628
+ if (Array.isArray(value)) {
629
+ value.forEach((item, index) => scanForFlowRefs(item, [...pathParts, String(index)], refs));
630
+ return;
631
+ }
632
+ if (!isRecord(value)) {
633
+ return;
634
+ }
635
+ const keyPath = pathParts.join('.');
636
+ const keyHint = keyPath.toLowerCase();
637
+ const looksLikeFlowRef = keyHint.includes('referencetoflowdataset') ||
638
+ keyHint.includes('reference_to_flow_dataset') ||
639
+ keyHint.includes('flowdataset') ||
640
+ keyHint.includes('flow_dataset');
641
+ if (looksLikeFlowRef) {
642
+ const id = firstNonEmpty(value['@refObjectId'], value.refObjectId, value.ref_object_id, value.id, value.uuid, value['common:UUID']);
643
+ if (id) {
644
+ const version = firstNonEmpty(value['@version'], value.version, value.dataSetVersion, value['common:dataSetVersion']);
645
+ refs.set(`${id}@${version ?? ''}@${keyPath}`, {
646
+ id,
647
+ version,
648
+ path: keyPath,
649
+ });
650
+ }
651
+ }
652
+ for (const [key, nested] of Object.entries(value)) {
653
+ scanForFlowRefs(nested, [...pathParts, key], refs);
654
+ }
655
+ }
656
+ function readExternalFlowRefs(inputPath) {
657
+ const rows = materializeDatasetRows(inputPath);
658
+ return rows.map((row) => {
659
+ const id = firstNonEmpty(row.id, row.row.id, row.row.dataset_id, row.row.uuid);
660
+ if (!id) {
661
+ throw new CliError(`External flow ref is missing id in ${inputPath} at index ${row.index}.`, {
662
+ code: 'CURATION_QUEUE_EXTERNAL_FLOW_REF_ID_MISSING',
663
+ exitCode: 2,
664
+ });
665
+ }
666
+ return {
667
+ id,
668
+ version: firstNonEmpty(row.version, row.row.version) ?? null,
669
+ path: `${inputPath}#${row.index}`,
670
+ };
671
+ });
672
+ }
673
+ function asList(value) {
674
+ if (value === undefined || value === null) {
675
+ return [];
676
+ }
677
+ return Array.isArray(value) ? value : [value];
678
+ }
679
+ function normalizeProcessLimit(value) {
680
+ if (value === undefined) {
681
+ return null;
682
+ }
683
+ if (!Number.isInteger(value) || value < 1) {
684
+ throw new CliError('--process-limit must be a positive integer.', {
685
+ code: 'CURATION_QUEUE_PROCESS_LIMIT_INVALID',
686
+ exitCode: 2,
687
+ });
688
+ }
689
+ return value;
690
+ }
691
+ function requirePath(value, flag) {
692
+ const trimmed = value?.trim();
693
+ if (!trimmed) {
694
+ throw new CliError(`${flag} is required.`, {
695
+ code: 'CURATION_QUEUE_REQUIRED_FLAG_MISSING',
696
+ exitCode: 2,
697
+ });
698
+ }
699
+ return trimmed;
700
+ }
701
+ function requireExistingPath(value, flag) {
702
+ const inputPath = requirePath(value, flag);
703
+ if (!existsSync(inputPath)) {
704
+ throw new CliError(`${flag} file does not exist: ${inputPath}`, {
705
+ code: 'CURATION_QUEUE_INPUT_NOT_FOUND',
706
+ exitCode: 2,
707
+ });
708
+ }
709
+ return inputPath;
710
+ }
711
+ function entityDirPlural(entityType) {
712
+ return entityType === 'process' ? 'processes' : entityType === 'flow' ? 'flows' : 'supports';
713
+ }
714
+ function entityDirName(id, version) {
715
+ return `${sanitizePathToken(id)}__${sanitizePathToken(version)}`;
716
+ }
717
+ function taskIdFor(entityType, id, version) {
718
+ return `${entityType}:${id}@${version}`;
719
+ }
720
+ function sanitizePathToken(value) {
721
+ return value.replace(/[^A-Za-z0-9._-]+/gu, '_').replace(/^_+|_+$/gu, '') || 'unknown';
722
+ }
723
+ function buildInputHashes(inputPaths) {
724
+ const hashes = {};
725
+ for (const inputPath of inputPaths) {
726
+ hashes[path.resolve(inputPath)] = sha256(readFileSync(inputPath));
727
+ }
728
+ return hashes;
729
+ }
730
+ function sha256(value) {
731
+ return createHash('sha256').update(value).digest('hex');
732
+ }
733
+ function writeJson(filePath, value) {
734
+ mkdirSync(path.dirname(filePath), { recursive: true });
735
+ writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
736
+ }
737
+ function writeText(filePath, value) {
738
+ mkdirSync(path.dirname(filePath), { recursive: true });
739
+ writeFileSync(filePath, value, 'utf8');
740
+ }
741
+ function jsonLines(rows) {
742
+ return rows.map((row) => JSON.stringify(row)).join('\n') + (rows.length > 0 ? '\n' : '');
743
+ }
744
+ export const __testInternals = {
745
+ buildTaskStates,
746
+ countTaskStates,
747
+ entityDirName,
748
+ entityDirPlural,
749
+ extractDeferredProcessFlowRefs,
750
+ extractProcessFlowRefs,
751
+ jsonLines,
752
+ normalizeReferencePath,
753
+ normalizeProcessLimit,
754
+ normalizeQueueScope,
755
+ parseQueueBlocker,
756
+ parseQueueTask,
757
+ readCheckpointStatus,
758
+ readJsonlFile,
759
+ readQueueRuntime,
760
+ requireExistingPath,
761
+ requirePath,
762
+ sanitizePathToken,
763
+ taskIdFor,
764
+ taskMatchesScope,
765
+ taskRuntimeStatus,
766
+ withQueueAction,
767
+ };
768
+ //# sourceMappingURL=dataset-curation-queue.js.map