@aifabrix/builder 2.51.1 → 2.52.1

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 (187) hide show
  1. package/jest.isolated-projects.js +25 -2
  2. package/jest.projects.js +4 -1
  3. package/lib/api/datasources-extended.api.js +8 -8
  4. package/lib/api/keycloak.api.js +92 -0
  5. package/lib/api/token-providers.api.js +77 -0
  6. package/lib/api/types/keycloak.types.js +23 -0
  7. package/lib/api/types/token-providers.types.js +65 -0
  8. package/lib/app/down.js +16 -1
  9. package/lib/app/run-container-start.js +4 -0
  10. package/lib/app/run-deploy-job.js +135 -27
  11. package/lib/app/run-env-compose.js +32 -0
  12. package/lib/app/run-env-recovery.js +35 -0
  13. package/lib/app/run-helpers.js +5 -6
  14. package/lib/app/run.js +11 -3
  15. package/lib/channels/add-channel.js +126 -19
  16. package/lib/channels/approval-guides/chatgpt.js +12 -9
  17. package/lib/channels/approval-guides/microsoft-copilot.js +16 -12
  18. package/lib/channels/channel-add-ensure.js +128 -0
  19. package/lib/channels/channel-artifact-generator.js +11 -1
  20. package/lib/channels/channel-branding.js +94 -0
  21. package/lib/channels/channel-credential.js +8 -0
  22. package/lib/channels/channel-discovery.js +330 -0
  23. package/lib/channels/channel-external-system.js +1 -1
  24. package/lib/channels/channel-runtime-resolver.js +103 -14
  25. package/lib/channels/channel-status-display.js +7 -1
  26. package/lib/channels/channel-targets.js +18 -1
  27. package/lib/channels/chatgpt-runtime-contract.js +1 -1
  28. package/lib/channels/microsoft-copilot-channel-add.js +138 -0
  29. package/lib/channels/microsoft-copilot-entra-sso-form.js +180 -0
  30. package/lib/channels/microsoft-copilot-entra-sso.js +264 -0
  31. package/lib/channels/microsoft-copilot-icons.js +74 -0
  32. package/lib/channels/microsoft-copilot-limits.js +84 -0
  33. package/lib/channels/microsoft-copilot-package.js +130 -49
  34. package/lib/channels/microsoft-copilot-validate.js +197 -0
  35. package/lib/cli/index.js +2 -0
  36. package/lib/cli/infra-guided-footers.js +1 -1
  37. package/lib/cli/infra-guided-public-url-warn.js +55 -0
  38. package/lib/cli/infra-guided.js +78 -62
  39. package/lib/cli/setup-app.help.js +4 -0
  40. package/lib/cli/setup-app.test-commands.js +59 -28
  41. package/lib/cli/setup-external-system.js +50 -12
  42. package/lib/cli/setup-infra.js +31 -4
  43. package/lib/commands/channel-add-setup.js +35 -10
  44. package/lib/commands/channel.js +27 -12
  45. package/lib/commands/deploy-gate-orchestrator.js +3 -2
  46. package/lib/commands/governance-verify-external.js +2 -5
  47. package/lib/commands/integration-definition.js +158 -0
  48. package/lib/commands/lifecycle-external.js +14 -7
  49. package/lib/commands/repair-auth-apply.js +19 -5
  50. package/lib/commands/repair-datasource-sync-operations.js +4 -1
  51. package/lib/commands/role-assistant-test-output.js +200 -0
  52. package/lib/commands/role-assistant.js +131 -114
  53. package/lib/commands/test-governance-external.js +2 -5
  54. package/lib/commands/up-miso.js +61 -24
  55. package/lib/commands/upload-data-sync-finalize.js +20 -3
  56. package/lib/commands/verify-governance-external.js +2 -5
  57. package/lib/commands/verify-operations-external.js +2 -5
  58. package/lib/commands/verify-operations-step-plan.js +31 -0
  59. package/lib/commands/verify-trust-command-action.js +2 -5
  60. package/lib/datasource/capability/capability-resolve.js +15 -5
  61. package/lib/datasource/capability/copy-operations.js +14 -4
  62. package/lib/datasource/datasource-identifier.js +4 -3
  63. package/lib/datasource/integration-context.js +3 -3
  64. package/lib/datasource/resolve-app.js +90 -8
  65. package/lib/datasource/test-e2e.js +2 -2
  66. package/lib/datasource/unified-validation-run-body.js +15 -1
  67. package/lib/datasource/unified-validation-run-resolve.js +2 -2
  68. package/lib/external-system/deploy-helpers.js +9 -5
  69. package/lib/external-system/deploy.js +2 -1
  70. package/lib/external-system/test-system-level.js +21 -3
  71. package/lib/external-system/test.js +4 -4
  72. package/lib/generator/builders.js +9 -15
  73. package/lib/infrastructure/index.js +19 -4
  74. package/lib/integration-definition/apply.js +478 -0
  75. package/lib/integration-definition/canonical-json.js +51 -0
  76. package/lib/integration-definition/emit-suite-bid.js +94 -0
  77. package/lib/integration-definition/enrich-crm-field-mappings.js +240 -0
  78. package/lib/integration-definition/enrich-openapi-operations.js +434 -0
  79. package/lib/integration-definition/from-discovery.js +339 -0
  80. package/lib/integration-definition/hash.js +37 -0
  81. package/lib/integration-definition/index.js +37 -0
  82. package/lib/integration-definition/openapi-enrichment/hubspot-crm-companies.json +205 -0
  83. package/lib/integration-definition/resource-type-catalog.js +59 -0
  84. package/lib/integration-definition/system-kind-enum.js +40 -0
  85. package/lib/integration-definition/validate.js +148 -0
  86. package/lib/programmatic/builder-api-request.js +5 -1
  87. package/lib/programmatic/builder-help-governance.js +3 -2
  88. package/lib/programmatic/capability-registry.js +19 -0
  89. package/lib/programmatic/openapi-component-schemas.js +6 -1
  90. package/lib/programmatic/openapi-descriptions.js +40 -0
  91. package/lib/programmatic/openapi-envelope-examples.js +269 -0
  92. package/lib/programmatic/openapi-operation-bindings.js +45 -0
  93. package/lib/programmatic/openapi-request-schemas.js +14 -0
  94. package/lib/programmatic/openapi-result-schemas-overflow.js +101 -0
  95. package/lib/programmatic/openapi-result-schemas.js +46 -99
  96. package/lib/programmatic/openapi-schema-descriptions-core.js +7 -4
  97. package/lib/programmatic/openapi-schema-descriptions-help.js +1 -1
  98. package/lib/programmatic/openapi-schema-descriptions-operations.js +24 -0
  99. package/lib/programmatic/openapi-schema-helpers.js +9 -9
  100. package/lib/programmatic/openapi-spec.js +11 -1
  101. package/lib/programmatic/orchestration-openapi-meta.js +10 -0
  102. package/lib/programmatic/route-handler-map.js +29 -1
  103. package/lib/programmatic/run-channel-package.js +236 -0
  104. package/lib/programmatic/run-integration-definition.js +69 -0
  105. package/lib/role-assistant/knowledge-markdown-frontmatter.js +75 -0
  106. package/lib/role-assistant/knowledge-sync-logging.js +58 -0
  107. package/lib/role-assistant/knowledge-sync.js +104 -89
  108. package/lib/role-assistant/test-cases-bid.js +155 -0
  109. package/lib/role-assistant/test-cases-evaluate.js +9 -1
  110. package/lib/role-assistant/test-cases-honesty.js +24 -10
  111. package/lib/role-assistant/test-cases.js +28 -0
  112. package/lib/role-assistant/test-runner-bid-apply.js +122 -0
  113. package/lib/role-assistant/test-runner-bid-document.js +278 -0
  114. package/lib/role-assistant/test-runner-workhub-answers.js +289 -46
  115. package/lib/role-assistant/test-runner-workhub.js +121 -25
  116. package/lib/role-assistant/test-suite.js +119 -0
  117. package/lib/schema/application-schema.json +64 -32
  118. package/lib/schema/external-datasource.schema.json +103 -17
  119. package/lib/schema/external-system.schema.json +54 -3
  120. package/lib/schema/infra.parameter.yaml +0 -20
  121. package/lib/schema/integration-definition.schema.json +195 -0
  122. package/lib/utils/authentication-shape.js +6 -0
  123. package/lib/utils/certify-external-workspace.js +40 -0
  124. package/lib/utils/docker-build.js +3 -3
  125. package/lib/utils/docker-run-ephemeral.js +77 -17
  126. package/lib/utils/health-check-public-warn.js +31 -13
  127. package/lib/utils/health-check.js +11 -1
  128. package/lib/utils/help-builder.js +1 -0
  129. package/lib/utils/paths.js +13 -15
  130. package/lib/utils/promote-dev-docker-image.js +97 -18
  131. package/lib/utils/role-assistant-paths.js +76 -14
  132. package/lib/utils/transient-developer-id.js +28 -0
  133. package/lib/utils/upload-sync-options.js +7 -4
  134. package/lib/validation/reserved-system-key-prefixes.js +56 -0
  135. package/lib/validation/validate.js +54 -19
  136. package/package.json +1 -1
  137. package/scripts/pnpm/af.mjs +24 -9
  138. package/scripts/pnpm/help.mjs +1 -1
  139. package/templates/applications/builder-api/application.yaml +3 -3
  140. package/templates/applications/dataplane/application.yaml +5 -5
  141. package/templates/applications/dataplane/env.template +8 -7
  142. package/templates/applications/dataplane/rbac.yaml +10 -10
  143. package/templates/applications/keycloak/application.yaml +2 -2
  144. package/templates/applications/keycloak/env.template +5 -2
  145. package/templates/applications/miso-controller/application.yaml +25 -11
  146. package/templates/applications/miso-controller/env.template +24 -5
  147. package/templates/channels/README.md +11 -1
  148. package/templates/channels/brand/color-192.png +0 -0
  149. package/templates/channels/brand/dark/128.png +0 -0
  150. package/templates/channels/brand/dark/192.png +0 -0
  151. package/templates/channels/brand/dark/256.png +0 -0
  152. package/templates/channels/brand/dark/32.png +0 -0
  153. package/templates/channels/brand/dark/512.png +0 -0
  154. package/templates/channels/brand/dark/64.png +0 -0
  155. package/templates/channels/brand/light/128.png +0 -0
  156. package/templates/channels/brand/light/192.png +0 -0
  157. package/templates/channels/brand/light/256.png +0 -0
  158. package/templates/channels/brand/light/32.png +0 -0
  159. package/templates/channels/brand/light/512.png +0 -0
  160. package/templates/channels/brand/light/64.png +0 -0
  161. package/templates/channels/brand/mono-white/128.png +0 -0
  162. package/templates/channels/brand/mono-white/192.png +0 -0
  163. package/templates/channels/brand/mono-white/256.png +0 -0
  164. package/templates/channels/brand/mono-white/32.png +0 -0
  165. package/templates/channels/brand/mono-white/512.png +0 -0
  166. package/templates/channels/brand/mono-white/64.png +0 -0
  167. package/templates/channels/brand/outline-32.png +0 -0
  168. package/templates/channels/chatgpt/install.template.md.hbs +76 -36
  169. package/templates/channels/chatgpt/integration/README.md +4 -4
  170. package/templates/channels/chatgpt/integration/chatgpt-system.json +1 -16
  171. package/templates/channels/chatgpt/manifest.template.json.hbs +3 -3
  172. package/templates/channels/microsoft-copilot/ai-plugin.template.json.hbs +10 -16
  173. package/templates/channels/microsoft-copilot/declarative-agent.template.json.hbs +5 -3
  174. package/templates/channels/microsoft-copilot/entra-sso-registration.template.md.hbs +27 -0
  175. package/templates/channels/microsoft-copilot/install.template.md.hbs +39 -20
  176. package/templates/channels/microsoft-copilot/integration/README.md +2 -3
  177. package/templates/channels/microsoft-copilot/integration/microsoft-copilot-system.json +1 -22
  178. package/templates/channels/microsoft-copilot/manifest.template.json.hbs +7 -7
  179. package/templates/channels/microsoft-copilot/oauth-plugin-vault-required.template.md.hbs +11 -0
  180. package/templates/channels/slack/install.template.md.hbs +6 -1
  181. package/templates/channels/slack/integration/README.md +2 -4
  182. package/templates/channels/teams-bot/install.template.md.hbs +4 -0
  183. package/templates/channels/teams-bot/integration/README.md +3 -5
  184. package/templates/channels/web-embed/integration/web-embed-system.json +1 -16
  185. package/templates/python/docker-compose.hbs +10 -0
  186. package/templates/typescript/docker-compose.hbs +10 -0
  187. /package/.cursor/plans/{Done → Archive}/157.1-security-quality-violations-by-type.csv +0 -0
