@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.
- package/README.md +90 -2
- package/dist/src/cli.js +553 -4
- package/dist/src/cli.js.map +1 -1
- package/dist/src/lib/dataset-command.js +11 -0
- package/dist/src/lib/dataset-command.js.map +1 -1
- package/dist/src/lib/dataset-maintenance-contract.js.map +1 -1
- package/dist/src/lib/dataset-maintenance-flow-identity-approval-claim.js +175 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-approval-claim.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-capture.js +511 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-capture.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-command.js +26 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-command.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-contract.js +784 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-contract.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-execution-contract.js +1317 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-execution-contract.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-freeze.js +342 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-freeze.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-plan.js +900 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-plan.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-recovery.js +688 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-recovery.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-run.js +1369 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-run.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-seal.js +144 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-seal.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-verify.js +377 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-verify.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-wire.js +178 -0
- package/dist/src/lib/dataset-maintenance-flow-identity-wire.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-remote.js +156 -10
- package/dist/src/lib/dataset-maintenance-remote.js.map +1 -1
- package/dist/src/lib/dataset-save-draft-run.js +667 -0
- package/dist/src/lib/dataset-save-draft-run.js.map +1 -1
- package/dist/src/lib/http.js.map +1 -1
- package/dist/src/lib/lca-release.js +683 -0
- package/dist/src/lib/lca-release.js.map +1 -0
- 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
|