@tiangong-lca/cli 0.0.28 → 0.0.30

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 (38) hide show
  1. package/README.md +90 -2
  2. package/dist/src/cli.js +553 -4
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/lib/dataset-command.js +11 -0
  5. package/dist/src/lib/dataset-command.js.map +1 -1
  6. package/dist/src/lib/dataset-maintenance-contract.js.map +1 -1
  7. package/dist/src/lib/dataset-maintenance-flow-identity-approval-claim.js +175 -0
  8. package/dist/src/lib/dataset-maintenance-flow-identity-approval-claim.js.map +1 -0
  9. package/dist/src/lib/dataset-maintenance-flow-identity-capture.js +511 -0
  10. package/dist/src/lib/dataset-maintenance-flow-identity-capture.js.map +1 -0
  11. package/dist/src/lib/dataset-maintenance-flow-identity-command.js +26 -0
  12. package/dist/src/lib/dataset-maintenance-flow-identity-command.js.map +1 -0
  13. package/dist/src/lib/dataset-maintenance-flow-identity-contract.js +784 -0
  14. package/dist/src/lib/dataset-maintenance-flow-identity-contract.js.map +1 -0
  15. package/dist/src/lib/dataset-maintenance-flow-identity-execution-contract.js +1317 -0
  16. package/dist/src/lib/dataset-maintenance-flow-identity-execution-contract.js.map +1 -0
  17. package/dist/src/lib/dataset-maintenance-flow-identity-freeze.js +342 -0
  18. package/dist/src/lib/dataset-maintenance-flow-identity-freeze.js.map +1 -0
  19. package/dist/src/lib/dataset-maintenance-flow-identity-plan.js +900 -0
  20. package/dist/src/lib/dataset-maintenance-flow-identity-plan.js.map +1 -0
  21. package/dist/src/lib/dataset-maintenance-flow-identity-recovery.js +688 -0
  22. package/dist/src/lib/dataset-maintenance-flow-identity-recovery.js.map +1 -0
  23. package/dist/src/lib/dataset-maintenance-flow-identity-run.js +1369 -0
  24. package/dist/src/lib/dataset-maintenance-flow-identity-run.js.map +1 -0
  25. package/dist/src/lib/dataset-maintenance-flow-identity-seal.js +144 -0
  26. package/dist/src/lib/dataset-maintenance-flow-identity-seal.js.map +1 -0
  27. package/dist/src/lib/dataset-maintenance-flow-identity-verify.js +377 -0
  28. package/dist/src/lib/dataset-maintenance-flow-identity-verify.js.map +1 -0
  29. package/dist/src/lib/dataset-maintenance-flow-identity-wire.js +178 -0
  30. package/dist/src/lib/dataset-maintenance-flow-identity-wire.js.map +1 -0
  31. package/dist/src/lib/dataset-maintenance-remote.js +156 -10
  32. package/dist/src/lib/dataset-maintenance-remote.js.map +1 -1
  33. package/dist/src/lib/dataset-save-draft-run.js +667 -0
  34. package/dist/src/lib/dataset-save-draft-run.js.map +1 -1
  35. package/dist/src/lib/http.js.map +1 -1
  36. package/dist/src/lib/lca-release.js +683 -0
  37. package/dist/src/lib/lca-release.js.map +1 -0
  38. package/package.json +1 -1