@@ -23,6 +23,10 @@ const {
23
23
  alreadyResponded,
24
24
  buildRespondPayload
25
25
  } = require('./test-runner-workhub-answers');
26
+ const {
27
+ shouldApplyBidAfterWait,
28
+ applyCaseBidViaBuilderApi
29
+ } = require('./test-runner-bid-apply');
26
30
 
27
31
  const TERMINAL_TASK = new Set(['completed', 'failed', 'cancelled', 'canceled']);
28
32
  const PLAN_LOST_RE =
@@ -180,23 +184,34 @@ async function createActivateAcceptPlan(dataplaneUrl, authConfig, planBody, assi
180
184
  throw lastError || new Error('Work Hub plan create/activate/accept failed.');
181
185
  }
182
186
 
183
- async function maybeApproveTask(dataplaneUrl, authConfig, taskId, ctx) {
187
+ function _approveGateDecision(ctx) {
184
188
  if (ctx.approveStopped) {
185
- return;
189
+ return 'skip';
186
190
  }
187
191
  const status = String(ctx.status || '').trim().toLowerCase();
188
192
  if (status !== 'pendingapproval' && status !== 'pending_approval') {
189
- return;
193
+ return 'skip';
190
194
  }
191
195
  const approvalState = String(ctx.taskRow?.approvalState || '').trim().toLowerCase();
192
- if (approvalState && approvalState !== 'pending') {
193
- ctx.approveStopped = true;
194
- return;
196
+ // Treat empty / pending* as still actionable. Stop only after a terminal decision.
197
+ const terminalDecision = new Set([
198
+ 'approved',
199
+ 'rejected',
200
+ 'cancelled',
201
+ 'canceled',
202
+ 'notrequired',
203
+ 'not_required'
204
+ ]);
205
+ if (approvalState && terminalDecision.has(approvalState)) {
206
+ return 'stop';
195
207
  }
196
208
  if ((Number(ctx.approveAttempts) || 0) >= 8) {
197
- ctx.approveStopped = true;
198
- return;
209
+ return 'stop';
199
210
  }
211
+ return 'run';
212
+ }
213
+
214
+ async function _attemptApproveTask(dataplaneUrl, authConfig, taskId, ctx) {
200
215
  try {
201
216
  await approveWorkTask(dataplaneUrl, authConfig, taskId, {});
202
217
  ctx.interactions.push({ type: 'approve', taskId, attempt: ctx.approveAttempts });
@@ -211,46 +226,126 @@ async function maybeApproveTask(dataplaneUrl, authConfig, taskId, ctx) {
211
226
  }
212
227
  }
213
228
 
229
+ async function maybeApproveTask(dataplaneUrl, authConfig, taskId, ctx) {
230
+ const gate = _approveGateDecision(ctx);
231
+ if (gate === 'skip') {
232
+ return;
233
+ }
234
+ if (gate === 'stop') {
235
+ ctx.approveStopped = true;
236
+ return;
237
+ }
238
+ await _attemptApproveTask(dataplaneUrl, authConfig, taskId, ctx);
239
+ }
240
+
241
+ function recordSkippedResponse(interactions, peId, fingerprint, wait) {
242
+ interactions.push({
243
+ type: 'respond-skipped',
244
+ processExecutionId: peId,
245
+ fingerprint,
246
+ questionKey: wait.questionKey || wait.requestKey || null,
247
+ objectRefKind: wait.objectRef?.kind || null,
248
+ reason: 'no-matching-answer-or-synthesis'
249
+ });
250
+ }
251
+
252
+ async function applyBidAfterWait(authConfig, testCase, interactions, peId) {
253
+ try {
254
+ const { loadCaseBidDocument } = require('./test-runner-workhub-answers');
255
+ const emittedBid = loadCaseBidDocument(testCase);
256
+ if (emittedBid && emittedBid.meta && emittedBid.meta.suiteRunId) {
257
+ interactions.push({
258
+ type: 'bid-emit',
259
+ processExecutionId: peId,
260
+ suiteRunId: emittedBid.meta.suiteRunId,
261
+ definitionId: emittedBid.definitionId,
262
+ definitionHash: emittedBid.definitionHash
263
+ });
264
+ }
265
+ const applied = await applyCaseBidViaBuilderApi(authConfig, testCase);
266
+ interactions.push({
267
+ type: 'bid-apply',
268
+ processExecutionId: peId,
269
+ systemKey: applied && applied.systemKey,
270
+ definitionHash: applied && applied.definitionHash,
271
+ suiteRunId: applied && applied.suiteRunId,
272
+ status: applied && applied.status
273
+ });
274
+ } catch (applyErr) {
275
+ interactions.push({
276
+ type: 'bid-apply-error',
277
+ processExecutionId: peId,
278
+ error: applyErr.message || String(applyErr)
279
+ });
280
+ }
281
+ }
282
+
283
+ async function respondToWaitingExecution(
284
+ dataplaneUrl,
285
+ authConfig,
286
+ peId,
287
+ testCase,
288
+ interactions,
289
+ statusRow
290
+ ) {
291
+ const wait = statusRow.wait && typeof statusRow.wait === 'object' ? statusRow.wait : {};
292
+ const fingerprint = waitRespondFingerprint(wait);
293
+ if (alreadyResponded(interactions, fingerprint)) {
294
+ return;
295
+ }
296
+ const payload = buildRespondPayload(
297
+ peId,
298
+ wait,
299
+ testCase,
300
+ statusRow,
301
+ interactions,
302
+ resolveObjective(testCase)
303
+ );
304
+ if (!payload) {
305
+ recordSkippedResponse(interactions, peId, fingerprint, wait);
306
+ return;
307
+ }
308
+ await respondWorkExecution(dataplaneUrl, authConfig, peId, payload);
309
+ await continueWorkExecution(dataplaneUrl, authConfig, peId, {});
310
+ interactions.push({ type: 'respond', processExecutionId: peId, fingerprint });
311
+ if (shouldApplyBidAfterWait(testCase, wait)) {
312
+ await applyBidAfterWait(authConfig, testCase, interactions, peId);
313
+ }
314
+ }
315
+
214
316
  async function maybeRespondWaiting(dataplaneUrl, authConfig, taskRow, testCase, interactions) {
215
317
  const peId = extractPeFromTask(taskRow);
216
318
  if (!peId) {
217
319
  return;
218
320
  }
219
321
  try {
322
+ testCase._activeProcessExecutionId = peId;
220
323
  const statusRow = await getWorkExecutionStatus(dataplaneUrl, authConfig, peId);
221
324
  if (String(statusRow?.status || '').toLowerCase() !== 'waiting') {
222
325
  return;
223
326
  }
224
- const wait = statusRow.wait && typeof statusRow.wait === 'object' ? statusRow.wait : {};
225
- const fingerprint = waitRespondFingerprint(wait);
226
- if (alreadyResponded(interactions, fingerprint)) {
227
- return;
228
- }
229
- const payload = buildRespondPayload(
327
+ await respondToWaitingExecution(
328
+ dataplaneUrl,
329
+ authConfig,
230
330
  peId,
231
- wait,
232
331
  testCase,
233
- statusRow,
234
332
  interactions,
235
- resolveObjective(testCase)
333
+ statusRow
236
334
  );
237
- if (!payload) {
238
- return;
239
- }
240
- await respondWorkExecution(dataplaneUrl, authConfig, peId, payload);
241
- await continueWorkExecution(dataplaneUrl, authConfig, peId, {});
242
- interactions.push({ type: 'respond', processExecutionId: peId, fingerprint });
243
335
  } catch (_err) {
244
336
  // Soft: continue polling task.
245
337
  }
246
338
  }
247
339
 
248
- function assertTerminalWorkHubTask(last, taskId, timeoutMs) {
340
+ function assertTerminalWorkHubTask(last, taskId, timeoutMs, options = {}) {
249
341
  if (!last) {
250
342
  throw new Error(`Work Hub task ${taskId} produced no status rows.`);
251
343
  }
252
344
  const finalStatusToken = String(last?.status || last?.taskStatus || '').trim().toLowerCase();
253
345
  if (!TERMINAL_TASK.has(finalStatusToken)) {
346
+ if (options.soft === true) {
347
+ return last;
348
+ }
254
349
  throw new Error(
255
350
  `Work Hub task ${taskId} did not reach a terminal status within ${timeoutMs}ms (last=${finalStatusToken || 'unknown'}).`
256
351
  );
@@ -261,6 +356,7 @@ function assertTerminalWorkHubTask(last, taskId, timeoutMs) {
261
356
  async function pollWorkHubTask(dataplaneUrl, authConfig, taskId, testCase, options) {
262
357
  const timeoutMs = Number(options.pollTimeoutMs) || 180000;
263
358
  const intervalMs = Number(options.pollIntervalMs) || 3000;
359
+ const soft = testCase?.expected?.soft === true;
264
360
  const started = Date.now();
265
361
  let last = null;
266
362
  const timeline = [];
@@ -287,7 +383,7 @@ async function pollWorkHubTask(dataplaneUrl, authConfig, taskId, testCase, optio
287
383
  }
288
384
 
289
385
  return {
290
- finalTask: assertTerminalWorkHubTask(last, taskId, timeoutMs),
386
+ finalTask: assertTerminalWorkHubTask(last, taskId, timeoutMs, { soft }),
291
387
  timeline,
292
388
  interactions: ctx.interactions
293
389
  };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Role Assistant suite discovery for multi-case CLI runs.
3
+ * @fileoverview Discover YAML cases under tests/role-assistant/ra-* suite folders
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const path = require('path');
9
+ const fs = require('../internal/fs-real-sync');
10
+ const { bareRoleKey } = require('../utils/role-assistant-paths');
11
+
12
+ function loadNodeFs() {
13
+ if (global.__AIFABRIX_NODE_FS_UNMOCKED__) {
14
+ return global.__AIFABRIX_NODE_FS_UNMOCKED__;
15
+ }
16
+ return typeof jest !== 'undefined' && typeof jest.requireActual === 'function'
17
+ ? jest.requireActual('node:fs')
18
+ : require('node:fs');
19
+ }
20
+
21
+ const nodeFs = loadNodeFs();
22
+
23
+ function exists(p) {
24
+ return nodeFs.existsSync(p) || fs.existsSync(p);
25
+ }
26
+
27
+ function readdir(p) {
28
+ if (nodeFs.existsSync(p)) {
29
+ return nodeFs.readdirSync(p, { withFileTypes: true });
30
+ }
31
+ return fs.readdirSync(p).map((name) => ({
32
+ name,
33
+ isDirectory: () => fs.statSync(path.join(p, name)).isDirectory()
34
+ }));
35
+ }
36
+
37
+ /**
38
+ * Resolve cases root for a role: prefer tests/role-assistant/ra-{bare}, else package tests/.
39
+ * @param {string} roleKey
40
+ * @param {{ casesRoot?: string, cwd?: string }} [options]
41
+ * @returns {string}
42
+ */
43
+ function resolveCasesRoot(roleKey, options = {}) {
44
+ if (options.casesRoot) {
45
+ return path.resolve(String(options.casesRoot));
46
+ }
47
+ const cwd = options.cwd || process.cwd();
48
+ const bare = bareRoleKey(roleKey);
49
+ const preferred = path.join(cwd, 'tests', 'role-assistant', `ra-${bare}`);
50
+ if (exists(preferred)) {
51
+ return preferred;
52
+ }
53
+ const packageTests = path.join(cwd, 'role-assistant', `ra-${bare}`, 'tests');
54
+ if (exists(packageTests)) {
55
+ return packageTests;
56
+ }
57
+ return preferred;
58
+ }
59
+
60
+ /**
61
+ * @param {string} casesRoot
62
+ * @param {{ suite?: string }} [options]
63
+ * @returns {Array<{ suite: string, file: string, absPath: string, caseIdHint: string }>}
64
+ */
65
+ function discoverSuiteCaseFiles(casesRoot, options = {}) {
66
+ if (!exists(casesRoot)) {
67
+ return [];
68
+ }
69
+ const suiteFilter = options.suite ? String(options.suite).trim() : '';
70
+ const rows = [];
71
+ for (const ent of readdir(casesRoot)) {
72
+ if (!ent.isDirectory() || String(ent.name).startsWith('.')) {
73
+ continue;
74
+ }
75
+ if (suiteFilter && ent.name !== suiteFilter) {
76
+ continue;
77
+ }
78
+ const suitePath = path.join(casesRoot, ent.name);
79
+ for (const fileEnt of readdir(suitePath)) {
80
+ const name = fileEnt.name || fileEnt;
81
+ if (typeof name !== 'string' || !/\.(ya?ml)$/i.test(name)) {
82
+ continue;
83
+ }
84
+ const absPath = path.join(suitePath, name);
85
+ rows.push({
86
+ suite: ent.name,
87
+ file: name,
88
+ absPath,
89
+ caseIdHint: path.basename(name, path.extname(name))
90
+ });
91
+ }
92
+ }
93
+ return rows.sort((a, b) => a.absPath.localeCompare(b.absPath));
94
+ }
95
+
96
+ /**
97
+ * Filter discovered rows by --case id or basename.
98
+ * @param {Array<{ absPath: string, caseIdHint: string, file: string }>} rows
99
+ * @param {string} caseSel
100
+ * @returns {Array}
101
+ */
102
+ function filterDiscoveredCases(rows, caseSel) {
103
+ const sel = String(caseSel || '').trim();
104
+ if (!sel) {
105
+ return rows;
106
+ }
107
+ return rows.filter((row) => {
108
+ if (row.absPath === path.resolve(sel) || row.file === sel) {
109
+ return true;
110
+ }
111
+ return row.caseIdHint === sel || row.caseIdHint.endsWith(`-${sel}`) || sel === row.caseIdHint;
112
+ });
113
+ }
114
+
115
+ module.exports = {
116
+ resolveCasesRoot,
117
+ discoverSuiteCaseFiles,
118
+ filterDiscoveredCases
119
+ };
@@ -7,12 +7,12 @@
7
7
  "key": "application-schema",
8
8
  "name": "Application Configuration Schema",
9
9
  "description": "JSON schema for validating AI Fabrix application configuration files",
10
- "version": "1.6.0",
10
+ "version": "1.6.2",
11
11
  "type": "schema",
12
12
  "category": "infrastructure",
13
13
  "author": "AI Fabrix Team",
14
14
  "createdAt": "2024-01-01T00:00:00Z",
15
- "updatedAt": "2026-07-28T00:00:00Z",
15
+ "updatedAt": "2026-08-06T00:00:00Z",
16
16
  "compatibility": {
17
17
  "minVersion": "1.0.0",
18
18
  "maxVersion": "2.0.0",
@@ -30,6 +30,22 @@
30
30
  "external-datasource.schema.json"
31
31
  ],
32
32
  "changelog": [
33
+ {
34
+ "version": "1.6.2",
35
+ "date": "2026-08-06T00:00:00Z",
36
+ "changes": [
37
+ "portalInput.options items may be plain strings (including empty for env-default) or {label, value} objects (matches miso-ui Configuration tab and miso-controller types)."
38
+ ],
39
+ "breaking": false
40
+ },
41
+ {
42
+ "version": "1.6.1",
43
+ "date": "2026-08-03T00:00:00Z",
44
+ "changes": [
45
+ "registryMode external no longer restricts configuration item names to DOCKER_REGISTRY_SERVER_*; external is image pull source only and full env/portal configuration remains valid."
46
+ ],
47
+ "breaking": false
48
+ },
33
49
  {
34
50
  "version": "1.6.0",
35
51
  "date": "2026-07-28T00:00:00Z",
@@ -273,9 +289,30 @@
273
289
  },
274
290
  "options": {
275
291
  "type": "array",
276
- "description": "Options for select fields",
292
+ "description": "Options for select fields (plain string or {label, value}; empty string allowed for env-default)",
277
293
  "items": {
278
- "type": "string"
294
+ "oneOf": [
295
+ {
296
+ "type": "string"
297
+ },
298
+ {
299
+ "type": "object",
300
+ "required": [
301
+ "label",
302
+ "value"
303
+ ],
304
+ "properties": {
305
+ "label": {
306
+ "type": "string",
307
+ "minLength": 1
308
+ },
309
+ "value": {
310
+ "type": "string"
311
+ }
312
+ },
313
+ "additionalProperties": false
314
+ }
315
+ ]
279
316
  }
280
317
  }
281
318
  },
@@ -391,9 +428,30 @@
391
428
  },
392
429
  "options": {
393
430
  "type": "array",
394
- "description": "Options for select fields",
431
+ "description": "Options for select fields (plain string or {label, value}; empty string allowed for env-default)",
395
432
  "items": {
396
- "type": "string"
433
+ "oneOf": [
434
+ {
435
+ "type": "string"
436
+ },
437
+ {
438
+ "type": "object",
439
+ "required": [
440
+ "label",
441
+ "value"
442
+ ],
443
+ "properties": {
444
+ "label": {
445
+ "type": "string",
446
+ "minLength": 1
447
+ },
448
+ "value": {
449
+ "type": "string"
450
+ }
451
+ },
452
+ "additionalProperties": false
453
+ }
454
+ ]
397
455
  }
398
456
  }
399
457
  },
@@ -974,32 +1032,6 @@
974
1032
  "databases"
975
1033
  ]
976
1034
  }
977
- },
978
- {
979
- "if": {
980
- "properties": {
981
- "registryMode": {
982
- "const": "external"
983
- }
984
- }
985
- },
986
- "then": {
987
- "properties": {
988
- "configuration": {
989
- "items": {
990
- "properties": {
991
- "name": {
992
- "enum": [
993
- "DOCKER_REGISTRY_SERVER_URL",
994
- "DOCKER_REGISTRY_SERVER_USERNAME",
995
- "DOCKER_REGISTRY_SERVER_PASSWORD"
996
- ]
997
- }
998
- }
999
- }
1000
- }
1001
- }
1002
- }
1003
1035
  }
1004
1036
  ]
1005
1037
  }
@@ -7,12 +7,12 @@
7
7
  "key":"external-datasource-schema",
8
8
  "name":"External Data Source Configuration Schema",
9
9
  "description":"JSON schema for validating ExternalDataSource configuration files",
10
- "version":"2.9.5",
10
+ "version":"2.9.6",
11
11
  "type":"schema",
12
12
  "category":"integration",
13
13
  "author":"AI Fabrix Team",
14
14
  "createdAt":"2024-01-01T00:00:00Z",
15
- "updatedAt":"2026-07-31T00:00:00Z",
15
+ "updatedAt":"2026-08-07T00:00:00Z",
16
16
  "compatibility":{
17
17
  "minVersion":"1.0.0",
18
18
  "maxVersion":"3.0.0",
@@ -390,6 +390,12 @@
390
390
  "enum":["documentStorage","vectorStore","recordStorage","messageService","serviceContract","none"],
391
391
  "description":"Entity type identifier. Defines storage semantics and behavior: 'documentStorage' (creates document storage service with vector-storage), 'vectorStore' (external vector storage system), 'recordStorage' (creates record-based system with metadata sync and access rights), 'messageService' (message service for notifications - Slack, Teams, Email), 'serviceContract' (normalized executable service adapter without persistence — requires metadataSchema, fieldMappings, exposed), 'none' (orchestration/composition only). Required field. Canonical values are camelCase."
392
392
  },
393
+ "storageType":{
394
+ "type":"string",
395
+ "enum":["table","view"],
396
+ "default":"table",
397
+ "description":"recordStorage persistence mode. 'table' (default) is ordinary synced record storage. 'view' is calculated read-only CIP perspectives with fetch.source viewpoint SQL; not a permanent PostgreSQL VIEW object."
398
+ },
393
399
  "resourceType":{
394
400
  "type":"string",
395
401
  "pattern":"^[a-z0-9-]+$",
@@ -1307,6 +1313,33 @@
1307
1313
  ]
1308
1314
  }
1309
1315
  },
