@tiangong-lca/mcp-server 0.0.27 → 0.0.29

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.
@@ -1,11 +1,12 @@
1
1
  import { createClient, FunctionRegion } from '@supabase/supabase-js';
2
- import { createContact, createFlow, createLifeCycleModel, createProcess, createSource, } from '@tiangong-lca/tidas-sdk/core';
3
2
  import { z } from 'zod';
4
3
  import { supabase_base_url, supabase_publishable_key } from '../_shared/config.js';
5
4
  import { resolveSupabaseAccessToken } from '../_shared/supabase_session.js';
5
+ import { prepareLifecycleModelFile } from './life_cycle_model_file_tools.js';
6
6
  const allowedTables = ['contacts', 'flows', 'lifecyclemodels', 'processes', 'sources'];
7
7
  const tableSchema = z.enum(allowedTables);
8
8
  const UPDATE_FUNCTION_NAME = 'update_data';
9
+ const MAX_VALIDATION_ERROR_LENGTH = 4_000;
9
10
  const tablePrimaryKey = {
10
11
  contacts: 'id',
11
12
  flows: 'id',
@@ -16,21 +17,13 @@ const tablePrimaryKey = {
16
17
  function getPrimaryKeyColumn(table) {
17
18
  return tablePrimaryKey[table] ?? 'id';
18
19
  }
19
- const jsonValueSchema = z.lazy(() => z.union([
20
- z.string(),
21
- z.number(),
22
- z.boolean(),
23
- z.null(),
24
- z.array(jsonValueSchema),
25
- z.record(jsonValueSchema),
26
- ]));
27
20
  const filterValueSchema = z.union([
28
21
  z.string(),
29
22
  z.number(),
30
23
  z.boolean(),
31
24
  z.null(),
32
25
  ]);
33
- const filtersSchema = z.record(filterValueSchema);
26
+ const filtersSchema = z.record(z.string(), filterValueSchema);
34
27
  const toolParamsSchema = {
35
28
  operation: z
36
29
  .enum(['select', 'insert', 'update', 'delete'])
@@ -55,9 +48,10 @@ const toolParamsSchema = {
55
48
  filters: filtersSchema
56
49
  .optional()
57
50
  .describe('Optional equality filters as JSON object, e.g. { "name": "Example" }. Only used for select operations. Leave empty for insert/update/delete operations.'),
58
- jsonOrdered: jsonValueSchema
51
+ jsonOrdered: z
52
+ .unknown()
59
53
  .optional()
60
- .describe('JSON value persisted into json_ordered (required for insert/update; omit for select/delete).'),
54
+ .describe('JSON value persisted into json_ordered (required for insert/update; omit for select/delete). For lifecyclemodels, native files, platform bundles, raw records, or a single-item array of those are accepted; json_tg and rule_verification are derived automatically before write.'),
61
55
  };
62
56
  const refinedInputSchema = z
63
57
  .object(toolParamsSchema)
@@ -123,6 +117,36 @@ const refinedInputSchema = z
123
117
  break;
124
118
  }
125
119
  });