@@ -0,0 +1,900 @@
1
+ import path from 'node:path';
2
+ import { computeFlowIdentityCaptureSha256, computeFlowIdentityMappingId, computeFlowIdentityPlanSha256, computeFlowIdentityProcessTemplateSha256, extractFlowIdentityReference, parseFlowIdentityCapture, parseFlowIdentityPolicy, parseFlowIdentityReference, parseFlowIdentityReviewLedger, } from './dataset-maintenance-flow-identity-contract.js';
3
+ import { flowIdentityRestrictedSha256 } from './dataset-maintenance-flow-identity-wire.js';
4
+ import { ensurePrivateArtifactDirectory, writePrivateImmutableJson, writePrivateImmutableText, } from './dataset-maintenance-protected-artifacts.js';
5
+ import { isJsonObject, maintenanceRowKey, sha256Json, snapshotRemoteRow, stableJsonText, } from './dataset-maintenance-contract.js';
6
+ import { CliError } from './errors.js';
7
+ import { validateFlowPayload, } from './flow-payload-validation.js';
8
+ import { validateProcessPayload, } from './process-payload-validation.js';
9
+ const POSTGREST_UTC_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?\+00:00$/u;
10
+ function fail(message, code = 'DATASET_FLOW_IDENTITY_PLAN_INVALID', details) {
11
+ throw new CliError(message, { code, exitCode: 1, ...(details === undefined ? {} : { details }) });
12
+ }
13
+ function rowKey(id, version) {
14
+ return `${id}\u0000${version}`;
15
+ }
16
+ function requireSnapshotIntegrity(row) {
17
+ const remote = {
18
+ table: row.table,
19
+ id: row.id,
20
+ version: row.version,
21
+ user_id: row.user_id,
22
+ state_code: row.state_code,
23
+ modified_at: row.modified_at,
24
+ json_ordered: row.json_ordered,
25
+ model_id: row.model_id,
26
+ rule_verification: row.rule_verification,
27
+ };
28
+ const expected = snapshotRemoteRow(remote);
29
+ if (expected.row_sha256 !== row.row_sha256 || expected.payload_sha256 !== row.payload_sha256) {
30
+ fail('Live capture contains a row whose canonical snapshot hash is invalid.', undefined, {
31
+ table: row.table,
32
+ id: row.id,
33
+ version: row.version,
34
+ });
35
+ }
36
+ }
37
+ function indexRows(rows, table) {
38
+ const selected = rows.filter((row) => row.table === table);
39
+ selected.forEach(requireSnapshotIntegrity);
40
+ const index = new Map(selected.map((row) => [rowKey(row.id, row.version), row]));
41
+ if (index.size !== selected.length)
42
+ fail(`Live capture contains duplicate ${table} rows.`);
43
+ return index;
44
+ }
45
+ function arrayOfObjects(value) {
46
+ const rows = Array.isArray(value) ? value : isJsonObject(value) ? [value] : null;
47
+ return rows?.every(isJsonObject) ? rows : null;
48
+ }
49
+ function flowRoot(payload) {
50
+ return isJsonObject(payload.flowDataSet) ? payload.flowDataSet : null;
51
+ }
52
+ function processRoot(payload) {
53
+ return isJsonObject(payload.processDataSet) ? payload.processDataSet : null;
54
+ }
55
+ function flowIdentity(payload) {
56
+ const root = flowRoot(payload);
57
+ const information = isJsonObject(root?.flowInformation) ? root.flowInformation : null;
58
+ const dataset = isJsonObject(information?.dataSetInformation)
59
+ ? information.dataSetInformation
60
+ : null;
61
+ const admin = isJsonObject(root?.administrativeInformation)
62
+ ? root.administrativeInformation
63
+ : null;
64
+ const publication = isJsonObject(admin?.publicationAndOwnership)
65
+ ? admin.publicationAndOwnership
66
+ : null;
67
+ const id = dataset?.['common:UUID'];
68
+ const version = publication?.['common:dataSetVersion'];
69
+ return typeof id === 'string' && typeof version === 'string' ? { id, version } : null;
70
+ }
71
+ export function flowType(payload) {
72
+ const root = flowRoot(payload);
73
+ const modelling = isJsonObject(root?.modellingAndValidation) ? root.modellingAndValidation : null;
74
+ const method = isJsonObject(modelling?.LCIMethod) ? modelling.LCIMethod : null;
75
+ return typeof method?.typeOfDataSet === 'string' ? method.typeOfDataSet : null;
76
+ }
77
+ function flowClassificationInformation(payload) {
78
+ const root = flowRoot(payload);
79
+ const information = isJsonObject(root?.flowInformation) ? root.flowInformation : null;
80
+ const dataset = isJsonObject(information?.dataSetInformation)
81
+ ? information.dataSetInformation
82
+ : null;
83
+ return dataset?.classificationInformation ?? null;
84
+ }
85
+ function textValues(value) {
86
+ const values = Array.isArray(value) ? value : [value];
87
+ return [
88
+ ...new Set(values.flatMap((entry) => {
89
+ const candidate = typeof entry === 'string'
90
+ ? entry
91
+ : isJsonObject(entry) && typeof entry['#text'] === 'string'
92
+ ? entry['#text']
93
+ : null;
94
+ return candidate?.trim() ? [candidate] : [];
95
+ })),
96
+ ].sort();
97
+ }
98
+ function targetReferenceMatches(payload, reference) {
99
+ const root = flowRoot(payload);
100
+ const information = isJsonObject(root?.flowInformation) ? root.flowInformation : null;
101
+ const dataset = isJsonObject(information?.dataSetInformation)
102
+ ? information.dataSetInformation
103
+ : null;
104
+ const name = isJsonObject(dataset?.name) ? dataset.name : null;
105
+ const referenceNames = textValues(reference['common:shortDescription']);
106
+ const baseNames = textValues(name?.baseName);
107
+ return referenceNames.some((entry) => baseNames.includes(entry));
108
+ }
109
+ function referenceIdentity(value) {
110
+ if (!isJsonObject(value) || typeof value['@refObjectId'] !== 'string')
111
+ return null;
112
+ return {
113
+ id: value['@refObjectId'],
114
+ version: typeof value['@version'] === 'string' ? value['@version'] : null,
115
+ };
116
+ }
117
+ function flowSupportFacts(row, supportRows, actorUserId) {
118
+ const payload = row.json_ordered;
119
+ const root = payload ? flowRoot(payload) : null;
120
+ const information = isJsonObject(root?.flowInformation) ? root.flowInformation : null;
121
+ const quantitative = isJsonObject(information?.quantitativeReference)
122
+ ? information.quantitativeReference
123
+ : null;
124
+ const referenceInternalId = quantitative?.referenceToReferenceFlowProperty;
125
+ const properties = isJsonObject(root?.flowProperties) ? root.flowProperties : null;
126
+ const entries = arrayOfObjects(properties?.flowProperty);
127
+ const property = entries?.find((entry) => entry['@dataSetInternalID'] === referenceInternalId);
128
+ const propertyRef = referenceIdentity(property?.referenceToFlowPropertyDataSet);
129
+ if (!payload || !propertyRef)
130
+ return null;
131
+ const flowPropertyVersion = propertyRef.version;
132
+ if (!flowPropertyVersion)
133
+ return null;
134
+ const fp = supportRows.find((entry) => entry.table === 'flowproperties' &&
135
+ entry.id === propertyRef.id &&
136
+ entry.version === flowPropertyVersion);
137
+ if (!fp || !(fp.state_code === 100 || (fp.user_id === actorUserId && fp.state_code === 0))) {
138
+ return null;
139
+ }
140
+ const fpRoot = isJsonObject(fp?.json_ordered?.flowPropertyDataSet)
141
+ ? fp.json_ordered.flowPropertyDataSet
142
+ : null;
143
+ const fpInformation = isJsonObject(fpRoot?.flowPropertiesInformation)
144
+ ? fpRoot.flowPropertiesInformation
145
+ : null;
146
+ const fpDataset = isJsonObject(fpInformation?.dataSetInformation)
147
+ ? fpInformation.dataSetInformation
148
+ : null;
149
+ const fpQuantitative = isJsonObject(fpInformation?.quantitativeReference)
150
+ ? fpInformation.quantitativeReference
151
+ : null;
152
+ const fpAdmin = isJsonObject(fpRoot?.administrativeInformation)
153
+ ? fpRoot.administrativeInformation
154
+ : null;
155
+ const fpPublication = isJsonObject(fpAdmin?.publicationAndOwnership)
156
+ ? fpAdmin.publicationAndOwnership
157
+ : null;
158
+ const unitGroupRef = referenceIdentity(fpQuantitative?.referenceToReferenceUnitGroup);
159
+ if (!unitGroupRef ||
160
+ fpDataset?.['common:UUID'] !== fp.id ||
161
+ fpPublication?.['common:dataSetVersion'] !== fp.version)
162
+ return null;
163
+ const unitGroupVersion = unitGroupRef.version;
164
+ const identity = flowIdentity(payload);
165
+ const type = flowType(payload);
166
+ const unitGroup = supportRows.find((entry) => entry.table === 'unitgroups' &&
167
+ entry.id === unitGroupRef.id &&
168
+ entry.version === unitGroupVersion);
169
+ const unitGroupRoot = isJsonObject(unitGroup?.json_ordered?.unitGroupDataSet)
170
+ ? unitGroup.json_ordered.unitGroupDataSet
171
+ : null;
172
+ const unitGroupInformation = isJsonObject(unitGroupRoot?.unitGroupInformation)
173
+ ? unitGroupRoot.unitGroupInformation
174
+ : null;
175
+ const unitGroupDataset = isJsonObject(unitGroupInformation?.dataSetInformation)
176
+ ? unitGroupInformation.dataSetInformation
177
+ : null;
178
+ const unitGroupQuantitative = isJsonObject(unitGroupInformation?.quantitativeReference)
179
+ ? unitGroupInformation.quantitativeReference
180
+ : null;
181
+ const referenceUnit = unitGroupQuantitative?.referenceToReferenceUnit;
182
+ const unitsRoot = isJsonObject(unitGroupRoot?.units) ? unitGroupRoot.units : null;
183
+ const units = arrayOfObjects(unitsRoot?.unit);
184
+ const referenceUnitRow = units?.find((entry) => entry['@dataSetInternalID'] === referenceUnit);
185
+ const unitGroupAdmin = isJsonObject(unitGroupRoot?.administrativeInformation)
186
+ ? unitGroupRoot.administrativeInformation
187
+ : null;
188
+ const unitGroupPublication = isJsonObject(unitGroupAdmin?.publicationAndOwnership)
189
+ ? unitGroupAdmin.publicationAndOwnership
190
+ : null;
191
+ if (!identity ||
192
+ identity.id !== row.id ||
193
+ identity.version !== row.version ||
194
+ type !== 'Elementary flow' ||
195
+ !unitGroupVersion ||
196
+ !unitGroup ||
197
+ !(unitGroup.state_code === 100 ||
198
+ (unitGroup.user_id === actorUserId && unitGroup.state_code === 0)) ||
199
+ unitGroupDataset?.['common:UUID'] !== unitGroup.id ||
200
+ unitGroupPublication?.['common:dataSetVersion'] !== unitGroup.version ||
201
+ !referenceUnitRow ||
202
+ Number(referenceUnitRow.meanValue) !== 1) {
203
+ return null;
204
+ }
205
+ return {
206
+ id: row.id,
207
+ version: row.version,
208
+ flow_type: 'Elementary flow',
209
+ flow_property_id: propertyRef.id,
210
+ flow_property_version: flowPropertyVersion,
211
+ unit_group_id: unitGroupRef.id,
212
+ unit_group_version: unitGroupVersion,
213
+ category_path_sha256: sha256Json(flowClassificationInformation(payload)),
214
+ };
215
+ }
216
+ function flowGuardRowSha256(row) {
217
+ if (!row.user_id ||
218
+ row.state_code === null ||
219
+ !row.modified_at ||
220
+ !POSTGREST_UTC_TIMESTAMP_PATTERN.test(row.modified_at) ||
221
+ !row.payload_sha256) {
222
+ return null;
223
+ }
224
+ return sha256Json({
225
+ id: row.id,
226
+ version: row.version,
227
+ user_id: row.user_id,
228
+ state_code: row.state_code,
229
+ modified_at: row.modified_at,
230
+ payload_sha256: row.payload_sha256,
231
+ });
232
+ }
233
+ function buildSupportSnapshots(options) {
234
+ const claimed = new Map();
235
+ for (const mapping of options.mappings) {
236
+ for (const endpoint of [mapping.source, mapping.target]) {
237
+ const identities = [
238
+ {
239
+ table: 'flowproperties',
240
+ id: endpoint.flow_property_id,
241
+ version: endpoint.flow_property_version,
242
+ },
243
+ {
244
+ table: 'unitgroups',
245
+ id: endpoint.unit_group_id,
246
+ version: endpoint.unit_group_version,
247
+ },
248
+ ];
249
+ for (const identity of identities) {
250
+ claimed.set(`${identity.table}\u0000${identity.id}\u0000${identity.version}`, identity);
251
+ }
252
+ }
253
+ }
254
+ const captureRows = new Map();
255
+ for (const row of options.capture.support_rows) {
256
+ if (row.table !== 'flowproperties' && row.table !== 'unitgroups')
257
+ continue;
258
+ requireSnapshotIntegrity(row);
259
+ const key = `${row.table}\u0000${row.id}\u0000${row.version}`;
260
+ if (captureRows.has(key))
261
+ fail('Live capture contains duplicate support rows.');
262
+ captureRows.set(key, row);
263
+ }
264
+ return [...claimed.values()]
265
+ .sort((left, right) => `${left.table}\u0000${left.id}\u0000${left.version}`.localeCompare(`${right.table}\u0000${right.id}\u0000${right.version}`))
266
+ .map((identity, index) => {
267
+ const row = captureRows.get(`${identity.table}\u0000${identity.id}\u0000${identity.version}`);
268
+ const rowSha256 = row ? flowGuardRowSha256(row) : null;
269
+ if (!row ||
270
+ !row.user_id ||
271
+ !row.modified_at ||
272
+ !row.payload_sha256 ||
273
+ (row.state_code !== 0 && row.state_code !== 100) ||
274
+ (row.state_code === 0 && row.user_id !== options.capture.account.user_id) ||
275
+ !rowSha256) {
276
+ fail('A claimed FP/UG support row cannot produce an exact database guard.', undefined, {
277
+ ...identity,
278
+ });
279
+ }
280
+ return {
281
+ ordinal: index + 1,
282
+ ...identity,
283
+ user_id: row.user_id,
284
+ state_code: row.state_code,
285
+ modified_at: row.modified_at,
286
+ payload_sha256: row.payload_sha256,
287
+ row_sha256: rowSha256,
288
+ };
289
+ });
290
+ }
291
+ function processGuardRowSha256(row) {
292
+ if (!row.user_id ||
293
+ row.state_code === null ||
294
+ !row.modified_at ||
295
+ !POSTGREST_UTC_TIMESTAMP_PATTERN.test(row.modified_at) ||
296
+ !row.payload_sha256) {
297
+ return null;
298
+ }
299
+ return sha256Json({
300
+ id: row.id,
301
+ version: row.version,
302
+ user_id: row.user_id,
303
+ state_code: row.state_code,
304
+ modified_at: row.modified_at,
305
+ model_id: row.model_id,
306
+ rule_verification: row.rule_verification,
307
+ payload_sha256: row.payload_sha256,
308
+ });
309
+ }
310
+ function endpoint(row, supportRows, actorUserId) {
311
+ const facts = flowSupportFacts(row, supportRows, actorUserId);
312
+ const rowSha256 = flowGuardRowSha256(row);
313
+ return facts &&
314
+ row.user_id &&
315
+ row.modified_at &&
316
+ row.payload_sha256 &&
317
+ typeof row.state_code === 'number' &&
318
+ rowSha256
319
+ ? {
320
+ ...facts,
321
+ user_id: row.user_id,
322
+ state_code: row.state_code,
323
+ modified_at: row.modified_at,
324
+ payload_sha256: row.payload_sha256,
325
+ row_sha256: rowSha256,
326
+ }
327
+ : null;
328
+ }
329
+ function processExchanges(payload) {
330
+ const root = processRoot(payload);
331
+ const exchanges = isJsonObject(root?.exchanges) ? root.exchanges : null;
332
+ return arrayOfObjects(exchanges?.exchange);
333
+ }
334
+ function exchangeReference(exchange) {
335
+ try {
336
+ return extractFlowIdentityReference(exchange.referenceToFlowDataSet, 'exchange reference');
337
+ }
338
+ catch {
339
+ return null;
340
+ }
341
+ }
342
+ function exchangeDirection(exchange) {
343
+ return exchange.exchangeDirection === 'Input' || exchange.exchangeDirection === 'Output'
344
+ ? exchange.exchangeDirection
345
+ : null;
346
+ }
347
+ function exchangeInternalId(exchange) {
348
+ return typeof exchange['@dataSetInternalID'] === 'string' ? exchange['@dataSetInternalID'] : null;
349
+ }
350
+ function patchReference(reference, target) {
351
+ for (const field of [
352
+ '@refObjectId',
353
+ '@type',
354
+ '@uri',
355
+ '@version',
356
+ 'common:shortDescription',
357
+ ]) {
358
+ reference[field] = structuredClone(target[field]);
359
+ }
360
+ }
361
+ function occurrence(options) {
362
+ const internalId = exchangeInternalId(options.exchange);
363
+ const direction = exchangeDirection(options.exchange);
364
+ if (!internalId || !direction)
365
+ fail('Process exchange identity or direction is malformed.');
366
+ return {
367
+ process_id: options.process.id,
368
+ process_version: options.process.version,
369
+ exchange_index: options.exchangeIndex,
370
+ internal_id: internalId,
371
+ direction,
372
+ reference_sha256: sha256Json(options.reference),
373
+ };
374
+ }
375
+ function collectOccurrences(processes) {
376
+ const result = new Map();
377
+ for (const process of [...processes].sort((left, right) => rowKey(left.id, left.version).localeCompare(rowKey(right.id, right.version)))) {
378
+ if (!process.json_ordered)
379
+ fail('Process capture row has no json_ordered payload.');
380
+ const exchanges = processExchanges(process.json_ordered);
381
+ if (!exchanges)
382
+ fail('Process capture row has a malformed exchange collection.');
383
+ exchanges.forEach((exchange, exchangeIndex) => {
384
+ const reference = exchangeReference(exchange);
385
+ if (!reference)
386
+ fail('Process exchange has a malformed flow reference.');
387
+ const key = rowKey(reference['@refObjectId'], reference['@version']);
388
+ const rows = result.get(key) ?? [];
389
+ rows.push(occurrence({ process, exchange, exchangeIndex, reference }));
390
+ result.set(key, rows);
391
+ });
392
+ }
393
+ return result;
394
+ }
395
+ function protectedReferenceEntry(review, occurrences) {
396
+ return {
397
+ source_id: review.source.id,
398
+ source_version: review.source.version,
399
+ expected_reference_count: occurrences.length,
400
+ occurrences,
401
+ occurrence_set_sha256: sha256Json(occurrences),
402
+ evidence_sha256: review.decision_evidence_sha256,
403
+ };
404
+ }
405
+ function buildProtectedClosure(review, occurrenceIndex) {
406
+ const pending = [];
407
+ const blockers = [];
408
+ const orphans = [];
409
+ for (const entry of review.entries) {
410
+ const occurrences = occurrenceIndex.get(rowKey(entry.source.id, entry.source.version)) ?? [];
411
+ if (entry.disposition === 'pending')
412
+ pending.push(protectedReferenceEntry(entry, occurrences));
413
+ if (entry.disposition === 'blocker')
414
+ blockers.push(protectedReferenceEntry(entry, occurrences));
415
+ if (entry.disposition === 'orphan') {
416
+ if (occurrences.length)
417
+ fail('A reviewed orphan has live process references.', undefined, entry.source);
418
+ orphans.push({
419
+ source_id: entry.source.id,
420
+ source_version: entry.source.version,
421
+ evidence_sha256: entry.decision_evidence_sha256,
422
+ });
423
+ }
424
+ }
425
+ return {
426
+ schema_version: 'dataset-flow-identity-protected-closure.v1',
427
+ pending,
428
+ blockers,
429
+ orphans,
430
+ pending_set_sha256: sha256Json(pending),
431
+ blocker_set_sha256: sha256Json(blockers),
432
+ orphan_set_sha256: sha256Json(orphans),
433
+ total_expected_reference_count: [...pending, ...blockers].reduce((sum, entry) => sum + entry.expected_reference_count, 0),
434
+ };
435
+ }
436
+ function flowSchemaProof(validation) {
437
+ return {
438
+ status: validation.ok ? 'pass' : 'legacy_warning',
439
+ warning_set_sha256: sha256Json(validation.ok ? [] : validation.issues),
440
+ };
441
+ }
442
+ function buildMappings(options) {
443
+ const sources = indexRows(options.capture.source_rows, 'flows');
444
+ const targets = indexRows(options.capture.target_rows, 'flows');
445
+ options.capture.support_rows.forEach(requireSnapshotIntegrity);
446
+ const reviewedSourceKeys = new Set(options.review.entries.map((entry) => rowKey(entry.source.id, entry.source.version)));
447
+ const approvedTargetKeys = new Set(options.review.entries
448
+ .filter((entry) => entry.disposition === 'map_public')
449
+ .map((entry) => rowKey(entry.target.id, entry.target.version)));
450
+ if (sources.size !== options.review.entries.length ||
451
+ [...sources.keys()].some((key) => !reviewedSourceKeys.has(key)) ||
452
+ [...reviewedSourceKeys].some((key) => !sources.has(key)) ||
453
+ targets.size !== approvedTargetKeys.size ||
454
+ [...targets.keys()].some((key) => !approvedTargetKeys.has(key))) {
455
+ fail('Fresh capture must contain exactly the 305 reviewed source flow rows.');
456
+ }
457
+ for (const row of sources.values()) {
458
+ if (row.user_id !== options.capture.account.user_id ||
459
+ row.state_code !== 0 ||
460
+ !endpoint(row, options.capture.support_rows, options.capture.account.user_id)) {
461
+ fail('Every reviewed source must remain an exact current-owner state-0 Elementary flow.');
462
+ }
463
+ }
464
+ return options.review.entries
465
+ .filter((entry) => entry.disposition === 'map_public')
466
+ .map((entry, index) => {
467
+ const sourceRow = sources.get(rowKey(entry.source.id, entry.source.version));
468
+ const target = entry.target;
469
+ const targetRow = targets.get(rowKey(target.id, target.version));
470
+ if (!sourceRow || !targetRow)
471
+ fail('Approved mapping source or target is absent from fresh capture.');
472
+ const source = endpoint(sourceRow, options.capture.support_rows, options.capture.account.user_id);
473
+ const targetEndpoint = endpoint(targetRow, options.capture.support_rows, options.capture.account.user_id);
474
+ const targetReference = parseFlowIdentityReference(target.reference, 'review target reference');
475
+ const occurrences = options.occurrenceIndex.get(rowKey(entry.source.id, entry.source.version)) ?? [];
476
+ if (!source ||
477
+ !targetEndpoint ||
478
+ source.user_id !== options.capture.account.user_id ||
479
+ source.state_code !== 0 ||
480
+ targetEndpoint.user_id === options.capture.account.user_id ||
481
+ targetEndpoint.state_code !== 100 ||
482
+ targetReference['@refObjectId'] !== targetEndpoint.id ||
483
+ targetReference['@version'] !== targetEndpoint.version ||
484
+ !targetReferenceMatches(targetRow.json_ordered, targetReference) ||
485
+ source.flow_property_id !== targetEndpoint.flow_property_id ||
486
+ source.flow_property_version !== targetEndpoint.flow_property_version ||
487
+ source.unit_group_id !== targetEndpoint.unit_group_id ||
488
+ source.unit_group_version !== targetEndpoint.unit_group_version ||
489
+ occurrences.some((occurrence) => !entry.allowed_directions.includes(occurrence.direction))) {
490
+ fail('Approved mapping failed a fresh owner/public/type/support/direction compatibility guard.', undefined, {
491
+ source: entry.source,
492
+ target: entry.target,
493
+ });
494
+ }
495
+ const targetValidation = options.validateFlow(targetRow.json_ordered);
496
+ const compatibilityEvidence = sha256Json({
497
+ decision_evidence_sha256: entry.decision_evidence_sha256,
498
+ compartment_evidence_sha256: entry.compartment_evidence_sha256,
499
+ source_trace_sha256: entry.source_trace_sha256,
500
+ source,
501
+ target: targetEndpoint,
502
+ allowed_directions: entry.allowed_directions,
503
+ });
504
+ const withoutId = {
505
+ ordinal: index + 1,
506
+ source: { ...source, source_trace_sha256: entry.source_trace_sha256 },
507
+ target: { ...targetEndpoint, reference: targetReference },
508
+ compatibility: {
509
+ policy_sha256: options.policy.policy_sha256,
510
+ mode: 'identity',
511
+ confidence: 'approved',
512
+ flow_property_compatible: true,
513
+ unit_group_compatible: true,
514
+ direction_compatible: true,
515
+ compartment_compatible: true,
516
+ conversion_factor: '1',
517
+ evidence_sha256: compatibilityEvidence,
518
+ flow_schema: flowSchemaProof(targetValidation),
519
+ process_schema_required: 'pass',
520
+ },
521
+ };
522
+ return { ...withoutId, mapping_id: computeFlowIdentityMappingId(withoutId) };
523
+ });
524
+ }
525
+ function collisionLedger(options) {
526
+ const touchedTargets = new Set(options.rewrites.map((rewrite) => `${rewrite.target_reference['@refObjectId']}\u0000${rewrite.target_reference['@version']}`));
527
+ const mappingByIndex = new Map(options.rewrites.map((rewrite) => [rewrite.exchange_index, rewrite.mapping_id]));
528
+ const entries = [];
529
+ for (const targetKey of [...touchedTargets].sort()) {
530
+ const matches = options.desiredExchanges
531
+ .map((exchange, exchangeIndex) => ({
532
+ exchange,
533
+ exchangeIndex,
534
+ reference: exchangeReference(exchange),
535
+ }))
536
+ .filter((entry) => entry.reference &&
537
+ rowKey(entry.reference['@refObjectId'], entry.reference['@version']) === targetKey);
538
+ if (matches.length <= 1)
539
+ continue;
540
+ const first = matches[0].reference;
541
+ entries.push({
542
+ target_id: first['@refObjectId'],
543
+ target_version: first['@version'],
544
+ exchange_indexes: matches.map((entry) => entry.exchangeIndex),
545
+ internal_ids: matches.map((entry) => exchangeInternalId(entry.exchange)),
546
+ mapping_ids: matches.map((entry) => mappingByIndex.get(entry.exchangeIndex) ?? null),
547
+ preserve_rows: true,
548
+ });
549
+ }
550
+ return { schema_version: 'dataset-flow-identity-collision-ledger.v1', entries };
551
+ }
552
+ function buildProcessTemplate(options) {
553
+ if (!options.row.json_ordered || !options.row.payload_sha256 || !options.row.modified_at) {
554
+ fail('Affected process row lacks a complete fresh snapshot.');
555
+ }
556
+ const beforeExchanges = processExchanges(options.row.json_ordered);
557
+ if (!beforeExchanges)
558
+ fail('Affected process exchange collection is malformed.');
559
+ const desired = structuredClone(options.row.json_ordered);
560
+ const desiredExchanges = processExchanges(desired);
561
+ const rewrites = [];
562
+ beforeExchanges.forEach((exchange, exchangeIndex) => {
563
+ const sourceReference = exchangeReference(exchange);
564
+ if (!sourceReference)
565
+ fail('Affected process contains a malformed source reference.');
566
+ const mapping = options.mappingsBySource.get(rowKey(sourceReference['@refObjectId'], sourceReference['@version']));
567
+ if (!mapping)
568
+ return;
569
+ const direction = exchangeDirection(exchange);
570
+ const internalId = exchangeInternalId(exchange);
571
+ const desiredReference = desiredExchanges[exchangeIndex].referenceToFlowDataSet;
572
+ if (!direction || !internalId || !isJsonObject(desiredReference)) {
573
+ fail('Affected exchange identity, direction, or reference object is malformed.');
574
+ }
575
+ patchReference(desiredReference, mapping.target.reference);
576
+ rewrites.push({
577
+ ordinal: rewrites.length + 1,
578
+ exchange_index: exchangeIndex,
579
+ internal_id: internalId,
580
+ direction,
581
+ mapping_id: mapping.mapping_id,
582
+ source_reference: sourceReference,
583
+ target_reference: structuredClone(mapping.target.reference),
584
+ before_reference_sha256: sha256Json(sourceReference),
585
+ after_reference_sha256: sha256Json(mapping.target.reference),
586
+ });
587
+ });
588
+ if (!rewrites.length)
589
+ return null;
590
+ const validation = options.validateProcess(desired);
591
+ if (!validation.ok) {
592
+ fail('Desired process failed ProcessSchema and cannot enter an executable plan.', undefined, {
593
+ id: options.row.id,
594
+ version: options.row.version,
595
+ issues: validation.issues,
596
+ });
597
+ }
598
+ const collision = collisionLedger({ desiredExchanges, rewrites });
599
+ const processSchemaEvidence = sha256Json({
600
+ validator: validation.validator,
601
+ status: 'pass',
602
+ desired_payload_sha256: sha256Json(desired),
603
+ });
604
+ const beforeRowSha256 = processGuardRowSha256(options.row);
605
+ if (!beforeRowSha256)
606
+ fail('Affected process row cannot produce the database guard hash.');
607
+ const manifestWithoutHash = {
608
+ ordinal: options.ordinal,
609
+ id: options.row.id,
610
+ version: options.row.version,
611
+ user_id: options.row.user_id,
612
+ state_code: 0,
613
+ modified_at: options.row.modified_at,
614
+ model_id: options.row.model_id,
615
+ rule_verification: options.row.rule_verification,
616
+ before_row_sha256: beforeRowSha256,
617
+ before_payload_sha256: options.row.payload_sha256,
618
+ before_exchange_set_sha256: sha256Json(beforeExchanges),
619
+ before_exchange_count: beforeExchanges.length,
620
+ desired_payload_sha256: sha256Json(desired),
621
+ desired_exchange_set_sha256: sha256Json(desiredExchanges),
622
+ rewrite_count: rewrites.length,
623
+ process_template_sha256: '',
624
+ rewrite_set_sha256: sha256Json(rewrites),
625
+ collision_ledger_sha256: sha256Json(collision),
626
+ process_schema: { status: 'pass', evidence_sha256: processSchemaEvidence },
627
+ pending_blocker_closure_sha256: options.pendingBlockerClosureSha256,
628
+ };
629
+ const process = {
630
+ ...manifestWithoutHash,
631
+ process_template_sha256: computeFlowIdentityProcessTemplateSha256(manifestWithoutHash),
632
+ };
633
+ return { process, rewrites, collision_ledger: collision, desired_payload: desired };
634
+ }
635
+ function buildProcessTemplates(options) {
636
+ const mappingsBySource = new Map(options.mappings.map((mapping) => [rowKey(mapping.source.id, mapping.source.version), mapping]));
637
+ const rows = options.capture.process_rows
638
+ .filter((row) => row.table === 'processes')
639
+ .sort((left, right) => maintenanceRowKey(left).localeCompare(maintenanceRowKey(right)));
640
+ rows.forEach(requireSnapshotIntegrity);
641
+ const templates = [];
642
+ for (const row of rows) {
643
+ if (row.user_id !== options.capture.account.user_id || row.state_code !== 0) {
644
+ fail('Process capture contains a foreign-owner or non-draft row.');
645
+ }
646
+ const template = buildProcessTemplate({
647
+ ordinal: templates.length + 1,
648
+ row,
649
+ mappingsBySource,
650
+ pendingBlockerClosureSha256: sha256Json(options.closure),
651
+ validateProcess: options.validateProcess,
652
+ });
653
+ if (!template)
654
+ continue;
655
+ templates.push(template);
656
+ }
657
+ return templates;
658
+ }
659
+ export function buildFlowIdentityCaptureRequest(options) {
660
+ const mappingOrdinalById = new Map(options.mappings.map((mapping) => [mapping.mapping_id, mapping.ordinal]));
661
+ const mappings = options.mappings.map((mapping) => ({
662
+ ordinal: mapping.ordinal,
663
+ source: {
664
+ id: mapping.source.id,
665
+ version: mapping.source.version,
666
+ source_trace_sha256: mapping.source.source_trace_sha256,
667
+ },
668
+ target: {
669
+ id: mapping.target.id,
670
+ version: mapping.target.version,
671
+ reference: mapping.target.reference,
672
+ },
673
+ compatibility: mapping.compatibility,
674
+ }));
675
+ const processIntents = options.processTemplates.map((template) => ({
676
+ ordinal: template.process.ordinal,
677
+ id: template.process.id,
678
+ version: template.process.version,
679
+ rewrites: template.rewrites.map((rewrite) => {
680
+ const mappingOrdinal = mappingOrdinalById.get(rewrite.mapping_id);
681
+ if (!mappingOrdinal)
682
+ fail('A process rewrite refers to a foreign mapping.');
683
+ return {
684
+ ordinal: rewrite.ordinal,
685
+ exchange_index: rewrite.exchange_index,
686
+ internal_id: rewrite.internal_id,
687
+ direction: rewrite.direction,
688
+ mapping_ordinal: mappingOrdinal,
689
+ };
690
+ }),
691
+ process_schema: template.process.process_schema,
692
+ }));
693
+ const occurrenceIntent = (entry) => ({
694
+ source_id: entry.source_id,
695
+ source_version: entry.source_version,
696
+ expected_reference_count: entry.expected_reference_count,
697
+ occurrences: entry.occurrences.map((occurrence) => ({
698
+ process_id: occurrence.process_id,
699
+ process_version: occurrence.process_version,
700
+ exchange_index: occurrence.exchange_index,
701
+ internal_id: occurrence.internal_id,
702
+ direction: occurrence.direction,
703
+ })),
704
+ evidence_sha256: entry.evidence_sha256,
705
+ });
706
+ return {
707
+ schema_version: 'dataset-flow-identity-capture-attest.v2',
708
+ request_id: options.requestId,
709
+ environment: options.capture.environment,
710
+ project_ref: options.capture.project_ref,
711
+ actor: options.capture.account,
712
+ target_visibility: 'owner_draft',
713
+ operation_id: options.operationId,
714
+ compatibility_policy: options.policy,
715
+ artifact_evidence: options.capture.artifact_evidence,
716
+ mappings,
717
+ process_intents: processIntents,
718
+ protected_closure: {
719
+ schema_version: 'dataset-flow-identity-protected-intent.v2',
720
+ pending: options.protectedClosure.pending.map(occurrenceIntent),
721
+ blockers: options.protectedClosure.blockers.map(occurrenceIntent),
722
+ orphans: options.protectedClosure.orphans.map((entry) => ({
723
+ source_id: entry.source_id,
724
+ source_version: entry.source_version,
725
+ evidence_sha256: entry.evidence_sha256,
726
+ })),
727
+ },
728
+ };
729
+ }
730
+ export function buildFlowIdentitySemantics(options) {
731
+ const occurrenceIndex = collectOccurrences(options.capture.process_rows);
732
+ const protectedClosure = buildProtectedClosure(options.review, occurrenceIndex);
733
+ const mappings = buildMappings({
734
+ policy: options.policy,
735
+ review: options.review,
736
+ capture: options.capture,
737
+ validateFlow: options.validation.validateFlow,
738
+ occurrenceIndex,
739
+ });
740
+ const supportSnapshots = buildSupportSnapshots({
741
+ mappings,
742
+ capture: options.capture,
743
+ });
744
+ const processTemplates = buildProcessTemplates({
745
+ capture: options.capture,
746
+ mappings,
747
+ closure: protectedClosure,
748
+ validateProcess: options.validation.validateProcess,
749
+ });
750
+ return { protectedClosure, mappings, supportSnapshots, processTemplates };
751
+ }
752
+ export function buildFlowIdentityPlan(options) {
753
+ const policy = parseFlowIdentityPolicy(options.policy);
754
+ const review = parseFlowIdentityReviewLedger(options.reviewLedger);
755
+ const capture = parseFlowIdentityCapture(options.liveCapture);
756
+ if (policy.evidence_resolution_sha256 !== review.review_evidence_sha256) {
757
+ fail('Approved policy does not bind the supplied v3 evidence resolution ledger.');
758
+ }
759
+ if (capture.artifact_evidence.review_ledger_sha256 !== review.ledger_sha256 ||
760
+ Date.parse(capture.attestation.expires_at) <= (options.now ?? new Date()).getTime()) {
761
+ fail('The v2 capture receipt is foreign, expired, or does not bind the review ledger.');
762
+ }
763
+ const validation = {
764
+ validateFlow: options.validation?.validateFlow ?? validateFlowPayload,
765
+ validateProcess: options.validation?.validateProcess ?? validateProcessPayload,
766
+ };
767
+ const { protectedClosure, mappings, supportSnapshots, processTemplates } = buildFlowIdentitySemantics({ policy, review, capture, validation });
768
+ const expectedCaptureRequest = buildFlowIdentityCaptureRequest({
769
+ requestId: capture.capture_request.request_id,
770
+ operationId: capture.attestation.operation_id,
771
+ policy,
772
+ capture,
773
+ mappings,
774
+ processTemplates,
775
+ protectedClosure,
776
+ });
777
+ const expectedCaptureRequestSha256 = flowIdentityRestrictedSha256(expectedCaptureRequest);
778
+ if (expectedCaptureRequestSha256 !== capture.attestation.capture_request_sha256 ||
779
+ expectedCaptureRequestSha256 !==
780
+ flowIdentityRestrictedSha256(capture.capture_request)) {
781
+ fail('The authenticated v2 receipt does not bind the exact local mapping, locator, process, and protected-closure semantics.');
782
+ }
783
+ const processes = processTemplates.map((template) => template.process);
784
+ const collisionCount = processTemplates.reduce((sum, template) => sum + template.collision_ledger.entries.length, 0);
785
+ const sourceUniverse = review.entries
786
+ .map((entry) => ({
787
+ id: entry.source.id,
788
+ version: entry.source.version,
789
+ user_id: capture.account.user_id,
790
+ state_code: 0,
791
+ flow_type: 'Elementary flow',
792
+ }))
793
+ .sort((left, right) => rowKey(left.id, left.version).localeCompare(rowKey(right.id, right.version)));
794
+ const body = {
795
+ schema_version: 'dataset-flow-identity-plan.v2',
796
+ generated_at_utc: (options.now ?? new Date()).toISOString(),
797
+ environment: capture.environment,
798
+ project_ref: capture.project_ref,
799
+ account: capture.account,
800
+ operation_id: capture.attestation.operation_id,
801
+ status: 'ready',
802
+ target_visibility: 'owner_draft',
803
+ review_ledger_sha256: review.ledger_sha256,
804
+ capture_artifact_sha256: computeFlowIdentityCaptureSha256(capture),
805
+ receipt_id: capture.attestation.receipt_id,
806
+ receipt_proof_sha256: capture.attestation.receipt_proof_sha256,
807
+ capture_request_sha256: capture.attestation.capture_request_sha256,
808
+ source_guard_set_sha256: capture.attestation.source_guard_set_sha256,
809
+ support_guard_set_sha256: capture.attestation.support_guard_set_sha256,
810
+ target_guard_set_sha256: capture.attestation.target_guard_set_sha256,
811
+ mapping_guard_set_sha256: capture.attestation.mapping_guard_set_sha256,
812
+ process_intent_set_sha256: capture.attestation.process_intent_set_sha256,
813
+ receipt_protected_closure_sha256: capture.attestation.protected_closure_sha256,
814
+ capture_whole_scope_proof_sha256: capture.attestation.whole_scope_proof_sha256,
815
+ source_universe_artifact_sha256: sha256Json(sourceUniverse),
816
+ compatibility_policy: policy,
817
+ support_snapshot_artifact_sha256: sha256Json(supportSnapshots),
818
+ mapping_artifact_sha256: sha256Json(mappings),
819
+ process_manifest_artifact_sha256: sha256Json(processes),
820
+ protected_closure_artifact_sha256: sha256Json(protectedClosure),
821
+ support_snapshots: supportSnapshots,
822
+ mappings,
823
+ processes,
824
+ protected_closure: protectedClosure,
825
+ summary: {
826
+ semantic_sources: 305,
827
+ mappings: mappings.length,
828
+ processes: processes.length,
829
+ rewrites: processes.reduce((sum, process) => sum + process.rewrite_count, 0),
830
+ collision_entries: collisionCount,
831
+ pending: protectedClosure.pending.length,
832
+ blockers: protectedClosure.blockers.length,
833
+ orphans: protectedClosure.orphans.length,
834
+ protected_references: protectedClosure.total_expected_reference_count,
835
+ },
836
+ artifacts: {
837
+ plan: 'flow-identity-plan.json',
838
+ live_capture: 'flow-identity-live-capture.json',
839
+ process_manifest: 'flow-identity-process-manifest.jsonl',
840
+ collision_ledger: 'flow-identity-collision-ledger.jsonl',
841
+ protected_closure: 'flow-identity-protected-closure.json',
842
+ desired_payload_dir: 'desired-processes',
843
+ process_request_dir: 'process-requests',
844
+ },
845
+ plan_sha256: '',
846
+ };
847
+ if (capture.attestation.mapping_count !== mappings.length ||
848
+ capture.attestation.process_count !== processes.length ||
849
+ capture.attestation.rewrite_count !== body.summary.rewrites) {
850
+ fail('The v2 database receipt counts do not match the deterministic semantic plan.');
851
+ }
852
+ body.plan_sha256 = computeFlowIdentityPlanSha256(body);
853
+ return { plan: body, process_templates: processTemplates };
854
+ }
855
+ export function runFlowIdentityPlan(options) {
856
+ const bundle = buildFlowIdentityPlan(options);
857
+ const outDir = ensurePrivateArtifactDirectory(path.resolve(options.outDir));
858
+ writePrivateImmutableJson(path.join(outDir, bundle.plan.artifacts.plan), bundle.plan);
859
+ writePrivateImmutableJson(path.join(outDir, bundle.plan.artifacts.live_capture), parseFlowIdentityCapture(options.liveCapture));
860
+ writePrivateImmutableText(path.join(outDir, bundle.plan.artifacts.process_manifest), bundle.plan.processes.length ? `${bundle.plan.processes.map(stableJsonText).join('\n')}\n` : '');
861
+ const collisionRows = bundle.process_templates.map((template) => ({
862
+ ordinal: template.process.ordinal,
863
+ process_id: template.process.id,
864
+ process_version: template.process.version,
865
+ ledger: template.collision_ledger,
866
+ }));
867
+ writePrivateImmutableText(path.join(outDir, bundle.plan.artifacts.collision_ledger), collisionRows.length ? `${collisionRows.map(stableJsonText).join('\n')}\n` : '');
868
+ writePrivateImmutableJson(path.join(outDir, bundle.plan.artifacts.protected_closure), bundle.plan.protected_closure);
869
+ for (const template of bundle.process_templates) {
870
+ const stem = `${String(template.process.ordinal).padStart(6, '0')}-${template.process.id}-${template.process.version}`;
871
+ writePrivateImmutableJson(path.join(outDir, bundle.plan.artifacts.desired_payload_dir, `${stem}.json`), template.desired_payload);
872
+ writePrivateImmutableJson(path.join(outDir, bundle.plan.artifacts.process_request_dir, `${stem}.json`), {
873
+ process: template.process,
874
+ rewrites: template.rewrites,
875
+ collision_ledger: template.collision_ledger,
876
+ });
877
+ }
878
+ return bundle.plan;
879
+ }
880
+ export const __testInternals = {
881
+ arrayOfObjects,
882
+ buildProtectedClosure,
883
+ collectOccurrences,
884
+ collisionLedger,
885
+ endpoint,
886
+ exchangeDirection,
887
+ exchangeInternalId,
888
+ exchangeReference,
889
+ flowClassificationInformation,
890
+ flowIdentity,
891
+ flowGuardRowSha256,
892
+ flowSupportFacts,
893
+ flowType,
894
+ indexRows,
895
+ patchReference,
896
+ processGuardRowSha256,
897
+ processExchanges,
898
+ rowKey,
899
+ };
900
+ //# sourceMappingURL=dataset-maintenance-flow-identity-plan.js.map