1316
+ {
1317
+ "if":{
1318
+ "properties":{
1319
+ "storageType":{
1320
+ "const":"view"
1321
+ }
1322
+ },
1323
+ "required":[
1324
+ "storageType"
1325
+ ]
1326
+ },
1327
+ "then":{
1328
+ "properties":{
1329
+ "entityType":{
1330
+ "const":"recordStorage"
1331
+ },
1332
+ "execution":{
1333
+ "type":"object",
1334
+ "properties":{
1335
+ "engine":{
1336
+ "const":"cip"
1337
+ }
1338
+ }
1339
+ }
1340
+ }
1341
+ }
1342
+ },
1310
1343
  {
1311
1344
  "if":{
1312
1345
  "properties":{
@@ -2498,11 +2531,12 @@
2498
2531
  "source":{
2499
2532
  "type":"string",
2500
2533
  "enum":[
2501
- "openapi",
2534
+ "openapi",
2502
2535
  "http",
2503
- "datasource"
2536
+ "datasource",
2537
+ "viewpoint"
2504
2538
  ],
2505
- "description":"If 'openapi', refer to openapi.operations.*. If 'http', use explicit method/path. If 'datasource', execute another datasource operation."
2539
+ "description":"If 'openapi', refer to openapi.operations.*. If 'http', use explicit method/path. If 'datasource', execute another datasource operation. If 'viewpoint', execute saved SQL in fetch.query against records storage (storageType view)."
2506
2540
  },
2507
2541
  "openapiRef":{
2508
2542
  "type":"string",
@@ -2555,16 +2589,7 @@
2555
2589
  "description":"When true, append fetch items to the pipeline accumulator for a following merge step."
2556
2590
  },
2557
2591
  "query":{
2558
- "type":"object",
2559
- "description":"Static query parameters. ABAC/runtime can still override or append at runtime.",
2560
- "additionalProperties":{
2561
- "type":[
2562
- "string",
2563
- "number",
2564
- "boolean",
2565
- "null"
2566
- ]
2567
- }
2592
+ "description":"When source is openapi/http/datasource: static query parameter object. When source is viewpoint: non-empty SQL SELECT string. ABAC/runtime may still append supported filters at execution for viewpoint."
2568
2593
  },
2569
2594
  "bodyTemplate":{
2570
2595
  "description":"Optional static request body or template for POST/PUT/PATCH fetches.",
@@ -2606,7 +2631,20 @@
2606
2631
  "operationId"
2607
2632
  ]
2608
2633
  }
2609
- ]
2634
+ ],
2635
+ "properties":{
2636
+ "query":{
2637
+ "type":"object",
2638
+ "additionalProperties":{
2639
+ "type":[
2640
+ "string",
2641
+ "number",
2642
+ "boolean",
2643
+ "null"
2644
+ ]
2645
+ }
2646
+ }
2647
+ }
2610
2648
  }
