@bpmsoftwaresolutions/ai-engine-client 1.1.14 → 1.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +289 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bpmsoftwaresolutions/ai-engine-client",
3
- "version": "1.1.14",
3
+ "version": "1.1.16",
4
4
  "description": "Thin npm client for the AI Engine operator and retrieval APIs",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/index.js CHANGED
@@ -93,6 +93,37 @@ function isJsonBody(value) {
93
93
  return typeof value === 'object';
94
94
  }
95
95
 
96
+ function cleanText(value) {
97
+ const text = String(value || '').trim();
98
+ return text || null;
99
+ }
100
+
101
+ function isPlainObject(value) {
102
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
103
+ }
104
+
105
+ function matchesExpectedState(actual, expected) {
106
+ if (typeof expected === 'function') {
107
+ throw new Error('matchesExpectedState does not support function expectations.');
108
+ }
109
+ if (Array.isArray(expected)) {
110
+ if (!Array.isArray(actual) || actual.length < expected.length) return false;
111
+ return expected.every((item, index) => matchesExpectedState(actual[index], item));
112
+ }
113
+ if (isPlainObject(expected)) {
114
+ if (!isPlainObject(actual)) return false;
115
+ return Object.entries(expected).every(([key, value]) => matchesExpectedState(actual[key], value));
116
+ }
117
+ return Object.is(actual, expected);
118
+ }
119
+
120
+ function buildVerificationError(message, details = {}) {
121
+ const error = new Error(message);
122
+ error.code = 'POST_CONDITION_VERIFICATION_FAILED';
123
+ error.details = details;
124
+ return error;
125
+ }
126
+
96
127
  export class AIEngineClient {
97
128
  constructor({ baseUrl, accessToken, tokenProvider, apiKey, clientId, actorId, fetchImpl, timeoutMs } = {}) {
98
129
  if (!baseUrl) throw new Error('baseUrl is required.');
@@ -1133,9 +1164,40 @@ export class AIEngineClient {
1133
1164
  }
1134
1165
 
1135
1166
  async updateImplementationItemStatus(implementationItemId, body) {
1136
- return this._request(`/api/governed-implementation/items/${implementationItemId}/status`, {
1167
+ const result = await this._request(`/api/governed-implementation/items/${implementationItemId}/status`, {
1137
1168
  method: 'PATCH', body,
1138
1169
  });
1170
+ const expectedStatus = cleanText(body?.status);
1171
+ const authoritativeItem = isPlainObject(result?.implementation_item) ? result.implementation_item : null;
1172
+ const actualStatus = cleanText(authoritativeItem?.status);
1173
+ if (expectedStatus && !authoritativeItem) {
1174
+ throw buildVerificationError(
1175
+ `updateImplementationItemStatus did not return authoritative item state for ${implementationItemId}.`,
1176
+ {
1177
+ mutation_name: 'updateImplementationItemStatus',
1178
+ mutation_attempted: true,
1179
+ authoritative_read_performed: false,
1180
+ post_condition_verified: false,
1181
+ expected_state: { status: expectedStatus },
1182
+ mutation_result: result ?? null,
1183
+ },
1184
+ );
1185
+ }
1186
+ if (expectedStatus && actualStatus !== expectedStatus) {
1187
+ throw buildVerificationError(
1188
+ `updateImplementationItemStatus returned ${actualStatus ?? 'unknown'} instead of ${expectedStatus}.`,
1189
+ {
1190
+ mutation_name: 'updateImplementationItemStatus',
1191
+ mutation_attempted: true,
1192
+ authoritative_read_performed: true,
1193
+ post_condition_verified: false,
1194
+ expected_state: { status: expectedStatus },
1195
+ verified_current_state: authoritativeItem ?? result ?? null,
1196
+ mutation_result: result ?? null,
1197
+ },
1198
+ );
1199
+ }
1200
+ return result;
1139
1201
  }
1140
1202
 
1141
1203
  async addImplementationItemEvidence(implementationItemId, body) {
@@ -1181,6 +1243,232 @@ export class AIEngineClient {
1181
1243
  return this._request(`/api/governed-implementation/workflows/${workflowId}/resume-context`);
1182
1244
  }
1183
1245
 
1246
+ async executeVerifiedMutation({
1247
+ mutationName,
1248
+ mutationFn,
1249
+ verificationFn,
1250
+ expectedState,
1251
+ evidenceLabel,
1252
+ } = {}) {
1253
+ if (typeof mutationFn !== 'function') throw new Error('mutationFn is required.');
1254
+ if (typeof verificationFn !== 'function') throw new Error('verificationFn is required.');
1255
+ const normalizedMutationName = cleanText(mutationName) || 'verifiedMutation';
1256
+ const normalizedEvidenceLabel = cleanText(evidenceLabel) || normalizedMutationName;
1257
+
1258
+ let mutationResult;
1259
+ try {
1260
+ mutationResult = await mutationFn();
1261
+ } catch (error) {
1262
+ throw buildVerificationError(
1263
+ `${normalizedMutationName} mutation failed before post-condition verification.`,
1264
+ {
1265
+ mutation_name: normalizedMutationName,
1266
+ evidence_label: normalizedEvidenceLabel,
1267
+ mutation_attempted: true,
1268
+ authoritative_read_performed: false,
1269
+ post_condition_verified: false,
1270
+ expected_state: expectedState ?? null,
1271
+ mutation_error: {
1272
+ message: error?.message || String(error),
1273
+ status: error?.status || null,
1274
+ payload: error?.payload || null,
1275
+ },
1276
+ },
1277
+ );
1278
+ }
1279
+
1280
+ let verificationResult;
1281
+ try {
1282
+ verificationResult = await verificationFn(mutationResult);
1283
+ } catch (error) {
1284
+ throw buildVerificationError(
1285
+ `${normalizedMutationName} authoritative read failed after mutation attempt.`,
1286
+ {
1287
+ mutation_name: normalizedMutationName,
1288
+ evidence_label: normalizedEvidenceLabel,
1289
+ mutation_attempted: true,
1290
+ authoritative_read_performed: false,
1291
+ post_condition_verified: false,
1292
+ expected_state: expectedState ?? null,
1293
+ mutation_result: mutationResult ?? null,
1294
+ verification_error: {
1295
+ message: error?.message || String(error),
1296
+ status: error?.status || null,
1297
+ payload: error?.payload || null,
1298
+ },
1299
+ },
1300
+ );
1301
+ }
1302
+
1303
+ let evaluation;
1304
+ if (typeof expectedState === 'function') {
1305
+ evaluation = expectedState(verificationResult, mutationResult);
1306
+ } else {
1307
+ evaluation = {
1308
+ ok: matchesExpectedState(verificationResult, expectedState),
1309
+ verifiedCurrentState: verificationResult,
1310
+ };
1311
+ }
1312
+ if (typeof evaluation === 'boolean') {
1313
+ evaluation = { ok: evaluation, verifiedCurrentState: verificationResult };
1314
+ }
1315
+ if (!isPlainObject(evaluation) || typeof evaluation.ok !== 'boolean') {
1316
+ throw new Error('expectedState must resolve to a boolean or an object with an ok field.');
1317
+ }
1318
+
1319
+ const evidence = {
1320
+ mutation_name: normalizedMutationName,
1321
+ evidence_label: normalizedEvidenceLabel,
1322
+ mutation_attempted: true,
1323
+ authoritative_read_performed: true,
1324
+ post_condition_verified: evaluation.ok,
1325
+ expected_state: expectedState ?? null,
1326
+ verified_current_state: evaluation.verifiedCurrentState ?? verificationResult ?? null,
1327
+ mutation_result: mutationResult ?? null,
1328
+ verification_result: verificationResult ?? null,
1329
+ verification_message: cleanText(evaluation.message) || null,
1330
+ };
1331
+
1332
+ if (!evaluation.ok) {
1333
+ throw buildVerificationError(
1334
+ cleanText(evaluation.message) || `${normalizedMutationName} post-condition verification failed.`,
1335
+ evidence,
1336
+ );
1337
+ }
1338
+
1339
+ return {
1340
+ mutationName: normalizedMutationName,
1341
+ evidenceLabel: normalizedEvidenceLabel,
1342
+ mutationResult,
1343
+ verificationResult,
1344
+ verifiedCurrentState: evidence.verified_current_state,
1345
+ mutationVerificationEvidence: evidence,
1346
+ };
1347
+ }
1348
+
1349
+ async updateImplementationItemStatusVerified(implementationItemId, status, body = {}) {
1350
+ return this.executeVerifiedMutation({
1351
+ mutationName: 'updateImplementationItemStatus',
1352
+ evidenceLabel: `implementation-item-status:${implementationItemId}`,
1353
+ mutationFn: () => this.updateImplementationItemStatus(implementationItemId, { ...body, status }),
1354
+ verificationFn: async (mutationResult) => {
1355
+ const workflowId = cleanText(body.workflowId)
1356
+ || cleanText(body.workflow_id)
1357
+ || cleanText(mutationResult?.workflow_id);
1358
+ if (!workflowId) {
1359
+ throw new Error('workflowId is required to verify implementation item status.');
1360
+ }
1361
+ const roadmap = await this.getWorkflowImplementationRoadmap(workflowId);
1362
+ const items = Array.isArray(roadmap?.items) ? roadmap.items : [];
1363
+ const matchedItem = items.find((item) => item?.implementation_item_id === implementationItemId) || null;
1364
+ return {
1365
+ workflow_id: workflowId,
1366
+ item: matchedItem,
1367
+ roadmap,
1368
+ };
1369
+ },
1370
+ expectedState: (verificationResult) => {
1371
+ const item = verificationResult?.item || null;
1372
+ if (!item) {
1373
+ return {
1374
+ ok: false,
1375
+ message: `Implementation item ${implementationItemId} was not present in the authoritative roadmap read.`,
1376
+ verifiedCurrentState: verificationResult,
1377
+ };
1378
+ }
1379
+ return {
1380
+ ok: item.status === status,
1381
+ message: `Implementation item ${implementationItemId} status remained ${item.status ?? 'unknown'} after mutation attempt.`,
1382
+ verifiedCurrentState: item,
1383
+ };
1384
+ },
1385
+ });
1386
+ }
1387
+
1388
+ async updateAcceptanceCheckStatusVerified(implementationItemId, acceptanceCheckId, status, body = {}) {
1389
+ return this.executeVerifiedMutation({
1390
+ mutationName: 'updateAcceptanceCheckStatus',
1391
+ evidenceLabel: `acceptance-check-status:${implementationItemId}:${acceptanceCheckId}`,
1392
+ mutationFn: () => this.updateAcceptanceCheckStatus(implementationItemId, acceptanceCheckId, { ...body, status }),
1393
+ verificationFn: async () => this.getImplementationItemAcceptanceChecks(implementationItemId),
1394
+ expectedState: (verificationResult) => {
1395
+ const checks = Array.isArray(verificationResult?.acceptance_checks) ? verificationResult.acceptance_checks : [];
1396
+ const targetCheck = checks.find((check) => check?.acceptance_check_id === acceptanceCheckId) || null;
1397
+ if (!targetCheck) {
1398
+ return {
1399
+ ok: false,
1400
+ message: `Acceptance check ${acceptanceCheckId} was not returned by the authoritative read.`,
1401
+ verifiedCurrentState: verificationResult,
1402
+ };
1403
+ }
1404
+ return {
1405
+ ok: targetCheck.status === status,
1406
+ message: `Acceptance check ${acceptanceCheckId} status remained ${targetCheck.status ?? 'unknown'} after mutation attempt.`,
1407
+ verifiedCurrentState: targetCheck,
1408
+ };
1409
+ },
1410
+ });
1411
+ }
1412
+
1413
+ async verifyImplementationItemArtifacts(implementationItemId, { requiredArtifacts = [] } = {}) {
1414
+ const requiredKinds = Array.isArray(requiredArtifacts) ? requiredArtifacts : [];
1415
+ const verifications = [];
1416
+ const artifactReaders = {
1417
+ artifact_manifest: async () => this.getArtifactManifest(implementationItemId),
1418
+ decision_packet: async () => this.getDecisionPacket(implementationItemId),
1419
+ };
1420
+
1421
+ for (const kind of requiredKinds) {
1422
+ const reader = artifactReaders[kind];
1423
+ if (typeof reader !== 'function') {
1424
+ throw new Error(`Unsupported required artifact: ${kind}`);
1425
+ }
1426
+ const payload = await reader();
1427
+ if (payload?.source_truth !== 'sql') {
1428
+ throw buildVerificationError(`Artifact ${kind} did not declare SQL as source_truth.`, {
1429
+ artifact_kind: kind,
1430
+ verified_current_state: payload ?? null,
1431
+ });
1432
+ }
1433
+ if (payload?.item_id !== implementationItemId) {
1434
+ throw buildVerificationError(`Artifact ${kind} returned item_id ${payload?.item_id ?? 'unknown'} instead of ${implementationItemId}.`, {
1435
+ artifact_kind: kind,
1436
+ verified_current_state: payload ?? null,
1437
+ });
1438
+ }
1439
+ const dataKey = kind === 'artifact_manifest' ? 'artifact_manifest' : 'decision_packet';
1440
+ if (!isPlainObject(payload?.[dataKey])) {
1441
+ throw buildVerificationError(`Artifact ${kind} did not include required ${dataKey} data.`, {
1442
+ artifact_kind: kind,
1443
+ verified_current_state: payload ?? null,
1444
+ });
1445
+ }
1446
+ verifications.push({
1447
+ artifact_kind: kind,
1448
+ http_status: 200,
1449
+ source_truth: payload.source_truth,
1450
+ item_id: payload.item_id,
1451
+ verified_current_state: payload[dataKey],
1452
+ });
1453
+ }
1454
+
1455
+ return {
1456
+ itemId: implementationItemId,
1457
+ requiredArtifacts: requiredKinds,
1458
+ verified: true,
1459
+ mutationVerificationEvidence: {
1460
+ mutation_name: 'verifyImplementationItemArtifacts',
1461
+ evidence_label: `implementation-item-artifacts:${implementationItemId}`,
1462
+ mutation_attempted: false,
1463
+ authoritative_read_performed: true,
1464
+ post_condition_verified: true,
1465
+ expected_state: { required_artifacts: requiredKinds },
1466
+ verified_current_state: verifications,
1467
+ },
1468
+ artifacts: verifications,
1469
+ };
1470
+ }
1471
+
1184
1472
  // ─── Skills ────────────────────────────────────────────────────────────────
1185
1473
 
1186
1474
  async currentSkillRegistryStatus() {