@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
@@ -0,0 +1,947 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { writeJsonArtifact, writeJsonLinesArtifact } from './artifacts.js';
5
+ import { cloneJson, firstNonEmpty, isRecord, readDatasetRowsInput, trimToken, unwrapDatasetPayload, } from './dataset-local.js';
6
+ import { CliError } from './errors.js';
7
+ import { readJsonInput } from './io.js';
8
+ const CATEGORY_CONFIGS = {
9
+ contact: {
10
+ type: 'contact',
11
+ schemaFile: 'tidas_contacts_category.json',
12
+ defaultValueKey: '@classId',
13
+ target: 'common:class',
14
+ },
15
+ 'flow-elementary': {
16
+ type: 'flow-elementary',
17
+ schemaFile: 'tidas_flows_elementary_category.json',
18
+ defaultValueKey: '@catId',
19
+ target: 'common:category',
20
+ },
21
+ 'flow-product': {
22
+ type: 'flow-product',
23
+ schemaFile: 'tidas_flows_product_category.json',
24
+ defaultValueKey: '@classId',
25
+ target: 'common:class',
26
+ },
27
+ flowproperty: {
28
+ type: 'flowproperty',
29
+ schemaFile: 'tidas_flowproperties_category.json',
30
+ defaultValueKey: '@classId',
31
+ target: 'common:class',
32
+ },
33
+ lciamethod: {
34
+ type: 'lciamethod',
35
+ schemaFile: 'tidas_lciamethods_category.json',
36
+ defaultValueKey: '@classId',
37
+ target: 'common:class',
38
+ },
39
+ location: {
40
+ type: 'location',
41
+ schemaFile: 'tidas_locations_category.json',
42
+ defaultValueKey: '@code',
43
+ target: 'location',
44
+ },
45
+ process: {
46
+ type: 'process',
47
+ schemaFile: 'tidas_processes_category.json',
48
+ defaultValueKey: '@classId',
49
+ target: 'common:class',
50
+ },
51
+ source: {
52
+ type: 'source',
53
+ schemaFile: 'tidas_sources_category.json',
54
+ defaultValueKey: '@classId',
55
+ target: 'common:class',
56
+ },
57
+ unitgroup: {
58
+ type: 'unitgroup',
59
+ schemaFile: 'tidas_unitgroups_category.json',
60
+ defaultValueKey: '@classId',
61
+ target: 'common:class',
62
+ },
63
+ };
64
+ const DATASET_ROOTS = [
65
+ { rootKey: 'contactDataSet', informationKey: 'contactInformation', type: 'contact' },
66
+ { rootKey: 'flowDataSet', informationKey: 'flowInformation', type: 'flow-product' },
67
+ {
68
+ rootKey: 'flowPropertyDataSet',
69
+ informationKey: 'flowPropertiesInformation',
70
+ type: 'flowproperty',
71
+ },
72
+ { rootKey: 'LCIAMethodDataSet', informationKey: 'LCIAMethodInformation', type: 'lciamethod' },
73
+ {
74
+ rootKey: 'lifeCycleModelDataSet',
75
+ informationKey: 'lifeCycleModelInformation',
76
+ type: 'lifecyclemodel',
77
+ },
78
+ { rootKey: 'processDataSet', informationKey: 'processInformation', type: 'process' },
79
+ { rootKey: 'sourceDataSet', informationKey: 'sourceInformation', type: 'source' },
80
+ { rootKey: 'unitGroupDataSet', informationKey: 'unitGroupInformation', type: 'unitgroup' },
81
+ ];
82
+ const TYPE_ALIASES = {
83
+ contact: 'contact',
84
+ contacts: 'contact',
85
+ elementary: 'flow-elementary',
86
+ elementaryflow: 'flow-elementary',
87
+ elementaryflows: 'flow-elementary',
88
+ 'elementary-flow': 'flow-elementary',
89
+ 'elementary-flows': 'flow-elementary',
90
+ flowelementary: 'flow-elementary',
91
+ 'flow-elementary': 'flow-elementary',
92
+ flowproduct: 'flow-product',
93
+ flowproducts: 'flow-product',
94
+ 'flow-product': 'flow-product',
95
+ 'flow-products': 'flow-product',
96
+ flows: 'flow-product',
97
+ product: 'flow-product',
98
+ productflow: 'flow-product',
99
+ productflows: 'flow-product',
100
+ 'product-flow': 'flow-product',
101
+ 'product-flows': 'flow-product',
102
+ flowproperties: 'flowproperty',
103
+ flowproperty: 'flowproperty',
104
+ lcia: 'lciamethod',
105
+ lciamethod: 'lciamethod',
106
+ lciamethods: 'lciamethod',
107
+ location: 'location',
108
+ locations: 'location',
109
+ process: 'process',
110
+ processes: 'process',
111
+ source: 'source',
112
+ sources: 'source',
113
+ unitgroup: 'unitgroup',
114
+ unitgroups: 'unitgroup',
115
+ };
116
+ function normalizeType(value) {
117
+ const token = trimToken(value)
118
+ ?.toLowerCase()
119
+ .replace(/[_\s]+/gu, '-');
120
+ const normalized = token ? (TYPE_ALIASES[token] ?? TYPE_ALIASES[token.replace(/-/gu, '')]) : null;
121
+ if (!normalized) {
122
+ throw new CliError(`Unsupported classification type: ${String(value ?? '')}`, {
123
+ code: 'CLASSIFICATION_TYPE_UNSUPPORTED',
124
+ exitCode: 2,
125
+ });
126
+ }
127
+ return normalized;
128
+ }
129
+ function schemasDir(candidatesOverride) {
130
+ const here = path.dirname(fileURLToPath(import.meta.url));
131
+ const candidates = candidatesOverride ?? [
132
+ path.resolve(here, '../../assets/tidas-schemas'),
133
+ path.resolve(here, '../../../assets/tidas-schemas'),
134
+ path.resolve(process.cwd(), 'assets/tidas-schemas'),
135
+ ];
136
+ const found = candidates.find((candidate) => existsSync(candidate));
137
+ if (!found) {
138
+ throw new CliError('Bundled TIDAS schemas were not found under assets/tidas-schemas.', {
139
+ code: 'TIDAS_SCHEMAS_NOT_FOUND',
140
+ exitCode: 2,
141
+ });
142
+ }
143
+ return found;
144
+ }
145
+ function schemaPath(config) {
146
+ return path.join(schemasDir(), config.schemaFile);
147
+ }
148
+ const FALLBACK_LOCATION_TARGET_KEYS = new Set([
149
+ '@location',
150
+ '@subLocation',
151
+ 'impactLocation',
152
+ 'impactSubLocation',
153
+ 'interventionLocation',
154
+ 'interventionSubLocation',
155
+ 'intervensionSubLocation',
156
+ 'location',
157
+ 'locationOfSupply',
158
+ 'subLocation',
159
+ ]);
160
+ let cachedLocationTargetKeys = null;
161
+ function lastSchemaPropertyName(schemaPathSegments) {
162
+ let propertyName = null;
163
+ for (let index = 0; index < schemaPathSegments.length - 1; index += 1) {
164
+ if (schemaPathSegments[index] === 'properties') {
165
+ propertyName = schemaPathSegments[index + 1];
166
+ }
167
+ }
168
+ return propertyName;
169
+ }
170
+ function collectLocationRefKeysFromSchema(value, schemaPathSegments, keys) {
171
+ if (Array.isArray(value)) {
172
+ value.forEach((item, index) => collectLocationRefKeysFromSchema(item, [...schemaPathSegments, String(index)], keys));
173
+ return;
174
+ }
175
+ if (!isRecord(value))
176
+ return;
177
+ if (value.$ref === 'tidas_locations_category.json') {
178
+ const propertyName = lastSchemaPropertyName(schemaPathSegments);
179
+ if (propertyName)
180
+ keys.add(propertyName);
181
+ }
182
+ for (const [key, child] of Object.entries(value)) {
183
+ collectLocationRefKeysFromSchema(child, [...schemaPathSegments, key], keys);
184
+ }
185
+ }
186
+ function locationTargetKeys() {
187
+ if (cachedLocationTargetKeys)
188
+ return cachedLocationTargetKeys;
189
+ const keys = new Set();
190
+ const dir = schemasDir();
191
+ for (const fileName of readdirSync(dir).filter(isJsonSchemaFileName)) {
192
+ const document = JSON.parse(readFileSync(path.join(dir, fileName), 'utf8'));
193
+ collectLocationRefKeysFromSchema(document, [], keys);
194
+ }
195
+ cachedLocationTargetKeys = new Set([...FALLBACK_LOCATION_TARGET_KEYS, ...keys]);
196
+ return cachedLocationTargetKeys;
197
+ }
198
+ function isJsonSchemaFileName(fileName) {
199
+ return fileName.endsWith('.json');
200
+ }
201
+ function constText(value) {
202
+ return isRecord(value) ? firstNonEmpty(value.const) : null;
203
+ }
204
+ function collectEntriesFromNode(node, defaultValueKey, entries) {
205
+ if (Array.isArray(node)) {
206
+ node.forEach((entry) => collectEntriesFromNode(entry, defaultValueKey, entries));
207
+ return;
208
+ }
209
+ if (!isRecord(node))
210
+ return;
211
+ if (isRecord(node.properties)) {
212
+ const properties = node.properties;
213
+ const level = firstNonEmpty(constText(properties['@level']), '0');
214
+ const valueKey = ['@classId', '@catId', '@code'].find((key) => constText(properties[key])) ??
215
+ defaultValueKey;
216
+ const code = constText(properties[valueKey]);
217
+ const text = constText(properties['#text']);
218
+ if (level && code && text) {
219
+ const parsedLevel = Number(level);
220
+ if (Number.isInteger(parsedLevel)) {
221
+ entries.push({ level: parsedLevel, code, text, value_key: valueKey });
222
+ }
223
+ }
224
+ }
225
+ else if (node.const !== undefined && typeof node.description === 'string') {
226
+ entries.push({
227
+ level: 0,
228
+ code: String(node.const),
229
+ text: node.description,
230
+ value_key: '@code',
231
+ });
232
+ }
233
+ for (const value of Object.values(node)) {
234
+ if (isRecord(value) || Array.isArray(value)) {
235
+ collectEntriesFromNode(value, defaultValueKey, entries);
236
+ }
237
+ }
238
+ }
239
+ function loadEntries(type) {
240
+ const config = CATEGORY_CONFIGS[type];
241
+ const schema = schemaPath(config);
242
+ const document = JSON.parse(readFileSync(schema, 'utf8'));
243
+ const entries = [];
244
+ collectEntriesFromNode(document, config.defaultValueKey, entries);
245
+ return { config, schema, entries };
246
+ }
247
+ function buildNavigator(entries) {
248
+ const childMap = new Map();
249
+ const parentMap = new Map();
250
+ const entriesByCode = new Map();
251
+ const lastPerLevel = new Map();
252
+ for (const entry of entries) {
253
+ entriesByCode.set(entry.code, entry);
254
+ if (entry.level === 0) {
255
+ const roots = childMap.get('') ?? [];
256
+ roots.push(entry);
257
+ childMap.set('', roots);
258
+ parentMap.set(entry.code, null);
259
+ }
260
+ else {
261
+ let parent = null;
262
+ for (let level = entry.level - 1; level >= 0; level -= 1) {
263
+ parent = lastPerLevel.get(level) ?? null;
264
+ if (parent)
265
+ break;
266
+ }
267
+ if (parent) {
268
+ const children = childMap.get(parent.code) ?? [];
269
+ children.push(entry);
270
+ childMap.set(parent.code, children);
271
+ parentMap.set(entry.code, parent);
272
+ }
273
+ }
274
+ lastPerLevel.set(entry.level, entry);
275
+ }
276
+ return { entries, entriesByCode, childMap, parentMap };
277
+ }
278
+ function navigatorFor(type) {
279
+ const { config, schema, entries } = loadEntries(type);
280
+ return { config, schema, navigator: buildNavigator(entries) };
281
+ }
282
+ function pathForCode(navigator, code) {
283
+ const entry = navigator.entriesByCode.get(code);
284
+ if (!entry)
285
+ return [];
286
+ const pathEntries = [entry];
287
+ let current = entry;
288
+ while (true) {
289
+ const parent = navigator.parentMap.get(current.code);
290
+ if (!parent)
291
+ break;
292
+ pathEntries.push(parent);
293
+ current = parent;
294
+ }
295
+ return pathEntries.reverse();
296
+ }
297
+ function toPathEntry(entry) {
298
+ return {
299
+ '@level': String(entry.level),
300
+ [entry.value_key]: entry.code,
301
+ '#text': entry.text,
302
+ };
303
+ }
304
+ function classCode(value) {
305
+ return isRecord(value)
306
+ ? firstNonEmpty(value['@classId'], value['@catId'], value['@code'], value.class_id, value.classId, value.cat_id, value.catId, value.code)
307
+ : null;
308
+ }
309
+ function pathExpression(pathSegments) {
310
+ return pathSegments.map(String).join('.');
311
+ }
312
+ function normalizeTargetPath(value) {
313
+ const raw = firstNonEmpty(value);
314
+ if (!raw)
315
+ return null;
316
+ if (raw.startsWith('/')) {
317
+ return raw
318
+ .split('/')
319
+ .slice(1)
320
+ .map((segment) => segment.replace(/~1/gu, '/').replace(/~0/gu, '~'))
321
+ .join('.');
322
+ }
323
+ return raw;
324
+ }
325
+ function decisionTargetPath(decision) {
326
+ return normalizeTargetPath(decision.target_path ??
327
+ decision.targetPath ??
328
+ decision.field_path ??
329
+ decision.fieldPath ??
330
+ decision.location_path ??
331
+ decision.locationPath ??
332
+ decision.json_pointer ??
333
+ decision.jsonPointer);
334
+ }
335
+ function normalizePathFromClasses(type, classes) {
336
+ const { navigator } = navigatorFor(type);
337
+ const rawClasses = Array.isArray(classes) ? classes : isRecord(classes) ? [classes] : [];
338
+ if (rawClasses.length === 0)
339
+ return [];
340
+ const leafCode = classCode(rawClasses[rawClasses.length - 1]);
341
+ if (!leafCode)
342
+ return [];
343
+ const canonical = pathForCode(navigator, leafCode);
344
+ const rawCodes = rawClasses.map(classCode).filter(Boolean);
345
+ const canonicalCodes = canonical.map((entry) => entry.code);
346
+ if (rawCodes.length > 0 &&
347
+ rawCodes.join('/') !== canonicalCodes.slice(0, rawCodes.length).join('/')) {
348
+ return [];
349
+ }
350
+ return canonical.map(toPathEntry);
351
+ }
352
+ function normalizePathFromDecision(type, decision) {
353
+ const explicit = decision.classes ??
354
+ decision.classification_classes ??
355
+ (isRecord(decision.classification)
356
+ ? (decision.classification['common:class'] ??
357
+ decision.classification['common:category'] ??
358
+ decision.classification.classes)
359
+ : null);
360
+ const explicitPath = normalizePathFromClasses(type, explicit);
361
+ if (explicitPath.length > 0)
362
+ return explicitPath;
363
+ const directCode = firstNonEmpty(decision.code, decision.class_id, decision.classId, decision.cat_id, decision.catId, decision.leaf_code, decision.leafCode);
364
+ if (directCode) {
365
+ const { navigator } = navigatorFor(type);
366
+ return pathForCode(navigator, directCode).map(toPathEntry);
367
+ }
368
+ const rawLabels = decision.classification_path ?? decision.classificationPath;
369
+ const rawIds = decision.class_ids ?? decision.classIds;
370
+ const labels = Array.isArray(rawLabels) ? rawLabels : [];
371
+ const ids = Array.isArray(rawIds) ? rawIds : [];
372
+ if (labels.length > 0 && labels.length === ids.length) {
373
+ return normalizePathFromClasses(type, labels.map((label, index) => ({
374
+ '@level': String(index),
375
+ '@classId': ids[index],
376
+ '#text': label,
377
+ })));
378
+ }
379
+ return [];
380
+ }
381
+ function normalizeDecisionType(decision, fallbackType) {
382
+ const raw = firstNonEmpty(decision.category_type, decision.categoryType, decision.classification_type, decision.classificationType, decision.type);
383
+ return raw ? normalizeType(raw) : fallbackType;
384
+ }
385
+ function readJsonLines(filePath) {
386
+ return readFileSync(filePath, 'utf8')
387
+ .split(/\r?\n/u)
388
+ .map((line) => line.trim())
389
+ .filter(Boolean)
390
+ .map((line, index) => {
391
+ const parsed = JSON.parse(line);
392
+ if (!isRecord(parsed)) {
393
+ throw new CliError(`Classification decision line ${index + 1} is not an object.`, {
394
+ code: 'CLASSIFICATION_DECISION_INVALID',
395
+ exitCode: 2,
396
+ });
397
+ }
398
+ return parsed;
399
+ });
400
+ }
401
+ function normalizeStructuredDecisions(value) {
402
+ if (Array.isArray(value))
403
+ return value.filter(isRecord);
404
+ if (isRecord(value) && Array.isArray(value.decisions))
405
+ return value.decisions.filter(isRecord);
406
+ if (isRecord(value) && Array.isArray(value.rows))
407
+ return value.rows.filter(isRecord);
408
+ return isRecord(value) ? [value] : [];
409
+ }
410
+ function readDecisions(pathValue, rawInput) {
411
+ if (!pathValue) {
412
+ throw new CliError('Missing required --decisions value.', {
413
+ code: 'CLASSIFICATION_DECISIONS_REQUIRED',
414
+ exitCode: 2,
415
+ });
416
+ }
417
+ if (rawInput !== undefined)
418
+ return normalizeStructuredDecisions(rawInput);
419
+ const resolved = path.resolve(pathValue);
420
+ if (!existsSync(resolved)) {
421
+ throw new CliError(`Classification decisions file not found: ${resolved}`, {
422
+ code: 'CLASSIFICATION_DECISIONS_NOT_FOUND',
423
+ exitCode: 2,
424
+ });
425
+ }
426
+ return resolved.toLowerCase().endsWith('.jsonl')
427
+ ? readJsonLines(resolved)
428
+ : normalizeStructuredDecisions(readJsonInput(resolved));
429
+ }
430
+ function prepareRows(inputPath, rawInput) {
431
+ return readDatasetRowsInput(inputPath, rawInput).map((row, index) => {
432
+ const clonedRow = cloneJson(row);
433
+ const payload = unwrapDatasetPayload(clonedRow);
434
+ const descriptor = DATASET_ROOTS.find((candidate) => isRecord(payload[candidate.rootKey]));
435
+ const root = descriptor ? payload[descriptor.rootKey] : {};
436
+ const info = descriptor && isRecord(root[descriptor.informationKey])
437
+ ? root[descriptor.informationKey]
438
+ : {};
439
+ const dataSetInformation = isRecord(info.dataSetInformation)
440
+ ? info.dataSetInformation
441
+ : {};
442
+ const administrativeInformation = isRecord(root.administrativeInformation)
443
+ ? root.administrativeInformation
444
+ : {};
445
+ const publicationAndOwnership = isRecord(administrativeInformation.publicationAndOwnership)
446
+ ? administrativeInformation.publicationAndOwnership
447
+ : {};
448
+ return {
449
+ index,
450
+ row: clonedRow,
451
+ payload,
452
+ rootKey: descriptor?.rootKey ?? null,
453
+ informationKey: descriptor?.informationKey ?? null,
454
+ id: firstNonEmpty(clonedRow.id, dataSetInformation['common:UUID']),
455
+ version: firstNonEmpty(clonedRow.version, publicationAndOwnership['common:dataSetVersion']),
456
+ };
457
+ });
458
+ }
459
+ function classificationContainer(row, type) {
460
+ if (CATEGORY_CONFIGS[type].target === 'location')
461
+ return null;
462
+ if (!row.rootKey || !row.informationKey)
463
+ return null;
464
+ const root = row.payload[row.rootKey];
465
+ if (!isRecord(root))
466
+ return null;
467
+ const info = root[row.informationKey];
468
+ if (!isRecord(info))
469
+ return null;
470
+ if (!isRecord(info.dataSetInformation))
471
+ info.dataSetInformation = {};
472
+ const dataSetInformation = info.dataSetInformation;
473
+ if (!isRecord(dataSetInformation.classificationInformation)) {
474
+ dataSetInformation.classificationInformation = {};
475
+ }
476
+ const classificationInformation = dataSetInformation.classificationInformation;
477
+ const target = CATEGORY_CONFIGS[type].target;
478
+ if (target === 'common:category') {
479
+ if (!isRecord(classificationInformation['common:elementaryFlowCategorization'])) {
480
+ classificationInformation['common:elementaryFlowCategorization'] = {};
481
+ }
482
+ return classificationInformation['common:elementaryFlowCategorization'];
483
+ }
484
+ if (!isRecord(classificationInformation['common:classification'])) {
485
+ classificationInformation['common:classification'] = {};
486
+ }
487
+ return classificationInformation['common:classification'];
488
+ }
489
+ function currentClassification(row, type) {
490
+ const container = classificationContainer(row, type);
491
+ if (!container)
492
+ return null;
493
+ return CATEGORY_CONFIGS[type].target === 'common:category'
494
+ ? container['common:category']
495
+ : container['common:class'];
496
+ }
497
+ function setClassification(row, type, classificationPath) {
498
+ const container = classificationContainer(row, type);
499
+ if (!container)
500
+ return false;
501
+ if (CATEGORY_CONFIGS[type].target === 'common:category') {
502
+ container['common:category'] = cloneJson(classificationPath);
503
+ }
504
+ else {
505
+ container['common:class'] = cloneJson(classificationPath);
506
+ }
507
+ return true;
508
+ }
509
+ function isLocationTargetKey(key) {
510
+ return locationTargetKeys().has(key);
511
+ }
512
+ function locationTargetStringValue(value) {
513
+ if (typeof value === 'string') {
514
+ return {
515
+ parent: null,
516
+ key: null,
517
+ pathSuffix: [],
518
+ value: value.trim(),
519
+ };
520
+ }
521
+ if (isRecord(value) && typeof value['#text'] === 'string') {
522
+ return {
523
+ parent: value,
524
+ key: '#text',
525
+ pathSuffix: ['#text'],
526
+ value: value['#text'].trim(),
527
+ };
528
+ }
529
+ return null;
530
+ }
531
+ function collectLocationTargets(value, pathSegments = [], targets = []) {
532
+ if (!isRecord(value)) {
533
+ if (Array.isArray(value)) {
534
+ value.forEach((item, index) => collectLocationTargets(item, [...pathSegments, index], targets));
535
+ }
536
+ return targets;
537
+ }
538
+ for (const [key, child] of Object.entries(value)) {
539
+ const childPath = [...pathSegments, key];
540
+ if (isLocationTargetKey(key)) {
541
+ const targetValue = locationTargetStringValue(child);
542
+ if (targetValue) {
543
+ const leafPath = [...childPath, ...targetValue.pathSuffix];
544
+ const parent = targetValue.pathSuffix.length > 0 ? targetValue.parent : value;
545
+ const targetKey = targetValue.pathSuffix.length > 0 ? targetValue.key : key;
546
+ targets.push({
547
+ path: pathExpression(leafPath),
548
+ parentPath: pathExpression(targetValue.pathSuffix.length > 0 ? childPath : pathSegments),
549
+ parent: parent,
550
+ key: targetKey,
551
+ value: targetValue.value,
552
+ });
553
+ }
554
+ }
555
+ collectLocationTargets(child, childPath, targets);
556
+ }
557
+ return targets;
558
+ }
559
+ function locationCodeFromPath(classificationPath) {
560
+ const leaf = classificationPath.at(-1);
561
+ return classCode(leaf);
562
+ }
563
+ function resolveLocationTarget(row, decision) {
564
+ const targets = collectLocationTargets(row.payload);
565
+ if (!decision.targetPath)
566
+ return targets;
567
+ return targets.filter((target) => target.path === decision.targetPath || target.parentPath === decision.targetPath);
568
+ }
569
+ function decisionMatchesRow(decision, row) {
570
+ if (decision.rowIndex !== null)
571
+ return decision.rowIndex === row.index;
572
+ if (!decision.datasetId || decision.datasetId !== row.id)
573
+ return false;
574
+ return !decision.datasetVersion || decision.datasetVersion === row.version;
575
+ }
576
+ function normalizeDecision(decision, decisionIndex, fallbackType, blockers) {
577
+ const rawRowIndex = decision.row_index ?? decision.rowIndex;
578
+ const rowIndexText = firstNonEmpty(rawRowIndex);
579
+ const rowIndex = typeof rawRowIndex === 'number'
580
+ ? rawRowIndex
581
+ : rowIndexText === null
582
+ ? null
583
+ : Number(rowIndexText);
584
+ const validRowIndex = typeof rowIndex === 'number' && Number.isInteger(rowIndex) ? rowIndex : null;
585
+ const datasetId = firstNonEmpty(decision.dataset_id, decision.datasetId, decision.id, decision.uuid);
586
+ const datasetVersion = firstNonEmpty(decision.dataset_version, decision.datasetVersion, decision.version);
587
+ const categoryType = normalizeDecisionType(decision, fallbackType);
588
+ if (validRowIndex === null && !datasetId) {
589
+ blockers.push({
590
+ code: 'classification_decision_target_missing',
591
+ message: 'Classification decision must target row_index or dataset_id.',
592
+ decision_index: decisionIndex,
593
+ });
594
+ return null;
595
+ }
596
+ if (!categoryType) {
597
+ blockers.push({
598
+ code: 'classification_decision_type_missing',
599
+ message: 'Classification decision must provide a category_type or the command must pass --type.',
600
+ decision_index: decisionIndex,
601
+ row_index: validRowIndex ?? undefined,
602
+ dataset_id: datasetId,
603
+ dataset_version: datasetVersion,
604
+ });
605
+ return null;
606
+ }
607
+ const classificationPath = normalizePathFromDecision(categoryType, decision);
608
+ if (classificationPath.length === 0) {
609
+ blockers.push({
610
+ code: 'classification_decision_path_invalid',
611
+ message: 'Classification decision must resolve to a valid path in the bundled TIDAS category schema.',
612
+ decision_index: decisionIndex,
613
+ row_index: validRowIndex ?? undefined,
614
+ dataset_id: datasetId,
615
+ dataset_version: datasetVersion,
616
+ });
617
+ return null;
618
+ }
619
+ return {
620
+ decisionIndex,
621
+ rowIndex: validRowIndex,
622
+ datasetId,
623
+ datasetVersion,
624
+ categoryType,
625
+ path: classificationPath,
626
+ targetPath: decisionTargetPath(decision),
627
+ basis: firstNonEmpty(decision.basis),
628
+ evidence: decision.evidence ?? null,
629
+ };
630
+ }
631
+ function maybeWriteReport(report, outDir, filename) {
632
+ if (!outDir)
633
+ return report;
634
+ const reportPath = path.join(path.resolve(outDir), 'outputs', filename);
635
+ const finalReport = { ...report, files: { report: reportPath } };
636
+ writeJsonArtifact(reportPath, finalReport);
637
+ return finalReport;
638
+ }
639
+ export async function runDatasetClassificationChildren(options) {
640
+ const type = normalizeType(options.type);
641
+ const { config, navigator } = navigatorFor(type);
642
+ const parentCode = firstNonEmpty(options.parent);
643
+ const rawChildren = navigator.childMap.get(parentCode ?? '') ?? [];
644
+ const parentKnown = !parentCode || navigator.entriesByCode.has(parentCode);
645
+ const query = firstNonEmpty(options.query)?.toLowerCase() ?? null;
646
+ const filtered = query
647
+ ? rawChildren.filter((entry) => entry.code.toLowerCase().includes(query) || entry.text.toLowerCase().includes(query))
648
+ : rawChildren;
649
+ const limit = options.limit && options.limit > 0 ? options.limit : null;
650
+ const returned = limit ? filtered.slice(0, limit) : filtered;
651
+ const report = {
652
+ schema_version: 1,
653
+ generated_at_utc: (options.now ?? new Date()).toISOString(),
654
+ status: parentKnown ? 'completed' : 'blocked',
655
+ command: 'dataset classification children',
656
+ category_type: type,
657
+ schema_file: config.schemaFile,
658
+ parent_code: parentCode,
659
+ query,
660
+ counts: {
661
+ children: rawChildren.length,
662
+ returned: returned.length,
663
+ },
664
+ children: returned.map((entry) => ({
665
+ ...entry,
666
+ path: pathForCode(navigator, entry.code).map(toPathEntry),
667
+ })),
668
+ blockers: parentKnown
669
+ ? []
670
+ : [{ code: 'classification_parent_unknown', message: `Unknown parent code: ${parentCode}` }],
671
+ };
672
+ return maybeWriteReport(report, options.outDir, 'classification-children-report.json');
673
+ }
674
+ export async function runDatasetClassificationPath(options) {
675
+ const type = normalizeType(options.type);
676
+ const { config, navigator } = navigatorFor(type);
677
+ const code = firstNonEmpty(options.code);
678
+ if (!code) {
679
+ throw new CliError('Missing required --code value.', {
680
+ code: 'CLASSIFICATION_CODE_REQUIRED',
681
+ exitCode: 2,
682
+ });
683
+ }
684
+ const pathEntries = pathForCode(navigator, code).map(toPathEntry);
685
+ const report = {
686
+ schema_version: 1,
687
+ generated_at_utc: (options.now ?? new Date()).toISOString(),
688
+ status: pathEntries.length > 0 ? 'completed' : 'blocked',
689
+ command: 'dataset classification path',
690
+ category_type: type,
691
+ schema_file: config.schemaFile,
692
+ code,
693
+ path: pathEntries,
694
+ blockers: pathEntries.length > 0
695
+ ? []
696
+ : [
697
+ {
698
+ code: 'classification_code_unknown',
699
+ message: `Unknown classification code: ${code}`,
700
+ },
701
+ ],
702
+ };
703
+ return maybeWriteReport(report, options.outDir, 'classification-path-report.json');
704
+ }
705
+ export async function runDatasetClassificationAudit(options) {
706
+ if (!options.inputPath) {
707
+ throw new CliError('Missing required --input value.', {
708
+ code: 'CLASSIFICATION_AUDIT_INPUT_REQUIRED',
709
+ exitCode: 2,
710
+ });
711
+ }
712
+ const type = normalizeType(options.type);
713
+ if (type !== 'location') {
714
+ throw new CliError('dataset classification audit currently supports only --type location.', {
715
+ code: 'CLASSIFICATION_AUDIT_TYPE_UNSUPPORTED',
716
+ exitCode: 2,
717
+ });
718
+ }
719
+ const { config, navigator } = navigatorFor(type);
720
+ const rows = prepareRows(options.inputPath, options.rawInput);
721
+ const findings = [];
722
+ const blockers = [];
723
+ for (const row of rows) {
724
+ for (const target of collectLocationTargets(row.payload)) {
725
+ const entry = navigator.entriesByCode.get(target.value);
726
+ const status = entry ? 'valid' : 'invalid';
727
+ findings.push({
728
+ row_index: row.index,
729
+ dataset_id: row.id,
730
+ dataset_version: row.version,
731
+ path: target.path,
732
+ value: target.value,
733
+ status,
734
+ description: entry?.text ?? null,
735
+ });
736
+ if (!entry) {
737
+ blockers.push({
738
+ code: 'location_code_invalid',
739
+ message: 'Location value is not a code from the bundled TIDAS location schema.',
740
+ row_index: row.index,
741
+ dataset_id: row.id,
742
+ dataset_version: row.version,
743
+ });
744
+ }
745
+ }
746
+ }
747
+ const invalid = findings.filter((finding) => finding.status === 'invalid').length;
748
+ const outDir = options.outDir ? path.resolve(options.outDir) : null;
749
+ const findingsPath = outDir
750
+ ? path.join(outDir, 'outputs', 'location-audit-findings.jsonl')
751
+ : null;
752
+ const reportPath = outDir ? path.join(outDir, 'outputs', 'location-audit-report.json') : null;
753
+ const report = {
754
+ schema_version: 1,
755
+ generated_at_utc: (options.now ?? new Date()).toISOString(),
756
+ status: invalid > 0 ? 'blocked' : 'completed',
757
+ command: 'dataset classification audit',
758
+ category_type: type,
759
+ schema_file: config.schemaFile,
760
+ input_path: path.resolve(options.inputPath),
761
+ counts: {
762
+ rows: rows.length,
763
+ location_targets: findings.length,
764
+ valid: findings.length - invalid,
765
+ invalid,
766
+ },
767
+ findings,
768
+ blockers,
769
+ ...(findingsPath && reportPath
770
+ ? {
771
+ files: {
772
+ findings: findingsPath,
773
+ report: reportPath,
774
+ },
775
+ }
776
+ : {}),
777
+ };
778
+ if (findingsPath && reportPath) {
779
+ writeJsonLinesArtifact(findingsPath, findings);
780
+ writeJsonArtifact(reportPath, report);
781
+ }
782
+ return report;
783
+ }
784
+ export async function runDatasetClassificationApply(options) {
785
+ if (!options.inputPath) {
786
+ throw new CliError('Missing required --input value.', {
787
+ code: 'CLASSIFICATION_INPUT_REQUIRED',
788
+ exitCode: 2,
789
+ });
790
+ }
791
+ if (!options.outPath) {
792
+ throw new CliError('Missing required --out value.', {
793
+ code: 'CLASSIFICATION_OUT_REQUIRED',
794
+ exitCode: 2,
795
+ });
796
+ }
797
+ const fallbackType = options.type ? normalizeType(options.type) : null;
798
+ const rows = prepareRows(options.inputPath, options.rawInput);
799
+ const rawDecisions = readDecisions(options.decisionsPath, options.rawDecisions);
800
+ const blockers = [];
801
+ const decisions = rawDecisions
802
+ .map((decision, index) => normalizeDecision(decision, index, fallbackType, blockers))
803
+ .filter((decision) => Boolean(decision));
804
+ const evidence = [];
805
+ for (const decision of decisions) {
806
+ const matches = rows.filter((row) => decisionMatchesRow(decision, row));
807
+ if (matches.length !== 1) {
808
+ blockers.push({
809
+ code: matches.length === 0
810
+ ? 'classification_target_not_found'
811
+ : 'classification_target_ambiguous',
812
+ message: matches.length === 0
813
+ ? 'Classification decision did not match an input row.'
814
+ : 'Classification decision matched more than one input row.',
815
+ decision_index: decision.decisionIndex,
816
+ row_index: decision.rowIndex ?? undefined,
817
+ dataset_id: decision.datasetId,
818
+ dataset_version: decision.datasetVersion,
819
+ });
820
+ continue;
821
+ }
822
+ const row = matches[0];
823
+ if (decision.categoryType === 'location') {
824
+ const code = locationCodeFromPath(decision.path);
825
+ const targetMatches = resolveLocationTarget(row, decision);
826
+ if (!code || targetMatches.length !== 1) {
827
+ blockers.push({
828
+ code: targetMatches.length === 0 ? 'location_target_not_found' : 'location_target_ambiguous',
829
+ message: targetMatches.length === 0
830
+ ? 'Location decision did not match a location field in the input row.'
831
+ : 'Location decision matched more than one location field; provide target_path.',
832
+ decision_index: decision.decisionIndex,
833
+ row_index: row.index,
834
+ dataset_id: row.id,
835
+ dataset_version: row.version,
836
+ });
837
+ continue;
838
+ }
839
+ const target = targetMatches[0];
840
+ const previous = target.value;
841
+ target.parent[target.key] = code;
842
+ evidence.push({
843
+ decision_index: decision.decisionIndex,
844
+ row_index: row.index,
845
+ dataset_id: row.id,
846
+ dataset_version: row.version,
847
+ category_type: decision.categoryType,
848
+ target_path: target.path,
849
+ previous_location: previous,
850
+ applied_location: code,
851
+ applied_location_path: cloneJson(decision.path),
852
+ basis: decision.basis,
853
+ evidence: decision.evidence,
854
+ });
855
+ continue;
856
+ }
857
+ const previous = currentClassification(row, decision.categoryType);
858
+ if (!setClassification(row, decision.categoryType, decision.path)) {
859
+ blockers.push({
860
+ code: 'classification_container_missing',
861
+ message: 'Input row does not contain a supported classification container for this type.',
862
+ decision_index: decision.decisionIndex,
863
+ row_index: row.index,
864
+ dataset_id: row.id,
865
+ dataset_version: row.version,
866
+ });
867
+ continue;
868
+ }
869
+ evidence.push({
870
+ decision_index: decision.decisionIndex,
871
+ row_index: row.index,
872
+ dataset_id: row.id,
873
+ dataset_version: row.version,
874
+ category_type: decision.categoryType,
875
+ previous_classification: previous,
876
+ applied_classification: cloneJson(decision.path),
877
+ basis: decision.basis,
878
+ evidence: decision.evidence,
879
+ });
880
+ }
881
+ const outDir = options.outDir
882
+ ? path.resolve(options.outDir)
883
+ : path.dirname(path.resolve(options.outPath));
884
+ const evidencePath = path.join(outDir, 'outputs', 'classification-apply-evidence.jsonl');
885
+ const reportPath = path.join(outDir, 'outputs', 'classification-apply-report.json');
886
+ writeJsonLinesArtifact(options.outPath, rows.map((row) => row.payload));
887
+ writeJsonLinesArtifact(evidencePath, evidence);
888
+ const report = {
889
+ schema_version: 1,
890
+ generated_at_utc: (options.now ?? new Date()).toISOString(),
891
+ status: blockers.length > 0 ? 'blocked' : 'completed',
892
+ command: 'dataset classification apply',
893
+ input_path: path.resolve(options.inputPath),
894
+ decisions_path: path.resolve(options.decisionsPath),
895
+ out_path: path.resolve(options.outPath),
896
+ default_category_type: fallbackType,
897
+ counts: {
898
+ rows: rows.length,
899
+ decisions: rawDecisions.length,
900
+ applied: evidence.length,
901
+ blockers: blockers.length,
902
+ },
903
+ blockers,
904
+ files: {
905
+ classified_rows: path.resolve(options.outPath),
906
+ evidence: evidencePath,
907
+ report: reportPath,
908
+ },
909
+ };
910
+ writeJsonArtifact(reportPath, report);
911
+ return report;
912
+ }
913
+ export const __testInternals = {
914
+ buildNavigator,
915
+ classCode,
916
+ classificationContainer,
917
+ collectEntriesFromNode,
918
+ collectLocationRefKeysFromSchema,
919
+ collectLocationTargets,
920
+ constText,
921
+ currentClassification,
922
+ decisionMatchesRow,
923
+ decisionTargetPath,
924
+ isJsonSchemaFileName,
925
+ lastSchemaPropertyName,
926
+ loadEntries,
927
+ locationCodeFromPath,
928
+ locationTargetStringValue,
929
+ locationTargetKeys,
930
+ maybeWriteReport,
931
+ navigatorFor,
932
+ normalizeDecision,
933
+ normalizeDecisionType,
934
+ normalizePathFromClasses,
935
+ normalizePathFromDecision,
936
+ normalizeStructuredDecisions,
937
+ normalizeTargetPath,
938
+ normalizeType,
939
+ pathForCode,
940
+ prepareRows,
941
+ readDecisions,
942
+ resolveLocationTarget,
943
+ schemasDir,
944
+ setClassification,
945
+ toPathEntry,
946
+ };
947
+ //# sourceMappingURL=dataset-classification.js.map