@tiangong-lca/cli 0.0.11 → 0.0.13

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 (60) hide show
  1. package/README.md +18 -7
  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 +847 -13
  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 +359 -2
  29. package/dist/src/lib/dataset-curation-queue.js.map +1 -1
  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/process-save-draft-run.js +10 -0
  55. package/dist/src/lib/process-save-draft-run.js.map +1 -1
  56. package/dist/src/lib/process-save-draft.js +59 -3
  57. package/dist/src/lib/process-save-draft.js.map +1 -1
  58. package/dist/src/lib/supabase-client.js +19 -8
  59. package/dist/src/lib/supabase-client.js.map +1 -1
  60. package/package.json +2 -1
@@ -32,6 +32,8 @@ export async function runDatasetCurationQueueBuild(options) {
32
32
  const flowTasks = flowRows.map((row) => buildTask(outDir, row, []));
33
33
  const processTasks = processRows.map((row) => {
34
34
  const refs = extractProcessFlowRefs(row.payload);
35
+ const deferredRefs = extractDeferredProcessFlowRefs(row.payload);
36
+ const deferredRefKeys = deferredProcessFlowRefKeys(deferredRefs);
35
37
  const dependsOn = new Set();
36
38
  const missingRefs = [];
37
39
  for (const ref of refs) {
@@ -39,7 +41,7 @@ export async function runDatasetCurationQueueBuild(options) {
39
41
  if (taskId) {
40
42
  dependsOn.add(taskId);
41
43
  }
42
- else if (!externalFlowIds.has(ref.id)) {
44
+ else if (!externalFlowIds.has(ref.id) && !deferredRefKeys.has(flowRefKey(ref))) {
43
45
  missingRefs.push(ref);
44
46
  }
45
47
  }
@@ -68,6 +70,7 @@ export async function runDatasetCurationQueueBuild(options) {
68
70
  row,
69
71
  task,
70
72
  flowRefs: row.entityType === 'process' ? extractProcessFlowRefs(row.payload) : [],
73
+ deferredFlowRefs: row.entityType === 'process' ? extractDeferredProcessFlowRefs(row.payload) : [],
71
74
  externalFlowIds,
72
75
  flowRowsById,
73
76
  });
@@ -134,6 +137,250 @@ export async function runDatasetCurationQueueBuild(options) {
134
137
  writeText(blockersPath, jsonLines(blockers));
135
138
  return report;
136
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
+ }
137
384
  function readQueueRows(inputPath, entityType) {
138
385
  const rows = materializeDatasetRows(inputPath);
139
386
  return rows.map((row) => queueRowFromDatasetRow(inputPath, entityType, row));
@@ -222,9 +469,11 @@ function buildDependencyClosure(options) {
222
469
  return {
223
470
  local_tasks: [],
224
471
  external_refs: [],
472
+ deferred_refs: [],
225
473
  unresolved_refs: [],
226
474
  };
227
475
  }
476
+ const deferredRefKeys = deferredProcessFlowRefKeys(options.deferredFlowRefs);
228
477
  return {
229
478
  local_tasks: options.flowRefs
230
479
  .map((ref) => ({ ref, row: options.flowRowsById.get(ref.id) }))
@@ -244,8 +493,25 @@ function buildDependencyClosure(options) {
244
493
  version: ref.version,
245
494
  ref_path: ref.path,
246
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
+ }),
247
511
  unresolved_refs: options.flowRefs
248
- .filter((ref) => !options.flowRowsById.has(ref.id) && !options.externalFlowIds.has(ref.id))
512
+ .filter((ref) => !options.flowRowsById.has(ref.id) &&
513
+ !options.externalFlowIds.has(ref.id) &&
514
+ !deferredRefKeys.has(flowRefKey(ref)))
249
515
  .map((ref) => ({
250
516
  entity_type: 'flow',
251
517
  entity_id: ref.id,
@@ -289,12 +555,76 @@ function buildRunPlan(task) {
289
555
  },
290
556
  };
291
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
+ }
292
619
  function extractProcessFlowRefs(payload) {
293
620
  const refs = new Map();
294
621
  scanForFlowRefs(payload, [], refs);
295
622
  return [...refs.values()].sort((a, b) => `${a.id}@${a.version ?? ''}`.localeCompare(`${b.id}@${b.version ?? ''}`));
296
623
  }
297
624
  function scanForFlowRefs(value, pathParts, refs) {
625
+ if (isFoundryTracePathParts(pathParts)) {
626
+ return;
627
+ }
298
628
  if (Array.isArray(value)) {
299
629
  value.forEach((item, index) => scanForFlowRefs(item, [...pathParts, String(index)], refs));
300
630
  return;
@@ -340,6 +670,12 @@ function readExternalFlowRefs(inputPath) {
340
670
  };
341
671
  });
342
672
  }
673
+ function asList(value) {
674
+ if (value === undefined || value === null) {
675
+ return [];
676
+ }
677
+ return Array.isArray(value) ? value : [value];
678
+ }
343
679
  function normalizeProcessLimit(value) {
344
680
  if (value === undefined) {
345
681
  return null;
@@ -406,6 +742,27 @@ function jsonLines(rows) {
406
742
  return rows.map((row) => JSON.stringify(row)).join('\n') + (rows.length > 0 ? '\n' : '');
407
743
  }
408
744
  export const __testInternals = {
745
+ buildTaskStates,
746
+ countTaskStates,
747
+ entityDirName,
748
+ entityDirPlural,
749
+ extractDeferredProcessFlowRefs,
409
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,
410
767
  };
411
768
  //# sourceMappingURL=dataset-curation-queue.js.map