120
+ let tidasValidationFactoryMapPromise;
121
+ function summarizeError(error) {
122
+ if (error instanceof Error) {
123
+ return error.message;
124
+ }
125
+ try {
126
+ const serialized = JSON.stringify(error);
127
+ if (!serialized) {
128
+ return String(error);
129
+ }
130
+ return serialized.length > MAX_VALIDATION_ERROR_LENGTH
131
+ ? `${serialized.slice(0, MAX_VALIDATION_ERROR_LENGTH)}...`
132
+ : serialized;
133
+ }
134
+ catch {
135
+ return String(error);
136
+ }
137
+ }
138
+ async function getTidasValidationFactoryMap() {
139
+ if (!tidasValidationFactoryMapPromise) {
140
+ tidasValidationFactoryMapPromise = import('@tiangong-lca/tidas-sdk/core').then((module) => ({
141
+ contacts: module.createContact,
142
+ flows: module.createFlow,
143
+ lifecyclemodels: module.createLifeCycleModel,
144
+ processes: module.createProcess,
145
+ sources: module.createSource,
146
+ }));
147
+ }
148
+ return tidasValidationFactoryMapPromise;
149
+ }
126
150
  function requireAccessToken(accessToken) {
127
151
  if (!accessToken) {
128
152
  throw new Error('An authenticated Supabase session is required for update operations. Provide a valid access token.');
@@ -135,44 +159,13 @@ function ensureRows(rows, errorMessage) {
135
159
  }
136
160
  return rows;
137
161
  }
138
- function validateJsonOrdered(table, jsonOrdered) {
162
+ async function validateJsonOrdered(table, jsonOrdered) {
139
163
  try {
140
- let validationResult;
141
- switch (table) {
142
- case 'contacts': {
143
- const contact = createContact(jsonOrdered, { mode: 'strict' });
144
- validationResult = contact.validate();
145
- break;
146
- }
147
- case 'flows': {
148
- const flow = createFlow(jsonOrdered, { mode: 'strict' });
149
- validationResult = flow.validate();
150
- break;
151
- }
152
- case 'lifecyclemodels': {
153
- const lifecycleModel = createLifeCycleModel(jsonOrdered, { mode: 'strict' });
154
- validationResult = lifecycleModel.validate();
155
- break;
156
- }
157
- case 'processes': {
158
- const process = createProcess(jsonOrdered, { mode: 'strict' });
159
- validationResult = process.validate();
160
- break;
161
- }
162
- case 'sources': {
163
- const source = createSource(jsonOrdered, { mode: 'strict' });
164
- validationResult = source.validate();
165
- break;
166
- }
167
- default: {
168
- const exhaustiveCheck = table;
169
- throw new Error(`Unsupported table type: ${table}`);
170
- }
171
- }
164
+ const validationFactoryMap = await getTidasValidationFactoryMap();
165
+ const createValidator = validationFactoryMap[table];
166
+ const validationResult = createValidator(jsonOrdered, { mode: 'strict' }).validate();
172
167
  if (!validationResult.success) {
173
- const errorDetails = validationResult.error?.issues
174
- ? JSON.stringify(validationResult.error.issues, null, 2)
175
- : JSON.stringify(validationResult.error);
168
+ const errorDetails = summarizeError(validationResult.error);
176
169
  throw new Error(`Validation failed for table "${table}". Errors: ${errorDetails}`);
177
170
  }
178
171
  }
@@ -183,6 +176,51 @@ function validateJsonOrdered(table, jsonOrdered) {
183
176
  throw error;
184
177
  }
185
178
  }
179
+ function sanitizeLifecycleModelRows(rows) {
180
+ return rows.map((row) => {
181
+ const record = row && typeof row === 'object' && !Array.isArray(row)
182
+ ? row
183
+ : {};
184
+ return {
185
+ id: record.id ?? null,
186
+ version: record.version ?? null,
187
+ json_ordered: record.json_ordered ?? null,
188
+ };
189
+ });
190
+ }
191
+ function sanitizeRowsForOutput(table, rows) {
192
+ return table === 'lifecyclemodels' ? sanitizeLifecycleModelRows(rows) : rows;
193
+ }
194
+ async function prepareWritePayload(table, jsonOrdered, inputId, inputVersion, bearerKey) {
195
+ if (table !== 'lifecyclemodels') {
196
+ await validateJsonOrdered(table, jsonOrdered);
197
+ return {
198
+ payload: {
199
+ json_ordered: jsonOrdered,
200
+ },
201
+ resolvedId: inputId,
202
+ resolvedVersion: inputVersion,
203
+ };
204
+ }
205
+ const prepared = await prepareLifecycleModelFile({
206
+ payload: jsonOrdered,
207
+ }, bearerKey);
208
+ if (inputId && inputId !== prepared.lifecycleModelId) {
209
+ throw new Error(`Provided id (${inputId}) does not match lifecycle model UUID (${prepared.lifecycleModelId}).`);
210
+ }
211
+ if (inputVersion && inputVersion !== prepared.lifecycleModelVersion) {
212
+ throw new Error(`Provided version (${inputVersion}) does not match lifecycle model version (${prepared.lifecycleModelVersion}).`);
213
+ }
214
+ return {
215
+ payload: {
216
+ json_ordered: prepared.jsonOrdered,
217
+ json_tg: prepared.jsonTg,
218
+ rule_verification: prepared.ruleVerification,
219
+ },
220
+ resolvedId: prepared.lifecycleModelId,
221
+ resolvedVersion: prepared.lifecycleModelVersion,
222
+ };
223
+ }
186
224
  async function createSupabaseClient(bearerKey) {
187
225
  const { session: normalizedSession, accessToken: bearerToken } = resolveSupabaseAccessToken(bearerKey);
188
226
  const supabase = createClient(supabase_base_url, supabase_publishable_key, {
@@ -214,7 +252,8 @@ async function createSupabaseClient(bearerKey) {
214
252
  async function handleSelect(supabase, input) {
215
253
  const { table, limit, id, version, filters } = input;
216
254
  const keyColumn = getPrimaryKeyColumn(table);
217
- let queryBuilder = supabase.from(table).select('*');
255
+ const selectColumns = table === 'lifecyclemodels' ? 'id, version, json_ordered' : '*';
256
+ let queryBuilder = supabase.from(table).select(selectColumns);
218
257
  if (filters) {
219
258
  for (const [column, value] of Object.entries(filters)) {
220
259
  if (value !== null && value !== undefined) {
@@ -236,29 +275,40 @@ async function handleSelect(supabase, input) {
236
275
  console.error('Error querying the database:', error);
237
276
  throw error;
238
277
  }
239
- return JSON.stringify({ data: data ?? [], count: data?.length ?? 0 });
278
+ const rows = sanitizeRowsForOutput(table, (data ?? []));
279
+ return JSON.stringify({ data: rows, count: rows.length });
240
280
  }
241
- async function handleInsert(supabase, input) {
242
- const { table, jsonOrdered, id } = input;
281
+ async function handleInsert(supabase, input, bearerKey) {
282
+ const { table, jsonOrdered, id, version } = input;
243
283
  if (jsonOrdered === undefined) {
244
284
  throw new Error('jsonOrdered is required for insert operations.');
245
285
  }
246
286
  if (id === undefined) {
247
287
  throw new Error('id is required for insert operations.');
248
288
  }
249
- validateJsonOrdered(table, jsonOrdered);
289
+ const jsonOrderedValue = jsonOrdered;
290
+ const preparedWrite = await prepareWritePayload(table, jsonOrderedValue, id, version, bearerKey);
291
+ const resolvedId = preparedWrite.resolvedId ?? id;
292
+ const resolvedVersion = preparedWrite.resolvedVersion ?? version;
250
293
  const keyColumn = getPrimaryKeyColumn(table);
251
294
  const { data, error } = await supabase
252
295
  .from(table)
253
- .insert([{ [keyColumn]: id, json_ordered: jsonOrdered }])
296
+ .insert([
297
+ {
298
+ [keyColumn]: resolvedId,
299
+ ...(resolvedVersion !== undefined ? { version: resolvedVersion } : {}),
300
+ ...preparedWrite.payload,
301
+ },
302
+ ])
254
303
  .select();
255
304
  if (error) {
256
305
  console.error('Error inserting into the database:', error);
257
306
  throw error;
258
307
  }
259
- return JSON.stringify({ id, data: data ?? [] });
308
+ const rows = sanitizeRowsForOutput(table, (data ?? []));
309
+ return JSON.stringify({ id: resolvedId, version: resolvedVersion, data: rows });
260
310
  }
261
- async function handleUpdate(supabase, accessToken, input) {
311
+ async function handleUpdate(supabase, accessToken, input, bearerKey) {
262
312
  const { table, id, version, jsonOrdered } = input;
263
313
  if (id === undefined) {
264
314
  throw new Error('id is required for update operations.');
@@ -269,13 +319,19 @@ async function handleUpdate(supabase, accessToken, input) {
269
319
  if (jsonOrdered === undefined) {
270
320
  throw new Error('jsonOrdered is required for update operations.');
271
321
  }
272
- validateJsonOrdered(table, jsonOrdered);
322
+ const jsonOrderedValue = jsonOrdered;
323
+ const preparedWrite = await prepareWritePayload(table, jsonOrderedValue, id, version, bearerKey);
273
324
  const token = requireAccessToken(accessToken);
274
325
  const { data: functionPayload, error } = await supabase.functions.invoke(UPDATE_FUNCTION_NAME, {
275
326
  headers: {
276
327
  Authorization: `Bearer ${token}`,
277
328
  },
278
- body: { id, version, table, data: { json_ordered: jsonOrdered } },
329
+ body: {
330
+ id: preparedWrite.resolvedId ?? id,
331
+ version: preparedWrite.resolvedVersion ?? version,
332
+ table,
333
+ data: preparedWrite.payload,
334
+ },
279
335
  region: FunctionRegion.UsEast1,
280
336
  });
281
337
  if (error) {
@@ -290,8 +346,12 @@ async function handleUpdate(supabase, accessToken, input) {
290
346
  throw new Error(message);
291
347
  }
292
348
  const keyColumn = getPrimaryKeyColumn(table);
293
- const rows = ensureRows(updatedRows, `Update affected 0 rows for table "${table}"; verify the provided ${keyColumn} (${id}) and version (${version}) exist and are accessible.`);
294
- return JSON.stringify({ id, version, data: rows });
349
+ const rows = ensureRows(updatedRows, `Update affected 0 rows for table "${table}"; verify the provided ${keyColumn} (${preparedWrite.resolvedId ?? id}) and version (${preparedWrite.resolvedVersion ?? version}) exist and are accessible.`);
350
+ return JSON.stringify({
351
+ id: preparedWrite.resolvedId ?? id,
352
+ version: preparedWrite.resolvedVersion ?? version,
353
+ data: sanitizeRowsForOutput(table, rows),
354
+ });
295
355
  }
296
356
  async function handleDelete(supabase, input) {
297
357
  const { table, id, version } = input;
@@ -313,7 +373,7 @@ async function handleDelete(supabase, input) {
313
373
  throw error;
314
374
  }
315
375
  const rows = ensureRows(data, `Delete affected 0 rows for table "${table}"; verify the provided ${keyColumn} (${id}) and version (${version}) exist and are accessible.`);
316
- return JSON.stringify({ id, version, data: rows });
376
+ return JSON.stringify({ id, version, data: sanitizeRowsForOutput(table, rows) });
317
377
  }
318
378
  async function performCrud(input, bearerKey) {
319
379
  try {
@@ -322,9 +382,9 @@ async function performCrud(input, bearerKey) {
322
382
  case 'select':
323
383
  return handleSelect(supabase, input);
324
384
  case 'insert':
325
- return handleInsert(supabase, input);
385
+ return handleInsert(supabase, input, bearerKey);
326
386
  case 'update':
327
- return handleUpdate(supabase, accessToken, input);
387
+ return handleUpdate(supabase, accessToken, input, bearerKey);
328
388
  case 'delete':
329
389
  return handleDelete(supabase, input);
330
390
  default: {
@@ -339,7 +399,7 @@ async function performCrud(input, bearerKey) {
339
399
  }
340
400
  }
341
401
  export function regCrudTool(server, bearerKey) {
342
- server.tool('Database_CRUD_Tool', 'Perform select/insert/update/delete against allowed Supabase tables (insert needs jsonOrdered, update/delete need id and version).', toolParamsSchema, async (rawInput) => {
402
+ server.tool('Database_CRUD_Tool', 'Perform select/insert/update/delete against allowed Supabase tables (insert needs jsonOrdered, update/delete need id and version). lifecyclemodels insert/update automatically validate the payload, derive platform json_tg, compute rule_verification, and then write the row; lifecyclemodels select returns id/version/json_ordered only.', toolParamsSchema, async (rawInput) => {
343
403
  const input = refinedInputSchema.parse(rawInput);
344
404
  const result = await performCrud(input, bearerKey);
345
405
  return {
@@ -0,0 +1,857 @@
1
+ import dagre from '@dagrejs/dagre';
2
+ import { createClient } from '@supabase/supabase-js';
3
+ import { createLifeCycleModel } from '@tiangong-lca/tidas-sdk/core';
4
+ import { supabase_base_url, supabase_publishable_key } from '../_shared/config.js';
5
+ import { resolveSupabaseAccessToken } from '../_shared/supabase_session.js';
6
+ const MAX_VALIDATION_ERROR_LENGTH = 4_000;
7
+ const NODE_WIDTH = 350;
8
+ const NODE_MIN_HEIGHT = 100;
9
+ const PORT_START_Y = 65;
10
+ const PORT_STEP_Y = 20;
11
+ const PAIRED_INPUT_START_Y = 58;
12
+ const PAIRED_OUTPUT_START_Y = 78;
13
+ const PAIRED_PORT_STEP_Y = 40;
14
+ const MIN_NODE_SIZE = 1;
15
+ const DAGRE_RANKDIR = 'LR';
16
+ const DAGRE_NODESEP = 88;
17
+ const DAGRE_EDGESEP = 24;
18
+ const DAGRE_RANKSEP = 170;
19
+ const DAGRE_MARGIN_X = 36;
20
+ const DAGRE_MARGIN_Y = 36;
21
+ const PRIMARY_COLOR = '#5c246a';
22
+ const BACKGROUND_COLOR = '#ffffff';
23
+ const MUTED_TEXT_COLOR = 'rgba(0,0,0,0.45)';
24
+ const BODY_TEXT_COLOR = '#000';
25
+ function ensureArray(value) {
26
+ if (value === null || value === undefined) {
27
+ return [];
28
+ }
29
+ return Array.isArray(value) ? value : [value];
30
+ }
31
+ function cloneJson(value) {
32
+ return JSON.parse(JSON.stringify(value));
33
+ }
34
+ function asRecord(value) {
35
+ return value && typeof value === 'object' && !Array.isArray(value)
36
+ ? value
37
+ : {};
38
+ }
39
+ function toJsonRecord(value, message) {
40
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
41
+ throw new Error(message);
42
+ }
43
+ return value;
44
+ }
45
+ function parsePayload(value) {
46
+ if (typeof value === 'string') {
47
+ return JSON.parse(value);
48
+ }
49
+ return value;
50
+ }
51
+ function summarizeError(error) {
52
+ if (error instanceof Error) {
53
+ return error.message;
54
+ }
55
+ try {
56
+ const serialized = JSON.stringify(error);
57
+ if (!serialized) {
58
+ return String(error);
59
+ }
60
+ return serialized.length > MAX_VALIDATION_ERROR_LENGTH
61
+ ? `${serialized.slice(0, MAX_VALIDATION_ERROR_LENGTH)}...`
62
+ : serialized;
63
+ }
64
+ catch {
65
+ return String(error);
66
+ }
67
+ }
68
+ function normalizeLifecycleModelPayload(rawPayload) {
69
+ const parsed = parsePayload(rawPayload);
70
+ const singlePayload = Array.isArray(parsed) ? parsed[0] : parsed;
71
+ if (Array.isArray(parsed) && parsed.length !== 1) {
72
+ throw new Error('Lifecycle model file import currently supports exactly one lifecycle model object per request.');
73
+ }
74
+ const payload = asRecord(singlePayload);
75
+ if (payload.json_ordered) {
76
+ return {
77
+ jsonOrdered: toJsonRecord(payload.json_ordered, 'payload.json_ordered must be an object.'),
78
+ providedJsonTg: payload.json_tg
79
+ ? toJsonRecord(payload.json_tg, 'payload.json_tg must be an object.')
80
+ : undefined,
81
+ sourceFormat: 'raw_record',
82
+ };
83
+ }
84
+ if (payload.jsonOrdered) {
85
+ return {
86
+ jsonOrdered: toJsonRecord(payload.jsonOrdered, 'payload.jsonOrdered must be an object.'),
87
+ providedJsonTg: payload.jsonTg
88
+ ? toJsonRecord(payload.jsonTg, 'payload.jsonTg must be an object.')
89
+ : undefined,
90
+ sourceFormat: 'direct_fields',
91
+ };
92
+ }
93
+ if (payload.lifeCycleModelDataSet) {
94
+ const { json_tg, ...jsonOrdered } = payload;
95
+ return {
96
+ jsonOrdered: toJsonRecord(jsonOrdered, 'payload must contain a lifeCycleModelDataSet object.'),
97
+ providedJsonTg: json_tg
98
+ ? toJsonRecord(json_tg, 'payload.json_tg must be an object.')
99
+ : undefined,
100
+ sourceFormat: json_tg ? 'platform_bundle' : 'native',
101
+ };
102
+ }
103
+ throw new Error('Unsupported lifecycle model payload. Provide { lifeCycleModelDataSet }, { lifeCycleModelDataSet, json_tg }, { json_ordered, json_tg }, or an array containing exactly one of those objects.');
104
+ }
105
+ function getModelDataSet(jsonOrdered) {
106
+ const dataSet = jsonOrdered.lifeCycleModelDataSet;
107
+ if (!dataSet || typeof dataSet !== 'object' || Array.isArray(dataSet)) {
108
+ throw new Error('jsonOrdered.lifeCycleModelDataSet is required.');
109
+ }
110
+ return dataSet;
111
+ }
112
+ function getModelUuid(jsonOrdered) {
113
+ const dataSet = getModelDataSet(jsonOrdered);
114
+ const uuid = asRecord(asRecord(asRecord(dataSet.lifeCycleModelInformation).dataSetInformation))['common:UUID'];
115
+ if (typeof uuid !== 'string' || uuid.length === 0) {
116
+ throw new Error('lifeCycleModelInformation.dataSetInformation.common:UUID is required in the lifecycle model.');
117
+ }
118
+ return uuid;
119
+ }
120
+ function getModelVersion(jsonOrdered) {
121
+ const dataSet = getModelDataSet(jsonOrdered);
122
+ const version = asRecord(asRecord(dataSet.administrativeInformation).publicationAndOwnership)['common:dataSetVersion'];
123
+ if (typeof version !== 'string' || version.length === 0) {
124
+ throw new Error('administrativeInformation.publicationAndOwnership.common:dataSetVersion is required in the lifecycle model.');
125
+ }
126
+ return version;
127
+ }
128
+ function createLifecycleModelValidator(jsonOrdered) {
129
+ return createLifeCycleModel(jsonOrdered, { mode: 'strict' });
130
+ }
131
+ function validateLifecycleModelStrict(validator) {
132
+ const validationResult = validator.validate();
133
+ if (!validationResult.success) {
134
+ const errorDetails = summarizeError(validationResult.error);
135
+ throw new Error(`Lifecycle model validation failed: ${errorDetails}`);
136
+ }
137
+ }
138
+ function langEntries(value) {
139
+ const entries = ensureArray(value).filter((item) => item && typeof item === 'object');
140
+ if (entries.length > 0) {
141
+ return cloneJson(entries);
142
+ }
143
+ if (typeof value === 'string' && value.trim().length > 0) {
144
+ return [{ '@xml:lang': 'en', '#text': value.trim() }];
145
+ }
146
+ return [];
147
+ }
148
+ function preferredText(value) {
149
+ const entries = langEntries(value);
150
+ const preferredOrder = ['zh', 'zh-cn', 'zh-hans', 'en'];
151
+ for (const lang of preferredOrder) {
152
+ const match = entries.find((item) => (item['@xml:lang'] || '').toLowerCase() === lang);
153
+ if (match?.['#text']) {
154
+ return match['#text'];
155
+ }
156
+ }
157
+ return entries[0]?.['#text'] ?? '';
158
+ }
159
+ function buildSyntheticName(shortDescription) {
160
+ return {
161
+ baseName: langEntries(shortDescription),
162
+ treatmentStandardsRoutes: [],
163
+ mixAndLocationTypes: [],
164
+ functionalUnitFlowProperties: [],
165
+ };
166
+ }
167
+ function buildNameSummary(name) {
168
+ const nameRecord = asRecord(name);
169
+ const partMap = new Map();
170
+ const keys = [
171
+ 'baseName',
172
+ 'treatmentStandardsRoutes',
173
+ 'mixAndLocationTypes',
174
+ 'functionalUnitFlowProperties',
175
+ ];
176
+ for (const key of keys) {
177
+ for (const item of langEntries(nameRecord[key])) {
178
+ const lang = (item['@xml:lang'] || 'en').toLowerCase();
179
+ const text = item['#text'] || '';
180
+ if (!text) {
181
+ continue;
182
+ }
183
+ if (!partMap.has(lang)) {
184
+ partMap.set(lang, []);
185
+ }
186
+ partMap.get(lang).push(text);
187
+ }
188
+ }
189
+ return Array.from(partMap.entries())
190
+ .map(([lang, parts]) => ({
191
+ '@xml:lang': lang,
192
+ '#text': parts.filter(Boolean).join('; '),
193
+ }))
194
+ .filter((item) => item['#text'].length > 0);
195
+ }
196
+ function extractInternalId(value) {
197
+ if (typeof value === 'string') {
198
+ return value;
199
+ }
200
+ const candidate = asRecord(value)['@id'];
201
+ return typeof candidate === 'string' ? candidate : '';
202
+ }
203
+ function processInstancesFromModel(jsonOrdered) {
204
+ const dataSet = getModelDataSet(jsonOrdered);
205
+ return ensureArray(asRecord(asRecord(asRecord(asRecord(dataSet.lifeCycleModelInformation).technology).processes))
206
+ .processInstance).map((item) => asRecord(item));
207
+ }
208
+ function processInstanceInternalId(instance) {
209
+ return String(instance['@dataSetInternalID'] ?? '').trim();
210
+ }
211
+ function graphProcessInstancesFromModel(jsonOrdered) {
212
+ const processInstances = processInstancesFromModel(jsonOrdered);
213
+ const missingInternalIdIndexes = processInstances.flatMap((instance, index) => processInstanceInternalId(instance) ? [] : [index]);
214
+ if (missingInternalIdIndexes.length > 0) {
215
+ throw new Error(`Lifecycle model graph generation requires processInstance.@dataSetInternalID for every process. Missing values at indexes: ${missingInternalIdIndexes.join(', ')}.`);
216
+ }
217
+ return processInstances;
218
+ }
219
+ function referenceProcessInternalIdFromModel(jsonOrdered) {
220
+ const dataSet = getModelDataSet(jsonOrdered);
221
+ const quantitativeReference = asRecord(asRecord(dataSet.lifeCycleModelInformation).quantitativeReference);
222
+ return extractInternalId(quantitativeReference.referenceToReferenceProcess);
223
+ }
224
+ function modelEdgesFromConnections(processInstances) {
225
+ const seen = new Set();
226
+ const edges = [];
227
+ for (const instance of processInstances) {
228
+ const srcInternalId = processInstanceInternalId(instance);
229
+ if (!srcInternalId) {
230
+ continue;
231
+ }
232
+ const outputExchanges = ensureArray(asRecord(instance.connections).outputExchange);
233
+ for (const outputExchange of outputExchanges.map((item) => asRecord(item))) {
234
+ const flowUuid = String(outputExchange['@flowUUID'] ?? '').trim();
235
+ if (!flowUuid) {
236
+ continue;
237
+ }
238
+ const downstreamProcesses = ensureArray(asRecord(outputExchange).downstreamProcess).map((item) => asRecord(item));
239
+ for (const downstreamProcess of downstreamProcesses) {
240
+ const dstInternalId = String(downstreamProcess['@id'] ?? '').trim();
241
+ if (!dstInternalId) {
242
+ continue;
243
+ }
244
+ const key = `${srcInternalId}|${dstInternalId}|${flowUuid}`;
245
+ if (seen.has(key)) {
246
+ continue;
247
+ }
248
+ seen.add(key);
249
+ edges.push({ srcInternalId, dstInternalId, flowUuid });
250
+ }
251
+ }
252
+ }
253
+ return edges;
254
+ }
255
+ async function createSupabaseClient(bearerKey) {
256
+ const { session: normalizedSession, accessToken: bearerToken } = resolveSupabaseAccessToken(bearerKey);
257
+ const supabase = createClient(supabase_base_url, supabase_publishable_key, {
258
+ auth: {
259
+ persistSession: false,
260
+ autoRefreshToken: Boolean(normalizedSession?.refresh_token),
261
+ },
262
+ ...(bearerToken
263
+ ? {
264
+ global: {
265
+ headers: {
266
+ Authorization: `Bearer ${bearerToken}`,
267
+ },
268
+ },
269
+ }
270
+ : {}),
271
+ });
272
+ if (normalizedSession?.refresh_token) {
273
+ const { error } = await supabase.auth.setSession({
274
+ access_token: normalizedSession.access_token,
275
+ refresh_token: normalizedSession.refresh_token,
276
+ });
277
+ if (error) {
278
+ console.warn('Failed to set Supabase session for lifecycle model file tools:', error.message);
279
+ }
280
+ }
281
+ return { supabase };
282
+ }
283
+ function buildFallbackProcessLookup(processId, version, referenceToProcess) {
284
+ const fallbackShortDescription = langEntries(referenceToProcess['common:shortDescription']);
285
+ const fallbackLabel = buildSyntheticName(fallbackShortDescription);
286
+ const fallbackSummary = buildNameSummary(fallbackLabel);
287
+ return {
288
+ processId,
289
+ version,
290
+ shortDescription: fallbackSummary,
291
+ label: fallbackLabel,
292
+ shortSummary: fallbackSummary,
293
+ exchangeByDirectionAndFlow: new Map(),
294
+ };
295
+ }
296
+ function processSelectionKey(processId, version) {
297
+ return `${processId}@@${version}`;
298
+ }
299
+ function extractProcessDataSet(row) {
300
+ return asRecord(asRecord(row?.json_ordered).processDataSet);
301
+ }
302
+ async function loadReferencedProcessDataSets(supabase, processInstances) {
303
+ const versionedIdsByVersion = new Map();
304
+ const unversionedIds = new Set();
305
+ for (const instance of processInstances) {
306
+ const referenceToProcess = asRecord(instance.referenceToProcess);
307
+ const processId = String(referenceToProcess['@refObjectId'] ?? '').trim();
308
+ const version = String(referenceToProcess['@version'] ?? '').trim();
309
+ if (!processId) {
310
+ continue;
311
+ }
312
+ if (version) {
313
+ if (!versionedIdsByVersion.has(version)) {
314
+ versionedIdsByVersion.set(version, new Set());
315
+ }
316
+ versionedIdsByVersion.get(version).add(processId);
317
+ continue;
318
+ }
319
+ unversionedIds.add(processId);
320
+ }
321
+ const processDataSetBySelection = new Map();
322
+ const batchFetches = [];
323
+ for (const [version, processIds] of versionedIdsByVersion.entries()) {
324
+ const ids = Array.from(processIds);
325
+ if (ids.length === 0) {
326
+ continue;
327
+ }
328
+ batchFetches.push((async () => {
329
+ const { data, error } = await supabase
330
+ .from('processes')
331
+ .select('id, version, json_ordered')
332
+ .eq('version', version)
333
+ .in('id', ids);
334
+ if (error) {
335
+ throw new Error(`Failed to load referenced processes for version ${version}: ${error.message}`);
336
+ }
337
+ for (const row of (data ?? [])) {
338
+ const processId = String(row.id ?? '').trim();
339
+ if (!processId) {
340
+ continue;
341
+ }
342
+ processDataSetBySelection.set(processSelectionKey(processId, version), extractProcessDataSet(row));
343
+ }
344
+ })());
345
+ }
346
+ if (unversionedIds.size > 0) {
347
+ batchFetches.push((async () => {
348
+ const { data, error } = await supabase
349
+ .from('processes')
350
+ .select('id, version, json_ordered')
351
+ .in('id', Array.from(unversionedIds))
352
+ .order('version', { ascending: false });
353
+ if (error) {
354
+ throw new Error(`Failed to load referenced processes without version: ${error.message}`);
355
+ }
356
+ for (const row of (data ?? [])) {
357
+ const processId = String(row.id ?? '').trim();
358
+ if (!processId || processDataSetBySelection.has(processSelectionKey(processId, ''))) {
359
+ continue;
360
+ }
361
+ processDataSetBySelection.set(processSelectionKey(processId, ''), extractProcessDataSet(row));
362
+ }
363
+ })());
364
+ }
365
+ await Promise.all(batchFetches);
366
+ return processDataSetBySelection;
367
+ }
368
+ async function fetchProcessLookups(supabase, processInstances) {
369
+ const lookups = new Map();
370
+ const processDataSetBySelection = await loadReferencedProcessDataSets(supabase, processInstances);
371
+ const fetches = processInstances.map(async (instance) => {
372
+ const referenceToProcess = asRecord(instance.referenceToProcess);
373
+ const processId = String(referenceToProcess['@refObjectId'] ?? '').trim();
374
+ const version = String(referenceToProcess['@version'] ?? '').trim();
375
+ const internalId = processInstanceInternalId(instance);
376
+ if (!internalId) {
377
+ return;
378
+ }
379
+ if (!processId) {
380
+ lookups.set(internalId, buildFallbackProcessLookup(processId, version, referenceToProcess));
381
+ return;
382
+ }
383
+ const fallbackLookup = buildFallbackProcessLookup(processId, version, referenceToProcess);
384
+ const processDataSet = processDataSetBySelection.get(processSelectionKey(processId, version));
385
+ if (Object.keys(processDataSet ?? {}).length === 0) {
386
+ lookups.set(internalId, fallbackLookup);
387
+ return;
388
+ }
389
+ const info = asRecord(asRecord(asRecord(processDataSet).processInformation).dataSetInformation);
390
+ const label = Object.keys(asRecord(info.name)).length > 0
391
+ ? cloneJson(asRecord(info.name))
392
+ : fallbackLookup.label;
393
+ const shortSummary = buildNameSummary(label);
394
+ const exchangeByDirectionAndFlow = new Map();
395
+ let referenceExchange;
396
+ const refExchangeInternalId = String(asRecord(asRecord(asRecord(processDataSet).processInformation).quantitativeReference)
397
+ .referenceToReferenceFlow ?? '').trim();
398
+ for (const exchange of ensureArray(asRecord(processDataSet).exchanges?.exchange).map((item) => asRecord(item))) {
399
+ const flowRef = asRecord(exchange.referenceToFlowDataSet);
400
+ const flowId = String(flowRef['@refObjectId'] ?? '').trim();
401
+ const direction = String(exchange.exchangeDirection ?? '').trim();
402
+ if (flowId && direction) {
403
+ exchangeByDirectionAndFlow.set(`${direction}:${flowId}`, exchange);
404
+ }
405
+ if ((!referenceExchange && exchange.quantitativeReference === true) ||
406
+ (refExchangeInternalId &&
407
+ String(exchange['@dataSetInternalID'] ?? '').trim() === refExchangeInternalId)) {
408
+ referenceExchange = exchange;
409
+ }
410
+ }
411
+ lookups.set(internalId, {
412
+ processId,
413
+ version,
414
+ shortDescription: shortSummary.length > 0
415
+ ? shortSummary
416
+ : langEntries(referenceToProcess['common:shortDescription']),
417
+ label,
418
+ shortSummary: shortSummary.length > 0
419
+ ? shortSummary
420
+ : langEntries(referenceToProcess['common:shortDescription']),
421
+ referenceExchange,
422
+ exchangeByDirectionAndFlow,
423
+ });
424
+ });
425
+ await Promise.all(fetches);
426
+ return lookups;
427
+ }
428
+ function flowPortFallback(flowUuid) {
429
+ return [{ '@xml:lang': 'en', '#text': flowUuid }];
430
+ }
431
+ function exchangeAmount(exchange) {
432
+ if (!exchange) {
433
+ return '';
434
+ }
435
+ const candidate = exchange.meanAmount ?? exchange.resultingAmount ?? exchange.meanValue;
436
+ return candidate === undefined || candidate === null ? '' : String(candidate);
437
+ }
438
+ function dagreLayout(nodes, edges) {
439
+ const dagreGraph = new dagre.graphlib.Graph({ multigraph: true });
440
+ dagreGraph.setGraph({
441
+ rankdir: DAGRE_RANKDIR,
442
+ nodesep: DAGRE_NODESEP,
443
+ edgesep: DAGRE_EDGESEP,
444
+ ranksep: DAGRE_RANKSEP,
445
+ marginx: DAGRE_MARGIN_X,
446
+ marginy: DAGRE_MARGIN_Y,
447
+ acyclicer: 'greedy',
448
+ ranker: 'network-simplex',
449
+ });
450
+ const nodeIdByInternalId = new Map(nodes.map((node) => [node.internalId, node.nodeId]));
451
+ for (const node of nodes) {
452
+ dagreGraph.setNode(node.nodeId, {
453
+ width: Math.max(node.width, MIN_NODE_SIZE),
454
+ height: Math.max(node.height, MIN_NODE_SIZE),
455
+ });
456
+ }
457
+ for (const edge of edges) {
458
+ const sourceNodeId = nodeIdByInternalId.get(edge.srcInternalId);
459
+ const targetNodeId = nodeIdByInternalId.get(edge.dstInternalId);
460
+ if (!sourceNodeId || !targetNodeId || sourceNodeId === targetNodeId) {
461
+ continue;
462
+ }
463
+ if (!dagreGraph.hasNode(sourceNodeId) || !dagreGraph.hasNode(targetNodeId)) {
464
+ continue;
465
+ }
466
+ dagreGraph.setEdge(sourceNodeId, targetNodeId, {
467
+ minlen: 1,
468
+ weight: 2,
469
+ }, `${sourceNodeId}|${targetNodeId}|${edge.flowUuid}|${edge.srcInternalId}|${edge.dstInternalId}`);
470
+ }
471
+ dagre.layout(dagreGraph);
472
+ const positions = new Map();
473
+ for (const node of nodes) {
474
+ const layoutNode = dagreGraph.node(node.nodeId);
475
+ if (!layoutNode) {
476
+ positions.set(node.internalId, { x: DAGRE_MARGIN_X, y: DAGRE_MARGIN_Y });
477
+ continue;
478
+ }
479
+ positions.set(node.internalId, {
480
+ x: layoutNode.x - layoutNode.width / 2,
481
+ y: layoutNode.y - layoutNode.height / 2,
482
+ });
483
+ }
484
+ return positions;
485
+ }
486
+ function buildPortItem(spec, y) {
487
+ return {
488
+ id: `${spec.side}:${spec.flowUuid}`,
489
+ group: spec.side === 'INPUT' ? 'groupInput' : 'groupOutput',
490
+ args: {
491
+ x: spec.side === 'INPUT' ? 0 : '100%',
492
+ y,
493
+ },
494
+ attrs: {
495
+ text: {
496
+ text: spec.displayText,
497
+ title: spec.displayText,
498
+ cursor: 'pointer',
499
+ fill: spec.quantitativeReference ? PRIMARY_COLOR : MUTED_TEXT_COLOR,
500
+ 'font-weight': spec.quantitativeReference ? 'bold' : 'normal',
501
+ },
502
+ },
503
+ data: {
504
+ textLang: spec.textLang,
505
+ flowId: spec.flowUuid,
506
+ flowVersion: spec.flowVersion,
507
+ quantitativeReference: spec.quantitativeReference,
508
+ allocations: spec.allocations,
509
+ },
510
+ tools: [{ id: 'portTool' }],
511
+ };
512
+ }
513
+ function buildPortSpec(side, flowUuid, exchange, overrideQuantitativeReference = false) {
514
+ const flowRef = asRecord(exchange?.referenceToFlowDataSet);
515
+ const textLang = langEntries(flowRef['common:shortDescription']).length
516
+ ? langEntries(flowRef['common:shortDescription'])
517
+ : flowPortFallback(flowUuid);
518
+ return {
519
+ side,
520
+ flowUuid,
521
+ flowVersion: String(flowRef['@version'] ?? '').trim(),
522
+ textLang,
523
+ displayText: preferredText(textLang) || flowUuid,
524
+ quantitativeReference: overrideQuantitativeReference || exchange?.quantitativeReference === true,
525
+ allocations: exchange?.allocations,
526
+ };
527
+ }
528
+ function mergeJsonTg(generated, provided, preferProvidedJsonTg) {
529
+ if (!provided || !preferProvidedJsonTg) {
530
+ return { jsonTg: generated, source: 'generated' };
531
+ }
532
+ const providedXflow = asRecord(provided.xflow);
533
+ const providedSubmodels = ensureArray(provided.submodels);
534
+ const jsonTg = {
535
+ ...cloneJson(generated),
536
+ ...cloneJson(provided),
537
+ xflow: Object.keys(providedXflow).length > 0
538
+ ? cloneJson(providedXflow)
539
+ : cloneJson(asRecord(generated.xflow)),
540
+ submodels: providedSubmodels.length > 0
541
+ ? cloneJson(providedSubmodels)
542
+ : cloneJson(ensureArray(generated.submodels)),
543
+ };
544
+ return { jsonTg, source: 'merged' };
545
+ }
546
+ function generateJsonTg(jsonOrdered, processInstances, processLookups) {
547
+ const referenceProcessInternalId = referenceProcessInternalIdFromModel(jsonOrdered);
548
+ const fallbackReferenceProcessInstance = processInstances[processInstances.length - 1];
549
+ const resolvedReferenceProcessInternalId = referenceProcessInternalId ||
550
+ String(fallbackReferenceProcessInstance?.['@dataSetInternalID'] ?? '').trim();
551
+ const edges = modelEdgesFromConnections(processInstances);
552
+ const outgoingEdges = new Map();
553
+ const incomingEdges = new Map();
554
+ for (const edge of edges) {
555
+ if (!outgoingEdges.has(edge.srcInternalId)) {
556
+ outgoingEdges.set(edge.srcInternalId, []);
557
+ }
558
+ outgoingEdges.get(edge.srcInternalId).push(edge);
559
+ if (!incomingEdges.has(edge.dstInternalId)) {
560
+ incomingEdges.set(edge.dstInternalId, []);
561
+ }
562
+ incomingEdges.get(edge.dstInternalId).push(edge);
563
+ }
564
+ const nodeIdCounts = new Map();
565
+ const nodeSpecs = processInstances.map((instance) => {
566
+ const internalId = processInstanceInternalId(instance);
567
+ const multiplicationFactor = String(instance['@multiplicationFactor'] ?? '1');
568
+ const referenceToProcess = asRecord(instance.referenceToProcess);
569
+ const lookup = processLookups.get(internalId);
570
+ const label = lookup?.label ?? buildSyntheticName(referenceToProcess['common:shortDescription']);
571
+ const shortSummary = lookup?.shortSummary && lookup.shortSummary.length > 0
572
+ ? lookup.shortSummary
573
+ : buildNameSummary(label);
574
+ const processId = lookup?.processId || String(referenceToProcess['@refObjectId'] ?? '').trim();
575
+ const processVersion = lookup?.version || String(referenceToProcess['@version'] ?? '').trim();
576
+ const baseNodeId = processId || internalId;
577
+ const occurrence = (nodeIdCounts.get(baseNodeId) ?? 0) + 1;
578
+ nodeIdCounts.set(baseNodeId, occurrence);
579
+ const nodeId = occurrence === 1 ? baseNodeId : `${baseNodeId}::${internalId}`;
580
+ const portMap = new Map();
581
+ const registerPort = (spec) => {
582
+ const key = `${spec.side}:${spec.flowUuid}`;
583
+ if (!portMap.has(key)) {
584
+ portMap.set(key, spec);
585
+ }
586
+ };
587
+ if (lookup?.referenceExchange) {
588
+ const direction = String(lookup.referenceExchange.exchangeDirection ?? '').toUpperCase();
589
+ const flowId = String(asRecord(lookup.referenceExchange.referenceToFlowDataSet)['@refObjectId'] ?? '').trim();
590
+ if (flowId && (direction === 'INPUT' || direction === 'OUTPUT')) {
591
+ registerPort(buildPortSpec(direction, flowId, lookup.referenceExchange, internalId === resolvedReferenceProcessInternalId));
592
+ }
593
+ }
594
+ for (const edge of incomingEdges.get(internalId) ?? []) {
595
+ registerPort(buildPortSpec('INPUT', edge.flowUuid, lookup?.exchangeByDirectionAndFlow.get(`Input:${edge.flowUuid}`)));
596
+ }
597
+ for (const edge of outgoingEdges.get(internalId) ?? []) {
598
+ registerPort(buildPortSpec('OUTPUT', edge.flowUuid, lookup?.exchangeByDirectionAndFlow.get(`Output:${edge.flowUuid}`)));
599
+ }
600
+ const inputPorts = Array.from(portMap.values()).filter((item) => item.side === 'INPUT');
601
+ const outputPorts = Array.from(portMap.values()).filter((item) => item.side === 'OUTPUT');
602
+ const hasInputs = inputPorts.length > 0;
603
+ const hasOutputs = outputPorts.length > 0;
604
+ const bothSides = hasInputs && hasOutputs;
605
+ const pairCount = Math.max(inputPorts.length, outputPorts.length);
606
+ const height = bothSides
607
+ ? Math.max(NODE_MIN_HEIGHT + 10, 110 + Math.max(pairCount - 1, 0) * PAIRED_PORT_STEP_Y)
608
+ : Math.max(inputPorts.length, outputPorts.length, 2) * PORT_STEP_Y + 60;
609
+ return {
610
+ internalId,
611
+ nodeId,
612
+ processId: processId || nodeId,
613
+ processVersion,
614
+ label,
615
+ shortSummary,
616
+ multiplicationFactor,
617
+ inputPorts,
618
+ outputPorts,
619
+ width: NODE_WIDTH,
620
+ height: Math.max(NODE_MIN_HEIGHT, height),
621
+ isReferenceProcess: internalId === resolvedReferenceProcessInternalId,
622
+ };
623
+ });
624
+ const nodeSpecByInternalId = new Map(nodeSpecs.map((node) => [node.internalId, node]));
625
+ const positions = dagreLayout(nodeSpecs, edges);
626
+ const nodes = nodeSpecs.map((nodeSpec) => {
627
+ const bothSides = nodeSpec.inputPorts.length > 0 && nodeSpec.outputPorts.length > 0;
628
+ const position = positions.get(nodeSpec.internalId) ?? { x: DAGRE_MARGIN_X, y: DAGRE_MARGIN_Y };
629
+ const labelText = preferredText(nodeSpec.shortSummary);
630
+ const portY = (side, index) => {
631
+ if (bothSides) {
632
+ return side === 'INPUT'
633
+ ? PAIRED_INPUT_START_Y + index * PAIRED_PORT_STEP_Y
634
+ : PAIRED_OUTPUT_START_Y + index * PAIRED_PORT_STEP_Y;
635
+ }
636
+ return PORT_START_Y + index * PORT_STEP_Y;
637
+ };
638
+ return {
639
+ id: nodeSpec.nodeId,
640
+ shape: 'rect',
641
+ position,
642
+ size: {
643
+ width: nodeSpec.width,
644
+ height: nodeSpec.height,
645
+ },
646
+ attrs: {
647
+ body: {
648
+ stroke: PRIMARY_COLOR,
649
+ strokeWidth: 1,
650
+ fill: BACKGROUND_COLOR,
651
+ rx: 6,
652
+ ry: 6,
653
+ },
654
+ label: {
655
+ fill: BODY_TEXT_COLOR,
656
+ refX: 0.5,
657
+ refY: 8,
658
+ text: labelText,
659
+ textAnchor: 'middle',
660
+ textVerticalAnchor: 'top',
661
+ },
662
+ text: {
663
+ fill: BODY_TEXT_COLOR,
664
+ text: labelText,
665
+ },
666
+ },
667
+ isMyProcess: true,
668
+ data: {
669
+ id: nodeSpec.processId,
670
+ version: nodeSpec.processVersion,
671
+ index: nodeSpec.internalId,
672
+ label: nodeSpec.label,
673
+ shortDescription: nodeSpec.shortSummary,
674
+ quantitativeReference: nodeSpec.isReferenceProcess ? '1' : '0',
675
+ targetAmount: nodeSpec.isReferenceProcess ? '1' : '',
676
+ multiplicationFactor: nodeSpec.multiplicationFactor,
677
+ },
678
+ ports: {
679
+ groups: {
680
+ groupInput: {
681
+ position: { name: 'absolute' },
682
+ label: { position: { name: 'right' } },
683
+ attrs: {
684
+ circle: {
685
+ stroke: PRIMARY_COLOR,
686
+ fill: BACKGROUND_COLOR,
687
+ strokeWidth: 1,
688
+ r: 4,
689
+ magnet: true,
690
+ },
691
+ text: { fill: MUTED_TEXT_COLOR, fontSize: 14 },
692
+ },
693
+ },
694
+ groupOutput: {
695
+ position: { name: 'absolute' },
696
+ label: { position: { name: 'left' } },
697
+ attrs: {
698
+ circle: {
699
+ stroke: PRIMARY_COLOR,
700
+ fill: BACKGROUND_COLOR,
701
+ strokeWidth: 1,
702
+ r: 4,
703
+ magnet: true,
704
+ },
705
+ text: { fill: MUTED_TEXT_COLOR, fontSize: 14 },
706
+ },
707
+ },
708
+ },
709
+ items: [
710
+ ...nodeSpec.inputPorts.map((item, index) => buildPortItem(item, portY('INPUT', index))),
711
+ ...nodeSpec.outputPorts.map((item, index) => buildPortItem(item, portY('OUTPUT', index))),
712
+ ],
713
+ },
714
+ tools: { name: null, items: [] },
715
+ visible: true,
716
+ zIndex: 1,
717
+ };
718
+ });
719
+ const instanceMap = new Map(processInstances.map((instance) => [processInstanceInternalId(instance), asRecord(instance)]));
720
+ const xflowEdges = edges.map((edge) => {
721
+ const sourceNode = nodeSpecByInternalId.get(edge.srcInternalId);
722
+ const targetNode = nodeSpecByInternalId.get(edge.dstInternalId);
723
+ const sourceLookup = processLookups.get(edge.srcInternalId);
724
+ const targetLookup = processLookups.get(edge.dstInternalId);
725
+ const targetExchange = targetLookup?.exchangeByDirectionAndFlow.get(`Input:${edge.flowUuid}`);
726
+ const sourceProcessId = sourceNode?.processId ??
727
+ sourceLookup?.processId ??
728
+ String(asRecord(instanceMap.get(edge.srcInternalId)?.referenceToProcess)['@refObjectId'] ?? '');
729
+ const targetProcessId = targetNode?.processId ??
730
+ targetLookup?.processId ??
731
+ String(asRecord(instanceMap.get(edge.dstInternalId)?.referenceToProcess)['@refObjectId'] ?? '');
732
+ return {
733
+ id: crypto.randomUUID(),
734
+ shape: 'edge',
735
+ source: { cell: sourceNode?.nodeId ?? edge.srcInternalId, port: `OUTPUT:${edge.flowUuid}` },
736
+ target: { cell: targetNode?.nodeId ?? edge.dstInternalId, port: `INPUT:${edge.flowUuid}` },
737
+ labels: [],
738
+ attrs: {
739
+ line: {
740
+ stroke: PRIMARY_COLOR,
741
+ },
742
+ },
743
+ data: {
744
+ connection: {
745
+ outputExchange: {
746
+ '@flowUUID': edge.flowUuid,
747
+ downstreamProcess: {
748
+ '@id': edge.dstInternalId,
749
+ '@flowUUID': edge.flowUuid,
750
+ },
751
+ },
752
+ isBalanced: true,
753
+ unbalancedAmount: 0,
754
+ exchangeAmount: exchangeAmount(targetExchange),
755
+ },
756
+ node: {
757
+ sourceNodeID: sourceNode?.nodeId ?? edge.srcInternalId,
758
+ sourceProcessId,
759
+ sourceProcessVersion: sourceNode?.processVersion ??
760
+ processLookups.get(edge.srcInternalId)?.version ??
761
+ String(asRecord(instanceMap.get(edge.srcInternalId)?.referenceToProcess)['@version'] ?? ''),
762
+ targetNodeID: targetNode?.nodeId ?? edge.dstInternalId,
763
+ targetProcessId,
764
+ targetProcessVersion: targetNode?.processVersion ??
765
+ processLookups.get(edge.dstInternalId)?.version ??
766
+ String(asRecord(instanceMap.get(edge.dstInternalId)?.referenceToProcess)['@version'] ?? ''),
767
+ },
768
+ },
769
+ zIndex: 4,
770
+ };
771
+ });
772
+ const modelDataSet = getModelDataSet(jsonOrdered);
773
+ const dataSetInformation = asRecord(asRecord(modelDataSet.lifeCycleModelInformation).dataSetInformation);
774
+ const referenceToResultingProcess = asRecord(dataSetInformation.referenceToResultingProcess);
775
+ const referenceProcessInstance = processInstances.find((instance) => String(instance['@dataSetInternalID'] ?? '').trim() === resolvedReferenceProcessInternalId) ?? fallbackReferenceProcessInstance;
776
+ const referenceProcessRef = asRecord(referenceProcessInstance?.referenceToProcess);
777
+ const referenceProcessLookup = processLookups.get(resolvedReferenceProcessInternalId);
778
+ const referenceNodeSpec = nodeSpecByInternalId.get(resolvedReferenceProcessInternalId);
779
+ const referenceExchange = referenceProcessLookup?.referenceExchange;
780
+ const fallbackEdge = (outgoingEdges.get(resolvedReferenceProcessInternalId) ?? [])[0];
781
+ const finalId = {
782
+ nodeId: referenceNodeSpec?.nodeId ||
783
+ String(referenceProcessRef['@refObjectId'] ?? '') ||
784
+ resolvedReferenceProcessInternalId,
785
+ processId: referenceNodeSpec?.processId || String(referenceProcessRef['@refObjectId'] ?? ''),
786
+ };
787
+ if (referenceExchange) {
788
+ finalId.allocatedExchangeFlowId = String(asRecord(referenceExchange.referenceToFlowDataSet)['@refObjectId'] ?? '');
789
+ finalId.allocatedExchangeDirection = String(referenceExchange.exchangeDirection ?? '');
790
+ }
791
+ else if (fallbackEdge) {
792
+ finalId.referenceToFlowDataSet = {
793
+ '@refObjectId': fallbackEdge.flowUuid,
794
+ '@exchangeDirection': 'Output',
795
+ };
796
+ }
797
+ const submodels = [
798
+ {
799
+ id: String(referenceToResultingProcess['@refObjectId'] ?? getModelUuid(jsonOrdered)),
800
+ type: 'primary',
801
+ finalId,
802
+ },
803
+ ];
804
+ return {
805
+ xflow: {
806
+ nodes,
807
+ edges: xflowEdges,
808
+ },
809
+ submodels,
810
+ };
811
+ }
812
+ function deriveRuleVerification(validator) {
813
+ const enhanced = validator.validateEnhanced();
814
+ if (enhanced.success) {
815
+ return { ruleVerification: true, issueCount: 0, filteredIssues: [] };
816
+ }
817
+ const issues = ensureArray(asRecord(enhanced.error).issues);
818
+ const filteredIssues = issues.filter((issue) => {
819
+ const path = ensureArray(asRecord(issue).path).map((part) => String(part));
820
+ return !path.includes('validation') && !path.includes('compliance');
821
+ });
822
+ return {
823
+ ruleVerification: filteredIssues.length === 0,
824
+ issueCount: filteredIssues.length,
825
+ filteredIssues,
826
+ };
827
+ }
828
+ export async function prepareLifecycleModelFile(input, bearerKey) {
829
+ const preferProvidedJsonTg = input.preferProvidedJsonTg ?? false;
830
+ const normalized = normalizeLifecycleModelPayload(input.payload);
831
+ const jsonOrdered = normalized.jsonOrdered;
832
+ const processInstances = graphProcessInstancesFromModel(jsonOrdered);
833
+ const modelId = input.id ?? getModelUuid(jsonOrdered);
834
+ const modelVersion = input.version ?? getModelVersion(jsonOrdered);
835
+ const validator = createLifecycleModelValidator(jsonOrdered);
836
+ validateLifecycleModelStrict(validator);
837
+ const { supabase } = await createSupabaseClient(bearerKey);
838
+ const processLookups = await fetchProcessLookups(supabase, processInstances);
839
+ const generatedJsonTg = generateJsonTg(jsonOrdered, processInstances, processLookups);
840
+ const merged = mergeJsonTg(generatedJsonTg, normalized.providedJsonTg, preferProvidedJsonTg);
841
+ const validation = deriveRuleVerification(validator);
842
+ return {
843
+ sourceFormat: normalized.sourceFormat,
844
+ lifecycleModelId: modelId,
845
+ lifecycleModelVersion: modelVersion,
846
+ jsonTgSource: merged.source,
847
+ processCount: processInstances.length,
848
+ nodeCount: ensureArray(asRecord(merged.jsonTg.xflow).nodes).length,
849
+ edgeCount: ensureArray(asRecord(merged.jsonTg.xflow).edges).length,
850
+ submodelCount: ensureArray(merged.jsonTg.submodels).length,
851
+ ruleVerification: validation.ruleVerification,
852
+ validationIssueCount: validation.issueCount,
853
+ validationIssues: validation.filteredIssues,
854
+ jsonOrdered,
855
+ jsonTg: merged.jsonTg,
856
+ };
857
+ }
@@ -50,7 +50,7 @@ const jsonValueSchema = z.lazy(() => z.union([
50
50
  z.boolean(),
51
51
  z.null(),
52
52
  z.array(jsonValueSchema),
53
- z.record(jsonValueSchema),
53
+ z.record(z.string(), jsonValueSchema),
54
54
  ]));
55
55
  function validateTidasData(entityType, data) {
56
56
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiangong-lca/mcp-server",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "description": "TianGong LCA MCP Server",
5
5
  "license": "MIT",
6
6
  "author": "Nan LI",
@@ -28,19 +28,20 @@
28
28
  "ncu:update": "npx npm-check-updates -u"
29
29
  },
30
30
  "dependencies": {
31
- "@modelcontextprotocol/sdk": "^1.25.3",
32
- "@supabase/supabase-js": "^2.91.1",
33
- "@tiangong-lca/tidas-sdk": "^0.1.29",
31
+ "@dagrejs/dagre": "^2.0.4",
32
+ "@modelcontextprotocol/sdk": "^1.27.1",
33
+ "@supabase/supabase-js": "^2.99.1",
34
+ "@tiangong-lca/tidas-sdk": "^0.1.30",
34
35
  "@types/express": "^5.0.6",
35
- "@upstash/redis": "^1.36.1",
36
+ "@upstash/redis": "^1.37.0",
36
37
  "aws-jwt-verify": "^5.1.1",
37
38
  "olca-ipc": "^2.2.1",
38
- "zod": "^3.25.76"
39
+ "zod": "^4.3.6"
39
40
  },
40
41
  "devDependencies": {
41
- "@modelcontextprotocol/inspector": "^0.19.0",
42
+ "@modelcontextprotocol/inspector": "^0.21.1",
42
43
  "dotenv-cli": "^11.0.0",
43
- "npm-check-updates": "^19.3.1",
44
+ "npm-check-updates": "^19.6.3",
44
45
  "prettier": "^3.8.1",
45
46
  "prettier-plugin-organize-imports": "^4.3.0",
46
47
  "shx": "^0.4.0",