@tiangong-lca/cli 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -2
- package/dist/src/cli.js +1327 -40
- package/dist/src/cli.js.map +1 -1
- package/dist/src/lib/dataset-author.js +100 -0
- package/dist/src/lib/dataset-author.js.map +1 -0
- package/dist/src/lib/dataset-bilingual.js +545 -0
- package/dist/src/lib/dataset-bilingual.js.map +1 -0
- package/dist/src/lib/dataset-contract.js +350 -0
- package/dist/src/lib/dataset-contract.js.map +1 -0
- package/dist/src/lib/dataset-evidence-search.js +636 -0
- package/dist/src/lib/dataset-evidence-search.js.map +1 -0
- package/dist/src/lib/dataset-import-lca.js +171 -0
- package/dist/src/lib/dataset-import-lca.js.map +1 -0
- package/dist/src/lib/dataset-remote-refresh.js +166 -0
- package/dist/src/lib/dataset-remote-refresh.js.map +1 -0
- package/dist/src/lib/dataset-remote-verify.js +543 -0
- package/dist/src/lib/dataset-remote-verify.js.map +1 -0
- package/dist/src/lib/dataset-validate.js +63 -7
- package/dist/src/lib/dataset-validate.js.map +1 -1
- package/dist/src/lib/flow-payload-validation.js +51 -0
- package/dist/src/lib/flow-payload-validation.js.map +1 -0
- package/dist/src/lib/flow-publish-reviewed-data.js +16 -0
- package/dist/src/lib/flow-publish-reviewed-data.js.map +1 -1
- package/dist/src/lib/flow-publish-version.js +182 -12
- package/dist/src/lib/flow-publish-version.js.map +1 -1
- package/dist/src/lib/identity-preflight.js +1021 -0
- package/dist/src/lib/identity-preflight.js.map +1 -0
- package/dist/src/lib/process-auto-build.js +147 -0
- package/dist/src/lib/process-auto-build.js.map +1 -1
- package/dist/src/lib/process-dedup-review.js +51 -0
- package/dist/src/lib/process-dedup-review.js.map +1 -1
- package/dist/src/lib/process-flow-build-plan.js +1071 -0
- package/dist/src/lib/process-flow-build-plan.js.map +1 -0
- package/dist/src/lib/process-payload-validation.js +14 -7
- package/dist/src/lib/process-payload-validation.js.map +1 -1
- package/dist/src/lib/process-publish-build.js +122 -4
- package/dist/src/lib/process-publish-build.js.map +1 -1
- package/dist/src/lib/process-refresh-references.js +19 -10
- package/dist/src/lib/process-refresh-references.js.map +1 -1
- package/dist/src/lib/process-required-fields.js +810 -0
- package/dist/src/lib/process-required-fields.js.map +1 -0
- package/dist/src/lib/process-save-draft-run.js +4 -1
- package/dist/src/lib/process-save-draft-run.js.map +1 -1
- package/dist/src/lib/publish.js +100 -0
- package/dist/src/lib/publish.js.map +1 -1
- package/dist/src/lib/review-flow.js +58 -0
- package/dist/src/lib/review-flow.js.map +1 -1
- package/dist/src/lib/review-process.js +150 -2
- package/dist/src/lib/review-process.js.map +1 -1
- package/dist/src/lib/runtime-rulesets.js +283 -0
- package/dist/src/lib/runtime-rulesets.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1071 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import * as tidasSdk from '@tiangong-lca/tidas-sdk';
|
|
4
|
+
import { writeJsonArtifact } from './artifacts.js';
|
|
5
|
+
import { CliError } from './errors.js';
|
|
6
|
+
import { cloneJson, detectDatasetKind, isRecord, unwrapDatasetPayload, } from './dataset-local.js';
|
|
7
|
+
import { readJsonInput } from './io.js';
|
|
8
|
+
import { normalizeIssuePath, validateSchemaWithDeepFallback, } from './tidas-sdk-validation.js';
|
|
9
|
+
const AUTO_DECISIONS = new Set([
|
|
10
|
+
'reuse',
|
|
11
|
+
'update_same_row',
|
|
12
|
+
'version_bump',
|
|
13
|
+
'create_new',
|
|
14
|
+
]);
|
|
15
|
+
const ALL_DECISIONS = new Set([
|
|
16
|
+
'reuse',
|
|
17
|
+
'update_same_row',
|
|
18
|
+
'version_bump',
|
|
19
|
+
'create_new',
|
|
20
|
+
'block_duplicate',
|
|
21
|
+
'manual_review',
|
|
22
|
+
]);
|
|
23
|
+
const AUTOMATIC_UNIT_OF_ANALYSIS_DECISIONS = new Set([
|
|
24
|
+
'ready_for_materialization',
|
|
25
|
+
'declared_unit_dataset',
|
|
26
|
+
]);
|
|
27
|
+
const ALL_UNIT_OF_ANALYSIS_DECISIONS = new Set([
|
|
28
|
+
'ready_for_materialization',
|
|
29
|
+
'declared_unit_dataset',
|
|
30
|
+
'blocked_until_scaling_evidence',
|
|
31
|
+
'manual_review',
|
|
32
|
+
]);
|
|
33
|
+
const SCHEMA_EXPORTS = {
|
|
34
|
+
flow: 'FlowSchema',
|
|
35
|
+
process: 'ProcessSchema',
|
|
36
|
+
};
|
|
37
|
+
const ENTITY_FACTORY_EXPORTS = {
|
|
38
|
+
flow: 'createFlow',
|
|
39
|
+
process: 'createProcess',
|
|
40
|
+
};
|
|
41
|
+
function nowIso(now = new Date()) {
|
|
42
|
+
return now.toISOString();
|
|
43
|
+
}
|
|
44
|
+
function requiredInputPath(inputPath) {
|
|
45
|
+
const normalized = inputPath.trim();
|
|
46
|
+
if (!normalized) {
|
|
47
|
+
throw new CliError('Missing required --input value.', {
|
|
48
|
+
code: 'BUILD_PLAN_INPUT_REQUIRED',
|
|
49
|
+
exitCode: 2,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return normalized;
|
|
53
|
+
}
|
|
54
|
+
function asObject(value, label) {
|
|
55
|
+
if (!isRecord(value)) {
|
|
56
|
+
throw new CliError(`${label} must be a JSON object.`, {
|
|
57
|
+
code: 'BUILD_PLAN_INVALID_INPUT',
|
|
58
|
+
exitCode: 2,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
function loadBuildPlan(inputPath, rawInput) {
|
|
64
|
+
const input = asObject(rawInput, 'build-plan input');
|
|
65
|
+
const nested = input.build_plan ??
|
|
66
|
+
input.buildPlan ??
|
|
67
|
+
input.process_build_plan ??
|
|
68
|
+
input.processBuildPlan ??
|
|
69
|
+
input.flow_build_plan ??
|
|
70
|
+
input.flowBuildPlan;
|
|
71
|
+
return nested === undefined ? input : asObject(nested, 'nested build plan');
|
|
72
|
+
}
|
|
73
|
+
function readBuildPlanInput(inputPath, rawInput) {
|
|
74
|
+
return loadBuildPlan(inputPath, rawInput === undefined ? readJsonInput(inputPath) : rawInput);
|
|
75
|
+
}
|
|
76
|
+
function textToken(value) {
|
|
77
|
+
if (typeof value === 'string') {
|
|
78
|
+
const trimmed = value.trim();
|
|
79
|
+
return trimmed || null;
|
|
80
|
+
}
|
|
81
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
82
|
+
return String(value);
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function valueAtPath(root, pathExpression) {
|
|
87
|
+
let current = root;
|
|
88
|
+
for (const segment of pathExpression.split('.')) {
|
|
89
|
+
if (!isRecord(current)) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
current = current[segment];
|
|
93
|
+
}
|
|
94
|
+
return current;
|
|
95
|
+
}
|
|
96
|
+
function firstValue(root, paths) {
|
|
97
|
+
for (const candidate of paths) {
|
|
98
|
+
const value = valueAtPath(root, candidate);
|
|
99
|
+
if (value !== undefined && value !== null) {
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
function firstToken(root, paths) {
|
|
106
|
+
for (const candidate of paths) {
|
|
107
|
+
const token = textToken(valueAtPath(root, candidate));
|
|
108
|
+
if (token) {
|
|
109
|
+
return token;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
function valueAsObject(root, paths) {
|
|
115
|
+
for (const candidate of paths) {
|
|
116
|
+
const value = valueAtPath(root, candidate);
|
|
117
|
+
if (isRecord(value)) {
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
function valueAsArray(root, paths) {
|
|
124
|
+
for (const candidate of paths) {
|
|
125
|
+
const value = valueAtPath(root, candidate);
|
|
126
|
+
if (Array.isArray(value)) {
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
function normalizeAmount(value, fallback = '1.0') {
|
|
133
|
+
const token = textToken(value);
|
|
134
|
+
if (!token) {
|
|
135
|
+
return fallback;
|
|
136
|
+
}
|
|
137
|
+
const numeric = Number(token);
|
|
138
|
+
return Number.isFinite(numeric) ? String(numeric) : token;
|
|
139
|
+
}
|
|
140
|
+
function normalizeVersion(value, fallback = '00.00.001') {
|
|
141
|
+
return textToken(value) ?? fallback;
|
|
142
|
+
}
|
|
143
|
+
function normalizeYear(value, fallback = 1970) {
|
|
144
|
+
const token = textToken(value);
|
|
145
|
+
if (!token) {
|
|
146
|
+
return fallback;
|
|
147
|
+
}
|
|
148
|
+
const year = Number.parseInt(token, 10);
|
|
149
|
+
return Number.isFinite(year) ? year : fallback;
|
|
150
|
+
}
|
|
151
|
+
function deterministicUuid(seed) {
|
|
152
|
+
const hex = createHash('sha256').update(seed).digest('hex');
|
|
153
|
+
const chars = hex.slice(0, 32).split('');
|
|
154
|
+
chars[12] = '5';
|
|
155
|
+
chars[16] = ((Number.parseInt(chars[16], 16) & 0x3) | 0x8).toString(16);
|
|
156
|
+
return `${chars.slice(0, 8).join('')}-${chars.slice(8, 12).join('')}-${chars
|
|
157
|
+
.slice(12, 16)
|
|
158
|
+
.join('')}-${chars.slice(16, 20).join('')}-${chars.slice(20, 32).join('')}`;
|
|
159
|
+
}
|
|
160
|
+
function uuidFromPlan(plan, paths, seed) {
|
|
161
|
+
const token = firstToken(plan, paths);
|
|
162
|
+
return token ?? deterministicUuid(seed);
|
|
163
|
+
}
|
|
164
|
+
function localizedText(text, lang = 'en') {
|
|
165
|
+
return { '#text': text, '@xml:lang': lang };
|
|
166
|
+
}
|
|
167
|
+
function multiLangFromValue(value, fallback, fallbackLang = 'en') {
|
|
168
|
+
if (Array.isArray(value)) {
|
|
169
|
+
const normalized = value
|
|
170
|
+
.map((entry) => {
|
|
171
|
+
if (isRecord(entry)) {
|
|
172
|
+
const text = textToken(entry['#text'] ?? entry.text ?? entry.value);
|
|
173
|
+
if (!text) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return localizedText(text, textToken(entry['@xml:lang'] ?? entry.lang) ?? fallbackLang);
|
|
177
|
+
}
|
|
178
|
+
const text = textToken(entry);
|
|
179
|
+
return text ? localizedText(text, fallbackLang) : null;
|
|
180
|
+
})
|
|
181
|
+
.filter((entry) => Boolean(entry));
|
|
182
|
+
if (normalized.length) {
|
|
183
|
+
return normalized;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (isRecord(value)) {
|
|
187
|
+
const text = textToken(value['#text'] ?? value.text ?? value.value);
|
|
188
|
+
if (text) {
|
|
189
|
+
return [localizedText(text, textToken(value['@xml:lang'] ?? value.lang) ?? fallbackLang)];
|
|
190
|
+
}
|
|
191
|
+
const en = textToken(value.en);
|
|
192
|
+
const zh = textToken(value.zh);
|
|
193
|
+
const rows = [en ? localizedText(en, 'en') : null, zh ? localizedText(zh, 'zh') : null].filter((entry) => Boolean(entry));
|
|
194
|
+
if (rows.length) {
|
|
195
|
+
return rows;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const token = textToken(value);
|
|
199
|
+
return [localizedText(token ?? fallback, fallbackLang)];
|
|
200
|
+
}
|
|
201
|
+
function firstMultiLang(plan, paths, fallback) {
|
|
202
|
+
for (const candidate of paths) {
|
|
203
|
+
const value = valueAtPath(plan, candidate);
|
|
204
|
+
if (value !== undefined && value !== null) {
|
|
205
|
+
return multiLangFromValue(value, fallback);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return multiLangFromValue(undefined, fallback);
|
|
209
|
+
}
|
|
210
|
+
function globalReference(options) {
|
|
211
|
+
const version = normalizeVersion(options.version, '00.00.000');
|
|
212
|
+
return {
|
|
213
|
+
'@type': options.type,
|
|
214
|
+
'@refObjectId': options.refObjectId,
|
|
215
|
+
'@version': version,
|
|
216
|
+
'@uri': options.uri ?? `../${options.type.replaceAll(' ', '-')}/${options.refObjectId}.xml`,
|
|
217
|
+
'common:shortDescription': localizedText(options.shortDescription),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function evidenceSourceReference(plan) {
|
|
221
|
+
const evidence = valueAsObject(plan, ['evidence_manifest', 'evidenceManifest']) ?? {};
|
|
222
|
+
const sources = Array.isArray(evidence.sources) ? evidence.sources : [];
|
|
223
|
+
const firstSource = sources.find(isRecord);
|
|
224
|
+
const sourceId = textToken(firstSource?.id ?? firstSource?.source_id ?? firstSource?.ref_object_id) ??
|
|
225
|
+
deterministicUuid(`${JSON.stringify(plan)}:source`);
|
|
226
|
+
const sourceVersion = textToken(firstSource?.version) ?? '00.00.000';
|
|
227
|
+
const shortDescription = textToken(firstSource?.title ?? firstSource?.name ?? firstSource?.short_description) ??
|
|
228
|
+
'Build plan evidence source';
|
|
229
|
+
return globalReference({
|
|
230
|
+
type: 'source data set',
|
|
231
|
+
refObjectId: sourceId,
|
|
232
|
+
version: sourceVersion,
|
|
233
|
+
uri: textToken(firstSource?.uri),
|
|
234
|
+
shortDescription,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function contactReference(plan, role) {
|
|
238
|
+
const explicit = valueAsObject(plan, [
|
|
239
|
+
`administrative_information.${role}`,
|
|
240
|
+
`administrativeInformation.${role}`,
|
|
241
|
+
]);
|
|
242
|
+
const id = textToken(explicit?.id ?? explicit?.ref_object_id ?? explicit?.refObjectId) ??
|
|
243
|
+
deterministicUuid(`${JSON.stringify(plan)}:${role}`);
|
|
244
|
+
return globalReference({
|
|
245
|
+
type: 'contact data set',
|
|
246
|
+
refObjectId: id,
|
|
247
|
+
version: textToken(explicit?.version) ?? '00.00.000',
|
|
248
|
+
uri: textToken(explicit?.uri),
|
|
249
|
+
shortDescription: textToken(explicit?.short_description ?? explicit?.shortDescription ?? explicit?.name) ??
|
|
250
|
+
`Build plan ${role}`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
function complianceReference(plan) {
|
|
254
|
+
const explicit = valueAsObject(plan, [
|
|
255
|
+
'compliance_reference',
|
|
256
|
+
'complianceReference',
|
|
257
|
+
'administrative_information.compliance_reference',
|
|
258
|
+
'administrativeInformation.complianceReference',
|
|
259
|
+
]);
|
|
260
|
+
return globalReference({
|
|
261
|
+
type: 'source data set',
|
|
262
|
+
refObjectId: textToken(explicit?.id ?? explicit?.ref_object_id ?? explicit?.refObjectId) ??
|
|
263
|
+
deterministicUuid(`${JSON.stringify(plan)}:compliance`),
|
|
264
|
+
version: textToken(explicit?.version) ?? '00.00.000',
|
|
265
|
+
uri: textToken(explicit?.uri),
|
|
266
|
+
shortDescription: textToken(explicit?.short_description ?? explicit?.shortDescription ?? explicit?.name) ??
|
|
267
|
+
'Build plan compliance system',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
function dataSetFormatReference(plan) {
|
|
271
|
+
const explicit = valueAsObject(plan, [
|
|
272
|
+
'format_reference',
|
|
273
|
+
'formatReference',
|
|
274
|
+
'administrative_information.format_reference',
|
|
275
|
+
'administrativeInformation.formatReference',
|
|
276
|
+
]);
|
|
277
|
+
return globalReference({
|
|
278
|
+
type: 'source data set',
|
|
279
|
+
refObjectId: textToken(explicit?.id ?? explicit?.ref_object_id ?? explicit?.refObjectId) ??
|
|
280
|
+
deterministicUuid('tiangong-lca-tidas-format-reference'),
|
|
281
|
+
version: textToken(explicit?.version) ?? '00.00.000',
|
|
282
|
+
uri: textToken(explicit?.uri),
|
|
283
|
+
shortDescription: textToken(explicit?.short_description ?? explicit?.shortDescription ?? explicit?.name) ??
|
|
284
|
+
'TIDAS / ILCD data set format',
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function classificationClasses(plan, kind) {
|
|
288
|
+
const pathValues = valueAsArray(plan, ['target.classification_path', 'target.classificationPath']).length > 0
|
|
289
|
+
? valueAsArray(plan, ['target.classification_path', 'target.classificationPath'])
|
|
290
|
+
: valueAsArray(plan, ['classification_path', 'classificationPath']);
|
|
291
|
+
const labels = pathValues
|
|
292
|
+
.map((entry) => textToken(entry))
|
|
293
|
+
.filter((entry) => Boolean(entry));
|
|
294
|
+
const fallback = kind === 'process'
|
|
295
|
+
? ['Technosphere', 'Unspecified sector', 'Unspecified activity', 'Unspecified process']
|
|
296
|
+
: ['Technosphere flows', 'Product flows', 'Unspecified category', 'Unspecified flow'];
|
|
297
|
+
const requiredCount = kind === 'process' ? 4 : Math.max(labels.length, 1);
|
|
298
|
+
const values = Array.from({ length: requiredCount }, (_, index) => labels[index] ?? fallback[index]);
|
|
299
|
+
return values.map((label, index) => ({
|
|
300
|
+
'@level': String(index),
|
|
301
|
+
'@classId': deterministicUuid(`${kind}:classification:${index}:${label}`),
|
|
302
|
+
'#text': label,
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
function normalizeFlowType(value) {
|
|
306
|
+
const lower = (value ?? '').toLowerCase();
|
|
307
|
+
if (lower.includes('elementary')) {
|
|
308
|
+
return 'Elementary flow';
|
|
309
|
+
}
|
|
310
|
+
if (lower.includes('waste')) {
|
|
311
|
+
return 'Waste flow';
|
|
312
|
+
}
|
|
313
|
+
return 'Product flow';
|
|
314
|
+
}
|
|
315
|
+
function normalizeProcessType(value) {
|
|
316
|
+
const allowed = new Set([
|
|
317
|
+
'Unit process, single operation',
|
|
318
|
+
'Unit process, black box',
|
|
319
|
+
'LCI result',
|
|
320
|
+
'Partly terminated system',
|
|
321
|
+
'Avoided product system',
|
|
322
|
+
]);
|
|
323
|
+
return value && allowed.has(value)
|
|
324
|
+
? value
|
|
325
|
+
: 'Unit process, single operation';
|
|
326
|
+
}
|
|
327
|
+
function flowPropertyReference(plan) {
|
|
328
|
+
const propertyName = firstToken(plan, [
|
|
329
|
+
'flow_property_plan.reference_property',
|
|
330
|
+
'flowPropertyPlan.referenceProperty',
|
|
331
|
+
]) ?? 'Reference flow property';
|
|
332
|
+
const propertyId = firstToken(plan, [
|
|
333
|
+
'flow_property_plan.reference_property_id',
|
|
334
|
+
'flowPropertyPlan.referencePropertyId',
|
|
335
|
+
]) ??
|
|
336
|
+
(propertyName.toLowerCase() === 'mass'
|
|
337
|
+
? '93a60a56-a3c8-11da-a746-0800200b9a66'
|
|
338
|
+
: deterministicUuid(`flow-property:${propertyName}`));
|
|
339
|
+
return globalReference({
|
|
340
|
+
type: 'flow property data set',
|
|
341
|
+
refObjectId: propertyId,
|
|
342
|
+
version: firstToken(plan, [
|
|
343
|
+
'flow_property_plan.reference_property_version',
|
|
344
|
+
'flowPropertyPlan.referencePropertyVersion',
|
|
345
|
+
]) ?? '00.00.000',
|
|
346
|
+
uri: firstToken(plan, [
|
|
347
|
+
'flow_property_plan.reference_property_uri',
|
|
348
|
+
'flowPropertyPlan.referencePropertyUri',
|
|
349
|
+
]),
|
|
350
|
+
shortDescription: propertyName,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function referenceFlowRef(plan) {
|
|
354
|
+
const referenceFlowId = firstToken(plan, [
|
|
355
|
+
'quantitative_reference_plan.reference_flow_id',
|
|
356
|
+
'quantitativeReferencePlan.referenceFlowId',
|
|
357
|
+
'target.intended_reference_flow',
|
|
358
|
+
]) ?? deterministicUuid(`${JSON.stringify(plan)}:reference-flow`);
|
|
359
|
+
return globalReference({
|
|
360
|
+
type: 'flow data set',
|
|
361
|
+
refObjectId: referenceFlowId,
|
|
362
|
+
version: firstToken(plan, [
|
|
363
|
+
'quantitative_reference_plan.reference_flow_version',
|
|
364
|
+
'quantitativeReferencePlan.referenceFlowVersion',
|
|
365
|
+
]) ?? '00.00.000',
|
|
366
|
+
uri: firstToken(plan, [
|
|
367
|
+
'quantitative_reference_plan.reference_flow_uri',
|
|
368
|
+
'quantitativeReferencePlan.referenceFlowUri',
|
|
369
|
+
]),
|
|
370
|
+
shortDescription: firstToken(plan, [
|
|
371
|
+
'quantitative_reference_plan.reference_flow_name',
|
|
372
|
+
'quantitativeReferencePlan.referenceFlowName',
|
|
373
|
+
'name_plan.functional_unit_flow_properties',
|
|
374
|
+
'namePlan.functionalUnitFlowProperties',
|
|
375
|
+
]) ?? 'Quantitative reference flow',
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
function buildAnnualSupply(plan, referenceExchange) {
|
|
379
|
+
const explicit = firstValue(plan, [
|
|
380
|
+
'required_fields.annualSupplyOrProductionVolume',
|
|
381
|
+
'requiredFields.annualSupplyOrProductionVolume',
|
|
382
|
+
'authoring.required_fields.annualSupplyOrProductionVolume',
|
|
383
|
+
'authoring.requiredFields.annualSupplyOrProductionVolume',
|
|
384
|
+
'modelling_and_validation.annualSupplyOrProductionVolume',
|
|
385
|
+
'modellingAndValidation.annualSupplyOrProductionVolume',
|
|
386
|
+
]);
|
|
387
|
+
if (explicit !== undefined && explicit !== null) {
|
|
388
|
+
return multiLangFromValue(explicit, String(explicit));
|
|
389
|
+
}
|
|
390
|
+
const amount = textToken(referenceExchange.meanAmount) ??
|
|
391
|
+
textToken(referenceExchange.resultingAmount) ??
|
|
392
|
+
'1.0';
|
|
393
|
+
const unit = firstToken(plan, [
|
|
394
|
+
'quantitative_reference_plan.reference_unit',
|
|
395
|
+
'quantitativeReferencePlan.referenceUnit',
|
|
396
|
+
'flow_property_plan.reference_unit',
|
|
397
|
+
'flowPropertyPlan.referenceUnit',
|
|
398
|
+
]) ?? 'unit';
|
|
399
|
+
return [localizedText(`${amount} ${unit}/year`, 'en')];
|
|
400
|
+
}
|
|
401
|
+
function normalizeExchangeDirection(value) {
|
|
402
|
+
return value === 'Input' ? 'Input' : 'Output';
|
|
403
|
+
}
|
|
404
|
+
function exchangeFromPlan(plan, entry, index) {
|
|
405
|
+
if (!isRecord(entry)) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
const flowId = textToken(entry.flow_id ?? entry.flowId ?? entry.reference_flow_id ?? entry.referenceFlowId) ??
|
|
409
|
+
deterministicUuid(`${JSON.stringify(plan)}:exchange:${index}`);
|
|
410
|
+
const internalId = textToken(entry.internal_id ?? entry.internalId ?? entry['@dataSetInternalID']) ??
|
|
411
|
+
String(index + 1);
|
|
412
|
+
const meanAmount = normalizeAmount(entry.mean_amount ?? entry.meanAmount);
|
|
413
|
+
return {
|
|
414
|
+
'@dataSetInternalID': internalId,
|
|
415
|
+
referenceToFlowDataSet: globalReference({
|
|
416
|
+
type: 'flow data set',
|
|
417
|
+
refObjectId: flowId,
|
|
418
|
+
version: normalizeVersion(entry.version, '00.00.000'),
|
|
419
|
+
uri: textToken(entry.uri),
|
|
420
|
+
shortDescription: textToken(entry.short_description ?? entry.shortDescription ?? entry.name) ??
|
|
421
|
+
`Exchange flow ${internalId}`,
|
|
422
|
+
}),
|
|
423
|
+
exchangeDirection: normalizeExchangeDirection(textToken(entry.direction ?? entry.exchangeDirection)),
|
|
424
|
+
meanAmount,
|
|
425
|
+
resultingAmount: normalizeAmount(entry.resulting_amount ?? entry.resultingAmount, meanAmount),
|
|
426
|
+
dataDerivationTypeStatus: textToken(entry.data_derivation_type_status ?? entry.dataDerivationTypeStatus) ?? 'Estimated',
|
|
427
|
+
quantitativeReference: Boolean(entry.quantitative_reference ?? entry.quantitativeReference),
|
|
428
|
+
referencesToDataSource: {
|
|
429
|
+
referenceToDataSource: evidenceSourceReference(plan),
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function exchangePlanEntries(plan) {
|
|
434
|
+
return valueAsArray(plan, ['exchange_plan.exchanges', 'exchangePlan.exchanges'])
|
|
435
|
+
.map((entry, index) => exchangeFromPlan(plan, entry, index))
|
|
436
|
+
.filter((entry) => Boolean(entry));
|
|
437
|
+
}
|
|
438
|
+
function referenceExchange(plan) {
|
|
439
|
+
const internalId = firstToken(plan, [
|
|
440
|
+
'quantitative_reference_plan.reference_flow_internal_id',
|
|
441
|
+
'quantitativeReferencePlan.referenceFlowInternalId',
|
|
442
|
+
]) ?? '1';
|
|
443
|
+
const meanAmount = firstToken(plan, [
|
|
444
|
+
'quantitative_reference_plan.mean_amount',
|
|
445
|
+
'quantitativeReferencePlan.meanAmount',
|
|
446
|
+
'quantitative_reference_plan.resulting_amount',
|
|
447
|
+
'quantitativeReferencePlan.resultingAmount',
|
|
448
|
+
]) ?? '1.0';
|
|
449
|
+
const resultingAmount = firstToken(plan, [
|
|
450
|
+
'quantitative_reference_plan.resulting_amount',
|
|
451
|
+
'quantitativeReferencePlan.resultingAmount',
|
|
452
|
+
]) ?? meanAmount;
|
|
453
|
+
return {
|
|
454
|
+
'@dataSetInternalID': internalId,
|
|
455
|
+
referenceToFlowDataSet: referenceFlowRef(plan),
|
|
456
|
+
exchangeDirection: 'Output',
|
|
457
|
+
meanAmount: normalizeAmount(meanAmount),
|
|
458
|
+
resultingAmount: normalizeAmount(resultingAmount, normalizeAmount(meanAmount)),
|
|
459
|
+
dataDerivationTypeStatus: firstToken(plan, [
|
|
460
|
+
'quantitative_reference_plan.data_derivation_type_status',
|
|
461
|
+
'quantitativeReferencePlan.dataDerivationTypeStatus',
|
|
462
|
+
]) ?? 'Estimated',
|
|
463
|
+
quantitativeReference: true,
|
|
464
|
+
referencesToDataSource: {
|
|
465
|
+
referenceToDataSource: evidenceSourceReference(plan),
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function buildCanonicalFlowPayload(plan, inputPath) {
|
|
470
|
+
const baseName = firstToken(plan, ['name_plan.base_name', 'namePlan.baseName']) ?? 'Unnamed flow';
|
|
471
|
+
const flowId = uuidFromPlan(plan, ['target.uuid', 'target.id', 'identity_decision.target_id', 'identityDecision.targetId'], `flow:${baseName}:${inputPath}`);
|
|
472
|
+
const version = normalizeVersion(firstToken(plan, [
|
|
473
|
+
'target.version',
|
|
474
|
+
'publication.version',
|
|
475
|
+
'administrative_information.version',
|
|
476
|
+
]));
|
|
477
|
+
const propertyMean = firstToken(plan, ['flow_property_plan.mean_value', 'flowPropertyPlan.meanValue']) ?? '1.0';
|
|
478
|
+
const flowType = normalizeFlowType(firstToken(plan, [
|
|
479
|
+
'target.flow_type',
|
|
480
|
+
'target.flowType',
|
|
481
|
+
'modelling_and_validation.typeOfDataSet',
|
|
482
|
+
]));
|
|
483
|
+
const location = firstToken(plan, ['target.geography', 'target.location']);
|
|
484
|
+
return {
|
|
485
|
+
flowDataSet: {
|
|
486
|
+
'@xmlns': 'http://lca.jrc.it/ILCD/Flow',
|
|
487
|
+
'@xmlns:common': 'http://lca.jrc.it/ILCD/Common',
|
|
488
|
+
'@xmlns:ecn': 'http://eplca.jrc.ec.europa.eu/ILCD/Extensions/2018/ECNumber',
|
|
489
|
+
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
490
|
+
'@version': '1.1',
|
|
491
|
+
'@locations': '../ILCDLocations.xml',
|
|
492
|
+
'@xsi:schemaLocation': 'http://lca.jrc.it/ILCD/Flow ../../schemas/ILCD_FlowDataSet.xsd',
|
|
493
|
+
flowInformation: {
|
|
494
|
+
dataSetInformation: {
|
|
495
|
+
'common:UUID': flowId,
|
|
496
|
+
name: {
|
|
497
|
+
baseName: firstMultiLang(plan, ['name_plan.base_name', 'namePlan.baseName'], baseName),
|
|
498
|
+
treatmentStandardsRoutes: firstMultiLang(plan, ['name_plan.treatment_standards_routes', 'namePlan.treatmentStandardsRoutes'], 'Reference flow'),
|
|
499
|
+
mixAndLocationTypes: firstMultiLang(plan, ['name_plan.mix_and_location_types', 'namePlan.mixAndLocationTypes'], location ?? 'Global'),
|
|
500
|
+
},
|
|
501
|
+
classificationInformation: {
|
|
502
|
+
'common:classification': {
|
|
503
|
+
'common:class': classificationClasses(plan, 'flow'),
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
...(firstToken(plan, ['target.cas_number', 'target.CASNumber'])
|
|
507
|
+
? { CASNumber: firstToken(plan, ['target.cas_number', 'target.CASNumber']) }
|
|
508
|
+
: {}),
|
|
509
|
+
'common:generalComment': firstMultiLang(plan, ['target.general_comment', 'target.generalComment', 'evidence_manifest.summary'], `Flow materialized from build plan ${inputPath}.`),
|
|
510
|
+
},
|
|
511
|
+
quantitativeReference: {
|
|
512
|
+
referenceToReferenceFlowProperty: '0',
|
|
513
|
+
},
|
|
514
|
+
...(location ? { geography: { locationOfSupply: location } } : {}),
|
|
515
|
+
},
|
|
516
|
+
modellingAndValidation: {
|
|
517
|
+
LCIMethod: {
|
|
518
|
+
typeOfDataSet: flowType,
|
|
519
|
+
},
|
|
520
|
+
complianceDeclarations: {
|
|
521
|
+
compliance: {
|
|
522
|
+
'common:referenceToComplianceSystem': complianceReference(plan),
|
|
523
|
+
'common:approvalOfOverallCompliance': 'Not defined',
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
administrativeInformation: {
|
|
528
|
+
dataEntryBy: {
|
|
529
|
+
'common:timeStamp': firstToken(plan, [
|
|
530
|
+
'administrative_information.time_stamp',
|
|
531
|
+
'administrativeInformation.timeStamp',
|
|
532
|
+
]) ?? '1970-01-01T00:00:00.000Z',
|
|
533
|
+
'common:referenceToDataSetFormat': dataSetFormatReference(plan),
|
|
534
|
+
},
|
|
535
|
+
publicationAndOwnership: {
|
|
536
|
+
'common:dataSetVersion': version,
|
|
537
|
+
'common:permanentDataSetURI': firstToken(plan, ['target.permanent_uri', 'target.permanentDataSetURI']) ??
|
|
538
|
+
`https://data.tiangong.earth/flows/${flowId}.xml`,
|
|
539
|
+
'common:referenceToOwnershipOfDataSet': contactReference(plan, 'owner'),
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
flowProperties: {
|
|
543
|
+
flowProperty: {
|
|
544
|
+
'@dataSetInternalID': '0',
|
|
545
|
+
referenceToFlowPropertyDataSet: flowPropertyReference(plan),
|
|
546
|
+
meanValue: normalizeAmount(propertyMean),
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function buildCanonicalProcessPayload(plan, inputPath) {
|
|
553
|
+
const baseName = firstToken(plan, ['name_plan.base_name', 'namePlan.baseName']) ?? 'Unnamed process';
|
|
554
|
+
const processId = uuidFromPlan(plan, ['target.uuid', 'target.id', 'identity_decision.target_id', 'identityDecision.targetId'], `process:${baseName}:${inputPath}`);
|
|
555
|
+
const location = firstToken(plan, ['target.geography', 'target.location']) ?? 'GLO';
|
|
556
|
+
const reference = referenceExchange(plan);
|
|
557
|
+
const exchangeEntries = exchangePlanEntries(plan);
|
|
558
|
+
const exchanges = [reference, ...exchangeEntries.filter((entry) => !entry.quantitativeReference)];
|
|
559
|
+
const annualSupply = buildAnnualSupply(plan, reference);
|
|
560
|
+
const sourceRef = evidenceSourceReference(plan);
|
|
561
|
+
return {
|
|
562
|
+
processDataSet: {
|
|
563
|
+
'@xmlns': 'http://lca.jrc.it/ILCD/Process',
|
|
564
|
+
'@xmlns:common': 'http://lca.jrc.it/ILCD/Common',
|
|
565
|
+
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
566
|
+
'@version': '1.1',
|
|
567
|
+
'@locations': '../ILCDLocations.xml',
|
|
568
|
+
'@xsi:schemaLocation': 'http://lca.jrc.it/ILCD/Process ../../schemas/ILCD_ProcessDataSet.xsd',
|
|
569
|
+
processInformation: {
|
|
570
|
+
dataSetInformation: {
|
|
571
|
+
'common:UUID': processId,
|
|
572
|
+
name: {
|
|
573
|
+
baseName: firstMultiLang(plan, ['name_plan.base_name', 'namePlan.baseName'], baseName),
|
|
574
|
+
treatmentStandardsRoutes: firstMultiLang(plan, ['name_plan.treatment_standards_routes', 'namePlan.treatmentStandardsRoutes'], firstToken(plan, ['target.technology_route', 'target.technologyRoute']) ??
|
|
575
|
+
'Technology route documented in build plan'),
|
|
576
|
+
mixAndLocationTypes: firstMultiLang(plan, ['name_plan.mix_and_location_types', 'namePlan.mixAndLocationTypes'], location),
|
|
577
|
+
functionalUnitFlowProperties: firstMultiLang(plan, [
|
|
578
|
+
'name_plan.functional_unit_flow_properties',
|
|
579
|
+
'namePlan.functionalUnitFlowProperties',
|
|
580
|
+
], firstToken(plan, [
|
|
581
|
+
'quantitative_reference_plan.reference_unit',
|
|
582
|
+
'quantitativeReferencePlan.referenceUnit',
|
|
583
|
+
]) ?? 'reference unit'),
|
|
584
|
+
},
|
|
585
|
+
classificationInformation: {
|
|
586
|
+
'common:classification': {
|
|
587
|
+
'common:class': classificationClasses(plan, 'process'),
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
'common:generalComment': firstMultiLang(plan, ['target.general_comment', 'target.generalComment', 'evidence_manifest.summary'], `Process materialized from build plan ${inputPath}.`),
|
|
591
|
+
},
|
|
592
|
+
quantitativeReference: {
|
|
593
|
+
'@type': 'Reference flow(s)',
|
|
594
|
+
referenceToReferenceFlow: String(reference['@dataSetInternalID']),
|
|
595
|
+
},
|
|
596
|
+
time: {
|
|
597
|
+
'common:referenceYear': normalizeYear(firstToken(plan, [
|
|
598
|
+
'target.reference_year',
|
|
599
|
+
'target.referenceYear',
|
|
600
|
+
'time.reference_year',
|
|
601
|
+
])),
|
|
602
|
+
'common:timeRepresentativenessDescription': firstMultiLang(plan, ['time.description', 'time.timeRepresentativenessDescription'], 'Reference year documented in build plan evidence.'),
|
|
603
|
+
},
|
|
604
|
+
geography: {
|
|
605
|
+
locationOfOperationSupplyOrProduction: {
|
|
606
|
+
'@location': location,
|
|
607
|
+
descriptionOfRestrictions: firstMultiLang(plan, ['target.geography_description', 'target.geographyDescription'], `Operation location: ${location}.`),
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
technology: {
|
|
611
|
+
technologyDescriptionAndIncludedProcesses: firstMultiLang(plan, ['technology.description', 'technology.technologyDescriptionAndIncludedProcesses'], firstToken(plan, ['target.technology_route', 'target.technologyRoute']) ??
|
|
612
|
+
'Technology route documented in build plan evidence.'),
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
modellingAndValidation: {
|
|
616
|
+
LCIMethodAndAllocation: {
|
|
617
|
+
typeOfDataSet: normalizeProcessType(firstToken(plan, [
|
|
618
|
+
'modelling_and_validation.type_of_dataset',
|
|
619
|
+
'modellingAndValidation.typeOfDataSet',
|
|
620
|
+
])),
|
|
621
|
+
LCIMethodPrinciple: firstToken(plan, [
|
|
622
|
+
'modelling_and_validation.lci_method_principle',
|
|
623
|
+
'modellingAndValidation.lciMethodPrinciple',
|
|
624
|
+
]) ?? 'Attributional',
|
|
625
|
+
},
|
|
626
|
+
dataSourcesTreatmentAndRepresentativeness: {
|
|
627
|
+
dataCutOffAndCompletenessPrinciples: firstMultiLang(plan, ['modelling_and_validation.data_cutoff', 'modellingAndValidation.dataCutoff'], 'Cut-off and completeness principles are documented in the build plan evidence.'),
|
|
628
|
+
referenceToDataSource: sourceRef,
|
|
629
|
+
annualSupplyOrProductionVolume: annualSupply,
|
|
630
|
+
},
|
|
631
|
+
validation: {
|
|
632
|
+
review: {
|
|
633
|
+
'@type': 'Not reviewed',
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
complianceDeclarations: {
|
|
637
|
+
compliance: {
|
|
638
|
+
'common:referenceToComplianceSystem': complianceReference(plan),
|
|
639
|
+
'common:approvalOfOverallCompliance': 'Not defined',
|
|
640
|
+
'common:nomenclatureCompliance': 'Not defined',
|
|
641
|
+
'common:methodologicalCompliance': 'Not defined',
|
|
642
|
+
'common:reviewCompliance': 'Not defined',
|
|
643
|
+
'common:documentationCompliance': 'Not defined',
|
|
644
|
+
'common:qualityCompliance': 'Not defined',
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
administrativeInformation: {
|
|
649
|
+
'common:commissionerAndGoal': {
|
|
650
|
+
'common:referenceToCommissioner': contactReference(plan, 'commissioner'),
|
|
651
|
+
'common:intendedApplications': firstMultiLang(plan, [
|
|
652
|
+
'administrative_information.intended_applications',
|
|
653
|
+
'administrativeInformation.intendedApplications',
|
|
654
|
+
], 'Automated LCA data production draft for expert review.'),
|
|
655
|
+
},
|
|
656
|
+
dataEntryBy: {
|
|
657
|
+
'common:timeStamp': firstToken(plan, [
|
|
658
|
+
'administrative_information.time_stamp',
|
|
659
|
+
'administrativeInformation.timeStamp',
|
|
660
|
+
]) ?? '1970-01-01T00:00:00.000Z',
|
|
661
|
+
'common:referenceToDataSetFormat': dataSetFormatReference(plan),
|
|
662
|
+
'common:referenceToPersonOrEntityEnteringTheData': contactReference(plan, 'data_entry'),
|
|
663
|
+
},
|
|
664
|
+
publicationAndOwnership: {
|
|
665
|
+
'common:dataSetVersion': normalizeVersion(firstToken(plan, [
|
|
666
|
+
'target.version',
|
|
667
|
+
'publication.version',
|
|
668
|
+
'administrative_information.version',
|
|
669
|
+
])),
|
|
670
|
+
'common:permanentDataSetURI': firstToken(plan, ['target.permanent_uri', 'target.permanentDataSetURI']) ??
|
|
671
|
+
`https://data.tiangong.earth/processes/${processId}.xml`,
|
|
672
|
+
'common:referenceToOwnershipOfDataSet': contactReference(plan, 'owner'),
|
|
673
|
+
'common:copyright': 'false',
|
|
674
|
+
'common:licenseType': 'Free of charge for all users and uses',
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
exchanges: {
|
|
678
|
+
exchange: exchanges,
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function isNonEmptyArray(value) {
|
|
684
|
+
return Array.isArray(value) && value.length > 0;
|
|
685
|
+
}
|
|
686
|
+
function pathIsSatisfied(root, paths) {
|
|
687
|
+
const value = firstValue(root, paths);
|
|
688
|
+
if (isNonEmptyArray(value)) {
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
if (isRecord(value)) {
|
|
692
|
+
return Object.keys(value).length > 0;
|
|
693
|
+
}
|
|
694
|
+
return Boolean(textToken(value));
|
|
695
|
+
}
|
|
696
|
+
function evidenceBindingPaths(plan) {
|
|
697
|
+
const evidence = firstValue(plan, ['evidence_manifest', 'evidenceManifest']);
|
|
698
|
+
const bindings = isRecord(evidence)
|
|
699
|
+
? firstValue(evidence, ['field_bindings', 'fieldBindings'])
|
|
700
|
+
: undefined;
|
|
701
|
+
const rows = Array.isArray(bindings) ? bindings : [];
|
|
702
|
+
return new Set(rows
|
|
703
|
+
.map((row) => isRecord(row) ? textToken(row.field_path ?? row.path ?? row.field ?? row.fieldPath) : null)
|
|
704
|
+
.filter((rowPath) => Boolean(rowPath)));
|
|
705
|
+
}
|
|
706
|
+
function evidenceSourcesPresent(plan) {
|
|
707
|
+
const evidence = firstValue(plan, ['evidence_manifest', 'evidenceManifest']);
|
|
708
|
+
const sources = isRecord(evidence) ? firstValue(evidence, ['sources']) : undefined;
|
|
709
|
+
return isNonEmptyArray(sources);
|
|
710
|
+
}
|
|
711
|
+
function decisionFromPlan(plan) {
|
|
712
|
+
const raw = firstToken(plan, ['identity_decision.decision', 'identityDecision.decision', 'decision']) ??
|
|
713
|
+
null;
|
|
714
|
+
return raw && ALL_DECISIONS.has(raw) ? raw : null;
|
|
715
|
+
}
|
|
716
|
+
function unitOfAnalysisFromPlan(plan) {
|
|
717
|
+
return valueAsObject(plan, ['unit_of_analysis', 'unitOfAnalysis']);
|
|
718
|
+
}
|
|
719
|
+
function unitOfAnalysisDecisionFromArtifact(artifact) {
|
|
720
|
+
const raw = textToken(artifact.decision);
|
|
721
|
+
return raw && ALL_UNIT_OF_ANALYSIS_DECISIONS.has(raw)
|
|
722
|
+
? raw
|
|
723
|
+
: null;
|
|
724
|
+
}
|
|
725
|
+
function buildPlanRuleset(plan, kind) {
|
|
726
|
+
const ruleset = firstValue(plan, ['ruleset']);
|
|
727
|
+
const id = firstToken(plan, ['ruleset_id', 'rulesetId']) ??
|
|
728
|
+
(isRecord(ruleset) ? textToken(ruleset.id) : null) ??
|
|
729
|
+
`${kind}-authoring/strict`;
|
|
730
|
+
const version = firstToken(plan, ['ruleset_version', 'rulesetVersion']) ??
|
|
731
|
+
(isRecord(ruleset) ? textToken(ruleset.version) : null) ??
|
|
732
|
+
'1';
|
|
733
|
+
return { id, version };
|
|
734
|
+
}
|
|
735
|
+
function requiredFieldSpecs(kind) {
|
|
736
|
+
const common = [
|
|
737
|
+
{ path: 'target', aliases: ['target'] },
|
|
738
|
+
{
|
|
739
|
+
path: 'identity_decision.decision',
|
|
740
|
+
aliases: ['identity_decision.decision', 'identityDecision.decision', 'decision'],
|
|
741
|
+
},
|
|
742
|
+
{ path: 'unit_of_analysis', aliases: ['unit_of_analysis', 'unitOfAnalysis'] },
|
|
743
|
+
{ path: 'name_plan.base_name', aliases: ['name_plan.base_name', 'namePlan.baseName'] },
|
|
744
|
+
];
|
|
745
|
+
const process = [
|
|
746
|
+
{ path: 'target.geography', aliases: ['target.geography', 'target.location'] },
|
|
747
|
+
{
|
|
748
|
+
path: 'target.technology_route',
|
|
749
|
+
aliases: ['target.technology_route', 'target.technologyRoute'],
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
path: 'quantitative_reference_plan.reference_flow_id',
|
|
753
|
+
aliases: [
|
|
754
|
+
'quantitative_reference_plan.reference_flow_id',
|
|
755
|
+
'quantitativeReferencePlan.referenceFlowId',
|
|
756
|
+
'target.intended_reference_flow',
|
|
757
|
+
],
|
|
758
|
+
},
|
|
759
|
+
];
|
|
760
|
+
const flow = [
|
|
761
|
+
{ path: 'target.flow_type', aliases: ['target.flow_type', 'target.flowType'] },
|
|
762
|
+
{
|
|
763
|
+
path: 'flow_property_plan.reference_property',
|
|
764
|
+
aliases: ['flow_property_plan.reference_property', 'flowPropertyPlan.referenceProperty'],
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
path: 'flow_property_plan.reference_unit',
|
|
768
|
+
aliases: ['flow_property_plan.reference_unit', 'flowPropertyPlan.referenceUnit'],
|
|
769
|
+
},
|
|
770
|
+
];
|
|
771
|
+
return kind === 'process' ? [...common, ...process] : [...common, ...flow];
|
|
772
|
+
}
|
|
773
|
+
function makeFinding(code, severity, message, pathExpression) {
|
|
774
|
+
return pathExpression
|
|
775
|
+
? { code, severity, message, path: pathExpression }
|
|
776
|
+
: { code, severity, message };
|
|
777
|
+
}
|
|
778
|
+
function evaluateUnitOfAnalysis(plan) {
|
|
779
|
+
const findings = [];
|
|
780
|
+
const blockers = [];
|
|
781
|
+
const artifact = unitOfAnalysisFromPlan(plan);
|
|
782
|
+
if (!artifact) {
|
|
783
|
+
blockers.push(makeFinding('unit_of_analysis_missing', 'blocker', 'Build plan must include the skill-authored unit_of_analysis artifact.', 'unit_of_analysis'));
|
|
784
|
+
return { findings, blockers, decision: null };
|
|
785
|
+
}
|
|
786
|
+
const decision = unitOfAnalysisDecisionFromArtifact(artifact);
|
|
787
|
+
if (!decision) {
|
|
788
|
+
blockers.push(makeFinding('unit_of_analysis_decision_missing', 'blocker', 'unit_of_analysis must include a supported decision.', 'unit_of_analysis.decision'));
|
|
789
|
+
}
|
|
790
|
+
else if (!AUTOMATIC_UNIT_OF_ANALYSIS_DECISIONS.has(decision)) {
|
|
791
|
+
blockers.push(makeFinding('unit_of_analysis_not_automatic', 'blocker', `unit_of_analysis decision ${decision} cannot proceed to materialization.`, 'unit_of_analysis.decision'));
|
|
792
|
+
}
|
|
793
|
+
for (const spec of [
|
|
794
|
+
{ path: 'unit_of_analysis.target_kind', aliases: ['target_kind', 'targetKind'] },
|
|
795
|
+
{ path: 'unit_of_analysis.reference_flow', aliases: ['reference_flow', 'referenceFlow'] },
|
|
796
|
+
{
|
|
797
|
+
path: 'unit_of_analysis.reference_flow.reference_unit',
|
|
798
|
+
aliases: ['reference_flow.reference_unit', 'referenceFlow.referenceUnit'],
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
path: 'unit_of_analysis.reference_flow.reference_amount',
|
|
802
|
+
aliases: ['reference_flow.reference_amount', 'referenceFlow.referenceAmount'],
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
path: 'unit_of_analysis.reference_flow.flow_property',
|
|
806
|
+
aliases: ['reference_flow.flow_property', 'referenceFlow.flowProperty'],
|
|
807
|
+
},
|
|
808
|
+
]) {
|
|
809
|
+
if (!pathIsSatisfied(artifact, spec.aliases)) {
|
|
810
|
+
blockers.push(makeFinding('unit_of_analysis_required_field_missing', 'blocker', `unit_of_analysis is missing ${spec.path}.`, spec.path));
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const hasBasis = pathIsSatisfied(artifact, ['functional_unit', 'functionalUnit']) ||
|
|
814
|
+
pathIsSatisfied(artifact, ['declared_unit', 'declaredUnit']);
|
|
815
|
+
if (!hasBasis) {
|
|
816
|
+
blockers.push(makeFinding('unit_of_analysis_basis_missing', 'blocker', 'unit_of_analysis must describe either functional_unit or declared_unit.', 'unit_of_analysis.functional_unit'));
|
|
817
|
+
}
|
|
818
|
+
if (decision === 'ready_for_materialization' &&
|
|
819
|
+
!pathIsSatisfied(artifact, [
|
|
820
|
+
'scaling_evidence',
|
|
821
|
+
'scalingEvidence',
|
|
822
|
+
'scaling_evidence_status',
|
|
823
|
+
'scalingEvidenceStatus',
|
|
824
|
+
])) {
|
|
825
|
+
blockers.push(makeFinding('scaling_evidence_missing', 'blocker', 'ready_for_materialization requires scaling evidence or an explicit scaling evidence status.', 'unit_of_analysis.scaling_evidence'));
|
|
826
|
+
}
|
|
827
|
+
if (blockers.length === 0) {
|
|
828
|
+
findings.push(makeFinding('unit_of_analysis_contract_satisfied', 'info', 'unit_of_analysis artifact is present and complete enough for deterministic validation.'));
|
|
829
|
+
}
|
|
830
|
+
return { findings, blockers, decision };
|
|
831
|
+
}
|
|
832
|
+
function evaluateBuildPlan(plan, kind) {
|
|
833
|
+
const findings = [];
|
|
834
|
+
const blockers = [];
|
|
835
|
+
const expectedKind = firstToken(plan, ['kind', 'dataset_kind', 'datasetKind']);
|
|
836
|
+
if (expectedKind && expectedKind !== kind) {
|
|
837
|
+
blockers.push(makeFinding('build_plan_kind_mismatch', 'blocker', `Expected ${kind} build plan but received ${expectedKind}.`, 'kind'));
|
|
838
|
+
}
|
|
839
|
+
const decision = decisionFromPlan(plan);
|
|
840
|
+
if (!decision) {
|
|
841
|
+
blockers.push(makeFinding('identity_decision_missing', 'blocker', 'Build plan must include a supported identity decision.', 'identity_decision.decision'));
|
|
842
|
+
}
|
|
843
|
+
else if (!AUTO_DECISIONS.has(decision)) {
|
|
844
|
+
blockers.push(makeFinding('identity_decision_not_automatic', 'blocker', `Build plan identity decision ${decision} cannot proceed without review.`, 'identity_decision.decision'));
|
|
845
|
+
}
|
|
846
|
+
if (!evidenceSourcesPresent(plan)) {
|
|
847
|
+
blockers.push(makeFinding('evidence_sources_missing', 'blocker', 'EvidenceManifest must include at least one source.', 'evidence_manifest.sources'));
|
|
848
|
+
}
|
|
849
|
+
const unitOfAnalysis = evaluateUnitOfAnalysis(plan);
|
|
850
|
+
findings.push(...unitOfAnalysis.findings);
|
|
851
|
+
blockers.push(...unitOfAnalysis.blockers);
|
|
852
|
+
const bindingPaths = evidenceBindingPaths(plan);
|
|
853
|
+
const required = requiredFieldSpecs(kind);
|
|
854
|
+
const satisfied = [];
|
|
855
|
+
const missing = [];
|
|
856
|
+
for (const spec of required) {
|
|
857
|
+
if (pathIsSatisfied(plan, spec.aliases)) {
|
|
858
|
+
satisfied.push(spec.path);
|
|
859
|
+
if (!bindingPaths.has(spec.path)) {
|
|
860
|
+
blockers.push(makeFinding('evidence_binding_missing', 'blocker', `EvidenceManifest must bind source evidence to ${spec.path}.`, spec.path));
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
missing.push(spec.path);
|
|
865
|
+
blockers.push(makeFinding('build_plan_required_field_missing', 'blocker', `Build plan is missing ${spec.path}.`, spec.path));
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (blockers.length === 0) {
|
|
869
|
+
findings.push(makeFinding('build_plan_contract_satisfied', 'info', `${kind} build plan satisfies the minimum authoring gate contract.`));
|
|
870
|
+
}
|
|
871
|
+
return {
|
|
872
|
+
plan,
|
|
873
|
+
findings,
|
|
874
|
+
blockers,
|
|
875
|
+
requiredFields: {
|
|
876
|
+
required: required.map((spec) => spec.path),
|
|
877
|
+
satisfied,
|
|
878
|
+
missing,
|
|
879
|
+
},
|
|
880
|
+
decision,
|
|
881
|
+
unitOfAnalysisDecision: unitOfAnalysis.decision,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
function schemaForKind(kind, schemas) {
|
|
885
|
+
const injected = schemas?.[kind] ?? null;
|
|
886
|
+
if (injected) {
|
|
887
|
+
return {
|
|
888
|
+
validator: 'injected',
|
|
889
|
+
schema: injected,
|
|
890
|
+
createEntity: null,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
const schema = tidasSdk[String(SCHEMA_EXPORTS[kind])];
|
|
894
|
+
const createEntity = tidasSdk[String(ENTITY_FACTORY_EXPORTS[kind])];
|
|
895
|
+
return {
|
|
896
|
+
validator: `@tiangong-lca/tidas-sdk/${String(SCHEMA_EXPORTS[kind])}`,
|
|
897
|
+
schema,
|
|
898
|
+
createEntity,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function normalizeSchemaIssue(issue) {
|
|
902
|
+
return {
|
|
903
|
+
path: normalizeIssuePath(issue.path),
|
|
904
|
+
message: issue.message ?? 'Validation failed',
|
|
905
|
+
code: issue.code ?? 'custom',
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
function validateMaterializedSchema(artifact, kind, schemas) {
|
|
909
|
+
const detectedKind = detectDatasetKind(artifact);
|
|
910
|
+
if (!detectedKind) {
|
|
911
|
+
return {
|
|
912
|
+
status: 'not_applicable',
|
|
913
|
+
validator: null,
|
|
914
|
+
issue_count: 0,
|
|
915
|
+
issues: [],
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
if (detectedKind !== kind) {
|
|
919
|
+
return {
|
|
920
|
+
status: 'failed',
|
|
921
|
+
validator: null,
|
|
922
|
+
issue_count: 1,
|
|
923
|
+
issues: [
|
|
924
|
+
{
|
|
925
|
+
path: '<root>',
|
|
926
|
+
message: `Expected ${kind} payload but detected ${detectedKind}.`,
|
|
927
|
+
code: 'dataset_kind_mismatch',
|
|
928
|
+
},
|
|
929
|
+
],
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
const { validator, schema, createEntity } = schemaForKind(kind, schemas);
|
|
933
|
+
const payload = unwrapDatasetPayload(artifact);
|
|
934
|
+
const outcome = validateSchemaWithDeepFallback(schema, payload, createEntity);
|
|
935
|
+
if (outcome.success) {
|
|
936
|
+
return {
|
|
937
|
+
status: 'passed',
|
|
938
|
+
validator,
|
|
939
|
+
issue_count: 0,
|
|
940
|
+
issues: [],
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
return {
|
|
944
|
+
status: 'failed',
|
|
945
|
+
validator,
|
|
946
|
+
issue_count: outcome.issues.length,
|
|
947
|
+
issues: outcome.issues.map(normalizeSchemaIssue),
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function materializePlan(plan, kind, inputPath) {
|
|
951
|
+
const payload = firstValue(plan, ['payload', 'materialized_payload', 'materializedPayload']);
|
|
952
|
+
if (isRecord(payload)) {
|
|
953
|
+
return cloneJson(payload);
|
|
954
|
+
}
|
|
955
|
+
return kind === 'process'
|
|
956
|
+
? buildCanonicalProcessPayload(plan, inputPath)
|
|
957
|
+
: buildCanonicalFlowPayload(plan, inputPath);
|
|
958
|
+
}
|
|
959
|
+
function emptySchemaValidation() {
|
|
960
|
+
return {
|
|
961
|
+
status: 'not_applicable',
|
|
962
|
+
validator: null,
|
|
963
|
+
issue_count: 0,
|
|
964
|
+
issues: [],
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function reportPaths(outDir, kind) {
|
|
968
|
+
if (!outDir) {
|
|
969
|
+
return {
|
|
970
|
+
gate_report: null,
|
|
971
|
+
materialized_artifact: null,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
return {
|
|
975
|
+
gate_report: path.join(outDir, 'outputs', 'build-plan-gate-report.json'),
|
|
976
|
+
materialized_artifact: path.join(outDir, 'outputs', `materialized-${kind}.json`),
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
function makeReport(options) {
|
|
980
|
+
const ruleset = buildPlanRuleset(options.evaluation.plan, options.kind);
|
|
981
|
+
const schemaBlockers = options.schemaValidation.status === 'failed'
|
|
982
|
+
? [
|
|
983
|
+
makeFinding('materialized_schema_failed', 'blocker', 'Materialized payload failed schema validation.', 'materialized_artifact'),
|
|
984
|
+
]
|
|
985
|
+
: [];
|
|
986
|
+
const blockers = [...options.evaluation.blockers, ...schemaBlockers];
|
|
987
|
+
const status = blockers.length > 0 ? 'blocked' : 'passed';
|
|
988
|
+
return {
|
|
989
|
+
schema_version: 1,
|
|
990
|
+
generated_at_utc: options.generatedAt,
|
|
991
|
+
kind: options.kind,
|
|
992
|
+
action: options.action,
|
|
993
|
+
status,
|
|
994
|
+
ruleset_id: ruleset.id,
|
|
995
|
+
ruleset_version: ruleset.version,
|
|
996
|
+
input_path: options.inputPath,
|
|
997
|
+
out_dir: options.outDir,
|
|
998
|
+
report_only: options.reportOnly,
|
|
999
|
+
inputs: {
|
|
1000
|
+
plan_schema_version: textToken(options.evaluation.plan.schema_version) ??
|
|
1001
|
+
textToken(options.evaluation.plan.schemaVersion),
|
|
1002
|
+
identity_decision: options.evaluation.decision,
|
|
1003
|
+
unit_of_analysis_decision: options.evaluation.unitOfAnalysisDecision,
|
|
1004
|
+
},
|
|
1005
|
+
required_fields: options.evaluation.requiredFields,
|
|
1006
|
+
schema_validation: options.schemaValidation,
|
|
1007
|
+
findings: options.evaluation.findings,
|
|
1008
|
+
blockers,
|
|
1009
|
+
next_action: status === 'blocked'
|
|
1010
|
+
? 'fix_build_plan'
|
|
1011
|
+
: options.action === 'validate'
|
|
1012
|
+
? 'materialize_payload'
|
|
1013
|
+
: 'use_materialized_artifact',
|
|
1014
|
+
files: options.files,
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
async function runBuildPlan(kind, action, options) {
|
|
1018
|
+
const inputPath = requiredInputPath(options.inputPath);
|
|
1019
|
+
const outDir = options.outDir?.trim() ? options.outDir.trim() : null;
|
|
1020
|
+
const files = reportPaths(outDir, kind);
|
|
1021
|
+
const evaluation = evaluateBuildPlan(readBuildPlanInput(inputPath, options.rawInput), kind);
|
|
1022
|
+
const materialized = action === 'materialize' && evaluation.blockers.length === 0
|
|
1023
|
+
? materializePlan(evaluation.plan, kind, inputPath)
|
|
1024
|
+
: null;
|
|
1025
|
+
const schemaValidation = materialized
|
|
1026
|
+
? validateMaterializedSchema(materialized, kind, options.schemas)
|
|
1027
|
+
: emptySchemaValidation();
|
|
1028
|
+
const report = makeReport({
|
|
1029
|
+
kind,
|
|
1030
|
+
action,
|
|
1031
|
+
inputPath,
|
|
1032
|
+
outDir,
|
|
1033
|
+
reportOnly: Boolean(options.reportOnly),
|
|
1034
|
+
evaluation,
|
|
1035
|
+
schemaValidation,
|
|
1036
|
+
generatedAt: nowIso(options.now),
|
|
1037
|
+
files,
|
|
1038
|
+
});
|
|
1039
|
+
if (files.gate_report) {
|
|
1040
|
+
writeJsonArtifact(files.gate_report, report);
|
|
1041
|
+
}
|
|
1042
|
+
if (files.materialized_artifact && materialized) {
|
|
1043
|
+
writeJsonArtifact(files.materialized_artifact, materialized);
|
|
1044
|
+
}
|
|
1045
|
+
return report;
|
|
1046
|
+
}
|
|
1047
|
+
export async function runProcessBuildPlanValidate(options) {
|
|
1048
|
+
return (await runBuildPlan('process', 'validate', options));
|
|
1049
|
+
}
|
|
1050
|
+
export async function runProcessBuildPlanMaterialize(options) {
|
|
1051
|
+
return (await runBuildPlan('process', 'materialize', options));
|
|
1052
|
+
}
|
|
1053
|
+
export async function runFlowBuildPlanValidate(options) {
|
|
1054
|
+
return (await runBuildPlan('flow', 'validate', options));
|
|
1055
|
+
}
|
|
1056
|
+
export async function runFlowBuildPlanMaterialize(options) {
|
|
1057
|
+
return (await runBuildPlan('flow', 'materialize', options));
|
|
1058
|
+
}
|
|
1059
|
+
export const __testInternals = {
|
|
1060
|
+
decisionFromPlan,
|
|
1061
|
+
evidenceBindingPaths,
|
|
1062
|
+
evaluateBuildPlan,
|
|
1063
|
+
loadBuildPlan,
|
|
1064
|
+
materializePlan,
|
|
1065
|
+
validateMaterializedSchema,
|
|
1066
|
+
buildCanonicalFlowPayload,
|
|
1067
|
+
buildCanonicalProcessPayload,
|
|
1068
|
+
buildAnnualSupply,
|
|
1069
|
+
multiLangFromValue,
|
|
1070
|
+
};
|
|
1071
|
+
//# sourceMappingURL=process-flow-build-plan.js.map
|