2611
2649
  },
2612
2650
  {
@@ -2621,7 +2659,20 @@
2621
2659
  "required":[
2622
2660
  "method",
2623
2661
  "path"
2624
- ]
2662
+ ],
2663
+ "properties":{
2664
+ "query":{
2665
+ "type":"object",
2666
+ "additionalProperties":{
2667
+ "type":[
2668
+ "string",
2669
+ "number",
2670
+ "boolean",
2671
+ "null"
2672
+ ]
2673
+ }
2674
+ }
2675
+ }
2625
2676
  }
2626
2677
  },
2627
2678
  {
@@ -2637,9 +2688,44 @@
2637
2688
  "datasource":{
2638
2689
  "type":"string",
2639
2690
  "description":"Target child datasource key. Omitted when parent manifest composition.datasources has two or more entries; runtime resolves via selector input."
2691
+ },
2692
+ "query":{
2693
+ "type":"object",
2694
+ "additionalProperties":{
2695
+ "type":[
2696
+ "string",
2697
+ "number",
2698
+ "boolean",
2699
+ "null"
2700
+ ]
2701
+ }
2640
2702
  }
2641
2703
  }
2642
2704
  }
2705
+ },
2706
+ {
2707
+ "if":{
2708
+ "properties":{
2709
+ "source":{
2710
+ "const":"viewpoint"
2711
+ }
2712
+ },
2713
+ "required":[
2714
+ "source"
2715
+ ]
2716
+ },
2717
+ "then":{
2718
+ "required":[
2719
+ "query"
2720
+ ],
2721
+ "properties":{
2722
+ "query":{
2723
+ "type":"string",
2724
+ "minLength":1,
2725
+ "description":"Saved read-only SQL SELECT for viewpoint fetch. Parameter-free in first implementation."
2726
+ }
2727
+ }
2728
+ }
2643
2729
  }
2644
2730
  ],
2645
2731
  "additionalProperties":false