@aifabrix/builder 2.60.0 → 2.61.2

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 (112) hide show
  1. package/README.md +14 -11
  2. package/docs/README.md +80 -0
  3. package/docs/builder-help/evidence-patterns.json +155 -0
  4. package/docs/builder-help/golden-examples/crm-company.json +30 -0
  5. package/docs/builder-help/golden-examples/crm-deal.json +29 -0
  6. package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
  7. package/docs/builder-help/golden-examples/document-storage.json +30 -0
  8. package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
  9. package/docs/builder-help/golden-examples/repository-template.json +29 -0
  10. package/docs/builder-help/golden-examples/service-ticket.json +29 -0
  11. package/docs/builder-help/platform-roles.json +98 -0
  12. package/docs/builder-help/resource-type-catalog.json +402 -0
  13. package/lib/api/configuration.api.js +131 -0
  14. package/lib/api/system-secrets.api.js +72 -0
  15. package/lib/app/run-docker-fallback.js +6 -1
  16. package/lib/app/run-helpers.js +2 -1
  17. package/lib/app/run-parameter-sync.js +142 -0
  18. package/lib/build/docker-build-args.js +5 -2
  19. package/lib/build/index.js +3 -2
  20. package/lib/build/standard-docker-build.js +2 -2
  21. package/lib/cli/setup-app.js +5 -0
  22. package/lib/cli/setup-environment.js +156 -0
  23. package/lib/commands/datasource-capability-upsert-cli.js +112 -0
  24. package/lib/commands/datasource-capability.js +4 -2
  25. package/lib/commands/env-secret-context.js +113 -0
  26. package/lib/commands/env-secret-list.js +200 -0
  27. package/lib/commands/env-secret-push-confirm.js +54 -0
  28. package/lib/commands/env-secret-push-run.js +227 -0
  29. package/lib/commands/env-secret-push.js +168 -0
  30. package/lib/commands/repair-datasource-apply.js +2 -0
  31. package/lib/commands/repair-datasource-keyed-document.js +122 -0
  32. package/lib/commands/repair-datasource-run.js +1 -0
  33. package/lib/commands/setup-modes.js +1 -8
  34. package/lib/commands/setup-prompts.js +2 -180
  35. package/lib/commands/verify-operations-skip-e2e.js +25 -1
  36. package/lib/commands/verify-operations-steps.js +13 -1
  37. package/lib/commands/wizard-config-normalizer.js +7 -4
  38. package/lib/commands/wizard-core.js +5 -157
  39. package/lib/commands/wizard-file-saving.js +163 -0
  40. package/lib/core/env-platform-expand.js +5 -1
  41. package/lib/core/secrets-env-content.js +5 -1
  42. package/lib/core/secrets-env-write.js +10 -3
  43. package/lib/core/secrets-load.js +4 -2
  44. package/lib/datasource/binary-documents-validator.js +190 -0
  45. package/lib/datasource/capability/run-capability-upsert.js +202 -0
  46. package/lib/datasource/capability/upsert-ingredients.js +291 -0
  47. package/lib/datasource/capability/upsert-operations.js +138 -0
  48. package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
  49. package/lib/datasource/validate.js +12 -5
  50. package/lib/generator/index.js +3 -0
  51. package/lib/lifecycle/product-model.js +4 -3
  52. package/lib/lifecycle/report-display.js +3 -2
  53. package/lib/parameters/infra-parameter-catalog.js +1 -1
  54. package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
  55. package/lib/programmatic/builder-help-governance.js +1 -1
  56. package/lib/programmatic/builder-help.js +1 -1
  57. package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
  58. package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
  59. package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
  60. package/lib/role-assistant/test-runner-workhub.js +28 -19
  61. package/lib/schema/application-schema.json +14 -2
  62. package/lib/schema/external-datasource.schema.json +23 -3
  63. package/lib/schema/infra.parameter.yaml +388 -38
  64. package/lib/utils/compose-generate-docker-compose.js +7 -12
  65. package/lib/utils/datasource-binary-evidence.js +92 -0
  66. package/lib/utils/datasource-test-run-capability-scope.js +44 -1
  67. package/lib/utils/datasource-test-run-debug-display.js +2 -0
  68. package/lib/utils/datasource-test-run-display.js +8 -2
  69. package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
  70. package/lib/utils/datasource-test-run-tty-log.js +2 -0
  71. package/lib/utils/env-copy.js +11 -10
  72. package/lib/utils/external-system-system-test-tty.js +3 -2
  73. package/lib/utils/image-tags.js +2 -2
  74. package/lib/utils/platform-kv-ref.js +1 -1
  75. package/lib/utils/prepare-local-data-mount.js +58 -0
  76. package/lib/utils/secrets-helpers.js +0 -1
  77. package/lib/utils/system-secret-mapping.js +125 -0
  78. package/lib/utils/test-log-writer.js +2 -1
  79. package/lib/validation/external-manifest-validator.js +5 -0
  80. package/lib/validation/openapi-contract-surface-validator.js +3 -1
  81. package/lib/validation/validate-external-file.js +5 -1
  82. package/package.json +4 -3
  83. package/templates/agent-kit/agent-kit.yaml +1 -1
  84. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
  85. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
  86. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -2
  87. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +6 -2
  88. package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
  89. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +7 -1
  90. package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
  91. package/templates/agent-kit/skills/shared/interaction.md +83 -0
  92. package/templates/agent-kit/skills/shared/status.md +3 -12
  93. package/templates/applications/builder-api/application.yaml +1 -1
  94. package/templates/applications/builder-api/env.template +5 -1
  95. package/templates/applications/dataplane/application.yaml +1 -1
  96. package/templates/applications/dataplane/env.template +10 -12
  97. package/templates/applications/keycloak/application.yaml +6 -1
  98. package/templates/applications/miso-controller/application.yaml +74 -2
  99. package/templates/applications/miso-controller/env.template +42 -3
  100. package/templates/applications/miso-controller/rbac.yaml +17 -0
  101. package/templates/external-system/external-datasource.yaml.hbs +7 -1
  102. package/templates/marketplace/main.json +4 -4
  103. package/templates/python/docker-compose.hbs +1 -1
  104. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -41
  105. /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
  106. /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
  107. /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
  108. /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
  109. /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
  110. /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
  111. /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
  112. /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @fileoverview Generate keyed document retrieval from vendor OpenAPI response media types.
3
+ * @author AI Fabrix Team
4
+ */
5
+ 'use strict';
6
+
7
+ const fs = require('fs');
8
+ const { toCamelCaseKey } = require('./repair-datasource-auto-rbac');
9
+ const { loadConfigFile } = require('../utils/config-format');
10
+ const { documentKeyToLocalOpenApiPath } = require('./repair-openapi-export');
11
+ const { findVendorOperation } = require('../validation/openapi-contract-vendor-validation');
12
+
13
+ /** @param {Object} ds @param {Object} options @returns {Object|null} */
14
+ function loadVendorSpec(ds, options) {
15
+ if (options.openapiSpec) return options.openapiSpec;
16
+ if (!options.appPath || !ds.openapi?.documentKey) return null;
17
+ const file = documentKeyToLocalOpenApiPath(options.appPath, ds.systemKey || '', ds.openapi.documentKey);
18
+ return fs.existsSync(file) ? loadConfigFile(file) : null;
19
+ }
20
+
21
+ /** @param {Object} operation @returns {Object[]} */
22
+ function binaryResponses(operation) {
23
+ return Object.entries(operation?.responses || {})
24
+ .filter(([status]) => /^2(?:\d\d|XX)$/i.test(status))
25
+ .map(([, response]) => response)
26
+ .filter(response => Object.keys(response?.content || {}).some(type =>
27
+ /^(application\/(octet-stream|pdf)|image\/[^;]+)(;|$)/i.test(type)));
28
+ }
29
+
30
+ /** @param {Object} ds @param {Object} operation @param {Object[]} responses */
31
+ function addMetadataMappings(ds, operation, responses) {
32
+ ds.fieldMappings = ds.fieldMappings || {};
33
+ ds.fieldMappings.attributes = ds.fieldMappings.attributes || {};
34
+ const attributes = ds.fieldMappings.attributes;
35
+ for (const response of responses) {
36
+ for (const name of Object.keys(response.headers || {})) {
37
+ addMappingStub(attributes, name, 'headers');
38
+ }
39
+ }
40
+ for (const param of operation.parameters || []) {
41
+ if (param.required && ['query', 'path'].includes(param.in) && param.name) {
42
+ addMappingStub(attributes, param.name, 'params');
43
+ }
44
+ }
45
+ }
46
+
47
+ /** @param {Object} attributes @param {string} name @param {string} source */
48
+ function addMappingStub(attributes, name, source) {
49
+ const key = toCamelCaseKey(name);
50
+ if (!/^[a-z][a-zA-Z0-9]*$/.test(key) || Object.hasOwn(attributes, key)) return;
51
+ attributes[key] = { expression: `{{raw.${source}.${name}}}` };
52
+ }
53
+
54
+ /** @param {Object} ds @param {string} ref @returns {Object[]} */
55
+ function ensureGetSteps(ds, ref) {
56
+ ds.execution = ds.execution || {};
57
+ ds.execution.engine = 'cip';
58
+ ds.execution.cip = ds.execution.cip || { version: '1.0', operations: {} };
59
+ ds.execution.cip.operations = ds.execution.cip.operations || {};
60
+ const operations = ds.execution.cip.operations;
61
+ if (!operations.get) {
62
+ operations.get = { steps: [
63
+ { fetch: { source: 'openapi', openapiRef: ref, responseType: 'binary' } },
64
+ { map: { useFieldMappings: true } },
65
+ { filter: { abac: { mode: 'mandatory' } } },
66
+ { output: { mode: 'documents' } }
67
+ ] };
68
+ }
69
+ return Array.isArray(operations.get.steps) ? operations.get.steps : [];
70
+ }
71
+
72
+ /**
73
+ * Leave list-based document sync and author-owned unrelated operations unchanged.
74
+ * @param {Object} ds - Datasource, mutated
75
+ * @param {Object} options - Vendor spec or local appPath
76
+ * @param {string[]} changes - Repair log
77
+ * @returns {boolean}
78
+ */
79
+ function repairKeyedDocument(ds, options = {}, changes = []) {
80
+ const candidate = keyedDocumentCandidate(ds, options);
81
+ if (!candidate) return false;
82
+ const { ref, vendor, responses } = candidate;
83
+ const before = JSON.stringify(ds);
84
+ const getSteps = ensureGetSteps(ds, ref);
85
+ if (!getSteps.some(step => step?.fetch) || !getSteps.some(step => step?.output)) return false;
86
+ for (const step of getSteps) {
87
+ if (step?.fetch && (step.fetch.openapiRef || 'get') === ref) step.fetch.responseType = 'binary';
88
+ if (step?.output) step.output.mode = 'documents';
89
+ }
90
+ ds.documentStorage = { enabled: true, ingestAfterSync: true,
91
+ ...ds.documentStorage, responseType: 'binary', binaryOperationRef: 'get' };
92
+ addMetadataMappings(ds, vendor, responses);
93
+ if (JSON.stringify(ds) === before) return false;
94
+ changes.push('Configured keyed binary get with documents output and header/parameter mappings');
95
+ return true;
96
+ }
97
+
98
+ /** @param {Object} ds @param {Object} options @returns {Object|null} */
99
+ function keyedDocumentCandidate(ds, options) {
100
+ if (!['documentStorage', 'document-storage'].includes(ds?.entityType)) return null;
101
+ if (!canGenerateKeyedDocument(ds)) return null;
102
+ const operations = ds.execution?.cip?.operations || {};
103
+ const steps = operations.get?.steps || [];
104
+ const fetch = steps.find(step => step?.fetch)?.fetch;
105
+ if (fetch && !['openapi', undefined].includes(fetch.source)) return null;
106
+ const ref = fetch?.openapiRef || 'get';
107
+ const binding = ds.openapi?.operations?.[ref];
108
+ const vendor = findVendorOperation(loadVendorSpec(ds, options), binding);
109
+ const responses = binaryResponses(vendor);
110
+ return responses.length ? { ref, vendor, responses } : null;
111
+ }
112
+
113
+ /** @param {Object} ds @returns {boolean} */
114
+ function canGenerateKeyedDocument(ds) {
115
+ if (ds.execution?.cip?.operations?.list || ds.openapi?.operations?.list) return false;
116
+ if (ds.capabilities?.some(cap => (cap.key || cap) === 'list')) return false;
117
+ if (ds.documentStorage?.enabled === false) return false;
118
+ if (ds.execution?.engine && ds.execution.engine !== 'cip') return false;
119
+ return !ds.documentStorage?.binaryOperationRef || ds.documentStorage.binaryOperationRef === 'get';
120
+ }
121
+
122
+ module.exports = { repairKeyedDocument };
@@ -37,6 +37,7 @@ function runDatasourceRepairs(appPath, datasourceFiles, options, dryRun, changes
37
37
  try {
38
38
  const parsed = loadConfigFile(filePath);
39
39
  const { updated: fileUpdated, changes: fileChanges } = repairDatasourceFile(parsed, {
40
+ appPath,
40
41
  expose: options.expose,
41
42
  sync: options.sync,
42
43
  test: options.test,
@@ -322,8 +322,7 @@ function removeUserLocalSecrets() {
322
322
  }
323
323
 
324
324
  /**
325
- * Fresh install: wizard collects admin email + password, then AI tool keys,
326
- * then up-infra + up-platform.
325
+ * Fresh install: wizard collects admin credentials, then runs up-infra + up-platform.
327
326
  *
328
327
  * @async
329
328
  * @param {{ adminEmail: string, adminPassword?: string, passwordBundle?: object }} adminCreds
@@ -345,9 +344,6 @@ async function runFreshInstall(adminCreds) {
345
344
  });
346
345
  stopSpinnerSuccess(infraSpin, 'Infrastructure ready');
347
346
  await runSetupImageRefresh({ quiet: true });
348
- // Bring up infra first, then collect optional platform-level secrets (AI tool)
349
- // so the flow feels like a product installer: infra → platform configuration → platform services.
350
- await setupPrompts.promptAiTool({ silentIfConfigured: true });
351
347
  // Fresh-install should reset dev defaults (env + controller URL) the same way
352
348
  // as other setup recovery paths, otherwise the platform can come up pointed at
353
349
  // stale configuration.
@@ -382,7 +378,6 @@ async function runReinstall() {
382
378
  await runSetupImageRefresh({ quiet: true });
383
379
  const adminCreds = await setupPrompts.promptAdminCredentials();
384
380
  await config.setAdminEmail(adminCreds.adminEmail);
385
- await setupPrompts.promptAiTool({ silentIfConfigured: true });
386
381
  const infraSpin = startSpinner('Starting infrastructure...');
387
382
  await startInfraFromConfig({
388
383
  adminEmail: adminCreds.adminEmail,
@@ -436,7 +431,6 @@ async function runWipeData() {
436
431
  });
437
432
  stopSpinnerSuccess(wipeSpin, 'Databases reset');
438
433
  await runSetupImageRefresh({ quiet: true });
439
- await setupPrompts.promptAiTool({ silentIfConfigured: true });
440
434
  const infraSpin = startSpinner('Starting infrastructure...');
441
435
  await startInfraFromConfig({});
442
436
  stopSpinnerSuccess(infraSpin, 'Infrastructure ready');
@@ -460,7 +454,6 @@ async function runWipeData() {
460
454
  */
461
455
  async function runUpdateImages() {
462
456
  await runSetupImageRefresh({ quiet: true });
463
- await setupPrompts.promptAiTool({ silentIfConfigured: true });
464
457
  const infraSpin = startSpinner('Starting infrastructure...');
465
458
  await startInfraFromConfig({});
466
459
  stopSpinnerSuccess(infraSpin, 'Infrastructure ready');
@@ -1,23 +1,12 @@
1
1
  /**
2
2
  * Inquirer prompts for `aifabrix setup`.
3
3
  *
4
- * Three small prompt helpers:
4
+ * Setup prompt helpers:
5
5
  * 1. {@link promptModeSelection} - Mode menu shown when infra is already up.
6
6
  * 2. {@link promptAdminCredentials} - Admin email + password (fresh install only); email default
7
7
  * from {@link module:lib/core/config.getAdminEmail} when `adminEmail` exists in config.yaml.
8
- * 3. {@link promptAiTool} - AI tool provider + keys, only when the merged
9
- * secret cascade (user-local + shared) does not already provide the keys.
10
8
  *
11
- * Reads merged secrets via `loadSecrets()` from `lib/core/secrets.js` so the
12
- * resolution honors both user-local secrets and the shared `aifabrix-secrets`
13
- * store (file or remote API) when configured.
14
- *
15
- * Writes use `setSecretInStore(...)` (primary user `secrets.local.yaml` only; shared is human via
16
- * `aifabrix secret set --shared`) so the
17
- * installer can write either to user-local secrets or to the shared store,
18
- * depending on config.
19
- *
20
- * @fileoverview Setup prompts (mode menu, admin creds, AI tool)
9
+ * @fileoverview Setup prompts (mode menu, admin credentials, builder conflicts)
21
10
  * @author AI Fabrix Team
22
11
  * @version 2.0.0
23
12
  */
@@ -26,11 +15,6 @@
26
15
 
27
16
  const path = require('path');
28
17
  const inquirer = require('inquirer');
29
- const chalk = require('chalk');
30
- const logger = require('../utils/logger');
31
- const secretsCore = require('../core/secrets');
32
- const secretsEnsure = require('../core/secrets-ensure');
33
- const { formatSuccessLine, infoLine } = require('../utils/cli-test-layout-chalk');
34
18
  const { validateEmail, validatePassword } = require('./setup-prompts-validators');
35
19
  const {
36
20
  promptAdminCredentialsWithProfile,
@@ -45,13 +29,6 @@ const MODE = Object.freeze({
45
29
  UPDATE_IMAGES: 'update-images'
46
30
  });
47
31
 
48
- /** AI tool key names in `secrets.local.yaml` / shared secrets. */
49
- const AI_KEYS = Object.freeze({
50
- OPENAI_API_KEY: 'secrets-openaiApiKeyVault',
51
- AZURE_OPENAI_URL: 'azure-openaiapi-urlKeyVault',
52
- AZURE_OPENAI_API_KEY: 'secrets-azureOpenaiApiKeyVault'
53
- });
54
-
55
32
  /** Builder folder conflict actions (setup preflight). */
56
33
  const BUILDER_DIR_ACTION = Object.freeze({
57
34
  KEEP: 'keep',
@@ -131,158 +108,6 @@ async function promptAdminCredentials(options = {}) {
131
108
  };
132
109
  }
133
110
 
134
- /**
135
- * @typedef {Object} AiToolStatus
136
- * @property {boolean} openAiConfigured - True when OpenAI key resolves non-empty
137
- * @property {boolean} azureOpenAiConfigured - True when Azure URL + key resolve non-empty
138
- */
139
-
140
- /**
141
- * Detect whether AI tool keys are already present in the merged secret view.
142
- * Treats values surrounded by Handlebars-style placeholders (e.g. `{{...}}`)
143
- * as not configured.
144
- *
145
- * @async
146
- * @returns {Promise<AiToolStatus>}
147
- */
148
- async function detectAiToolStatus() {
149
- let merged = {};
150
- try {
151
- merged = await secretsCore.loadSecrets(undefined);
152
- } catch {
153
- merged = {};
154
- }
155
- const isSet = (key) => {
156
- const raw = merged ? merged[key] : undefined;
157
- if (raw === undefined || raw === null) return false;
158
- const value = String(raw).trim();
159
- if (!value) return false;
160
- if (value.startsWith('{{') && value.endsWith('}}')) return false;
161
- return true;
162
- };
163
- return {
164
- openAiConfigured: isSet(AI_KEYS.OPENAI_API_KEY),
165
- azureOpenAiConfigured:
166
- isSet(AI_KEYS.AZURE_OPENAI_URL) && isSet(AI_KEYS.AZURE_OPENAI_API_KEY)
167
- };
168
- }
169
-
170
- /**
171
- * Ask which AI tool the user wants to configure when none is configured yet.
172
- * @async
173
- * @returns {Promise<'openai'|'azure'|'skip'>}
174
- */
175
- async function askAiToolChoice() {
176
- const { choice } = await inquirer.prompt([
177
- {
178
- type: 'list',
179
- name: 'choice',
180
- message: 'Which AI tool do you want to configure for the dataplane?',
181
- choices: [
182
- { name: 'OpenAI (api.openai.com)', value: 'openai' },
183
- { name: 'Azure OpenAI (your Azure resource)', value: 'azure' },
184
- { name: 'Skip for now (set later via aifabrix secret set)', value: 'skip' }
185
- ]
186
- }
187
- ]);
188
- return choice;
189
- }
190
-
191
- /**
192
- * Collect and persist OpenAI key.
193
- * @async
194
- * @returns {Promise<void>}
195
- */
196
- async function collectAndSaveOpenAiKey() {
197
- const { apiKey } = await inquirer.prompt([
198
- {
199
- type: 'password',
200
- name: 'apiKey',
201
- message: 'OpenAI API key:',
202
- mask: '*',
203
- validate(input) {
204
- return String(input || '').trim() ? true : 'API key is required';
205
- }
206
- }
207
- ]);
208
- await secretsEnsure.setSecretInStore(AI_KEYS.OPENAI_API_KEY, String(apiKey).trim());
209
- logger.log(formatSuccessLine('OpenAI API key saved'));
210
- }
211
-
212
- /**
213
- * Collect and persist Azure OpenAI URL + key.
214
- * @async
215
- * @returns {Promise<void>}
216
- */
217
- async function collectAndSaveAzureOpenAi() {
218
- const answers = await inquirer.prompt([
219
- {
220
- type: 'input',
221
- name: 'url',
222
- message: 'Azure OpenAI endpoint URL (e.g. https://my-aoai.openai.azure.com):',
223
- validate(input) {
224
- const value = String(input || '').trim();
225
- if (!value) return 'Endpoint URL is required';
226
- if (!/^https?:\/\//.test(value)) return 'URL must start with http(s)://';
227
- return true;
228
- }
229
- },
230
- {
231
- type: 'password',
232
- name: 'apiKey',
233
- message: 'Azure OpenAI API key:',
234
- mask: '*',
235
- validate(input) {
236
- return String(input || '').trim() ? true : 'API key is required';
237
- }
238
- }
239
- ]);
240
- await secretsEnsure.setSecretInStore(AI_KEYS.AZURE_OPENAI_URL, String(answers.url).trim());
241
- await secretsEnsure.setSecretInStore(AI_KEYS.AZURE_OPENAI_API_KEY, String(answers.apiKey).trim());
242
- logger.log(formatSuccessLine('Azure OpenAI endpoint and API key saved'));
243
- }
244
-
245
- /**
246
- * Run the AI tool prompt sequence.
247
- * Skips silently when either provider is already configured (in any source).
248
- *
249
- * @async
250
- * @param {{ silentIfConfigured?: boolean }} [options]
251
- * @returns {Promise<void>}
252
- */
253
- async function promptAiTool(options = {}) {
254
- const silentIfConfigured = options && options.silentIfConfigured === true;
255
- const status = await detectAiToolStatus();
256
- if (status.openAiConfigured) {
257
- if (!silentIfConfigured) {
258
- logger.log(infoLine('OpenAI key already configured - skipping AI tool prompt.'));
259
- }
260
- return;
261
- }
262
- if (status.azureOpenAiConfigured) {
263
- if (!silentIfConfigured) {
264
- logger.log(infoLine('Azure OpenAI endpoint and key already configured - skipping AI tool prompt.'));
265
- }
266
- return;
267
- }
268
- const choice = await askAiToolChoice();
269
- if (choice === 'openai') {
270
- await collectAndSaveOpenAiKey();
271
- return;
272
- }
273
- if (choice === 'azure') {
274
- await collectAndSaveAzureOpenAi();
275
- return;
276
- }
277
- logger.log(
278
- chalk.gray(
279
- 'Skipped. Set later: aifabrix secret set ' +
280
- AI_KEYS.OPENAI_API_KEY +
281
- ' <key> (or the two azure-openaiapi-urlKeyVault / secrets-azureOpenaiApiKeyVault keys)'
282
- )
283
- );
284
- }
285
-
286
111
  /**
287
112
  * Absolute directory paths (with trailing sep) shown when setup will replace platform apps.
288
113
  *
@@ -353,7 +178,6 @@ async function promptBuilderDirConflict(info) {
353
178
 
354
179
  module.exports = {
355
180
  MODE,
356
- AI_KEYS,
357
181
  BUILDER_DIR_ACTION,
358
182
  INSTALLATION_PROFILE,
359
183
  formatBuilderPlatformReplaceLines,
@@ -361,8 +185,6 @@ module.exports = {
361
185
  confirmDestructiveMode,
362
186
  promptInstallationProfile,
363
187
  promptAdminCredentials,
364
- promptAiTool,
365
- detectAiToolStatus,
366
188
  promptBuilderDirConflict,
367
189
  validateEmail,
368
190
  validatePassword
@@ -105,7 +105,8 @@ function shouldSkipE2eForStaticPackage(systemKey, options) {
105
105
  if (storage.length === 0) {
106
106
  return false;
107
107
  }
108
- return storage.every((ds) => String(ds.syncBridge || 'disabled').toLowerCase() === 'disabled');
108
+ return storage.every((ds) => !isKeyedDocumentDatasource(ds) &&
109
+ String(ds.syncBridge || 'disabled').toLowerCase() === 'disabled');
109
110
  } catch {
110
111
  return false;
111
112
  }
@@ -124,7 +125,30 @@ function shouldSkipOperationsE2e(systemKey, options) {
124
125
  || shouldSkipE2eForStaticPackage(systemKey, options);
125
126
  }
126
127
 
128
+ /** @param {Object} ds @returns {boolean} */
129
+ function isKeyedDocumentDatasource(ds) {
130
+ const ops = ds?.execution?.cip?.operations;
131
+ return ds?.entityType === 'documentStorage' && !ops?.list &&
132
+ !ds.openapi?.operations?.list && !ds.capabilities?.some(cap => (cap.key || cap) === 'list') &&
133
+ Array.isArray(ops?.get?.steps) && ops.get.steps.some(step => step?.output?.mode === 'documents');
134
+ }
135
+
136
+ /** @param {string} systemKey @param {Object} options @returns {string[]} */
137
+ function keyedDocumentDatasourceKeys(systemKey, options) {
138
+ if (options.skipE2e === true || options.minimal === true) return [];
139
+ try {
140
+ const { resolveAppWorkspace } = require('../utils/app-path-resolve');
141
+ const { appPath } = resolveAppWorkspace(systemKey, options);
142
+ const storage = loadStorageDatasources(appPath);
143
+ return storage.length && storage.every(isKeyedDocumentDatasource) ? storage.map(ds => ds.key) : [];
144
+ } catch {
145
+ return [];
146
+ }
147
+ }
148
+
127
149
  module.exports = {
150
+ isKeyedDocumentDatasource,
151
+ keyedDocumentDatasourceKeys,
128
152
  shouldSkipOperationsE2e,
129
153
  shouldSkipE2eForRoleAssistant,
130
154
  shouldSkipE2eForStaticPackage
@@ -4,6 +4,9 @@
4
4
 
5
5
  'use strict';
6
6
 
7
+ const { keyedDocumentDatasourceKeys } = require('./verify-operations-skip-e2e');
8
+ const { hasAllKeyedDocumentPersistence } = require('../utils/datasource-binary-evidence');
9
+
7
10
  const { syncLocalIfRequested } = require('./test-e2e-external');
8
11
  const { cliOptsSkipPublish } = require('../utils/cli-sync-options');
9
12
  const { buildOperationsVerifyStepPlan } = require('./verify-operations-step-plan');
@@ -99,10 +102,19 @@ async function runOperationsVerifySteps(systemKey, options) {
99
102
  }
100
103
  }
101
104
 
105
+ const keyedKeys = keyedDocumentDatasourceKeys(systemKey, options);
102
106
  const e2eLabel = STEP_PROGRESS.e2e;
103
107
  stepResults.e2e = await runWithVerifyStepProgress(
104
108
  e2eLabel,
105
- async(progress) => runVerifyStep(() => plan.e2e.run(progress), plan.e2e.ok),
109
+ async(progress) => {
110
+ const result = await plan.e2e.run(progress);
111
+ if (keyedKeys.length) {
112
+ stepResults.keyedDocumentGet = true;
113
+ stepResults.documentPersistence = hasAllKeyedDocumentPersistence(result, keyedKeys);
114
+ return plan.e2e.ok(result) && stepResults.documentPersistence;
115
+ }
116
+ return plan.e2e.ok(result);
117
+ },
106
118
  options
107
119
  );
108
120
  return { stepResults, hardFailure: !stepResults.e2e, stoppedEarly: false };
@@ -4,6 +4,8 @@
4
4
  * @version 2.0.0
5
5
  */
6
6
 
7
+ const { repairKeyedDocument } = require('./repair-datasource-keyed-document');
8
+
7
9
  const ENTITY_TYPE_FALLBACK = 'record-storage';
8
10
  const VALID_ENTITY_TYPES = new Set([
9
11
  'document-storage',
@@ -56,7 +58,7 @@ function normalizeCipOutputModes(datasourceConfig) {
56
58
  for (const operation of Object.values(operations)) {
57
59
  if (!operation || !Array.isArray(operation.steps)) continue;
58
60
  for (const step of operation.steps) {
59
- if (step?.output?.mode && step.output.mode !== 'records') {
61
+ if (step?.output?.mode && !['records', 'documents'].includes(step.output.mode)) {
60
62
  step.output.mode = 'records';
61
63
  }
62
64
  }
@@ -80,7 +82,7 @@ function normalizeWizardTestPayload(datasourceConfig) {
80
82
  * @param {Object} datasourceConfig - Datasource config
81
83
  * @returns {Object} Normalized config
82
84
  */
83
- function normalizeDatasourceConfig(datasourceConfig) {
85
+ function normalizeDatasourceConfig(datasourceConfig, openapiSpec) {
84
86
  if (!datasourceConfig || typeof datasourceConfig !== 'object') {
85
87
  return datasourceConfig;
86
88
  }
@@ -89,6 +91,7 @@ function normalizeDatasourceConfig(datasourceConfig) {
89
91
  }
90
92
  normalizePortalInput(datasourceConfig);
91
93
  normalizeCipOutputModes(datasourceConfig);
94
+ repairKeyedDocument(datasourceConfig, { openapiSpec });
92
95
  normalizeWizardTestPayload(datasourceConfig);
93
96
  return datasourceConfig;
94
97
  }
@@ -100,10 +103,10 @@ function normalizeDatasourceConfig(datasourceConfig) {
100
103
  * @param {Object|Object[]} datasourceConfigs - Datasource config(s)
101
104
  * @returns {{ systemConfig: Object, datasourceConfigs: Object[] }} Normalized configs
102
105
  */
103
- function normalizeWizardConfigs(systemConfig, datasourceConfigs) {
106
+ function normalizeWizardConfigs(systemConfig, datasourceConfigs, openapiSpec) {
104
107
  const normalizedSystem = normalizeSystemConfig(systemConfig);
105
108
  const configs = Array.isArray(datasourceConfigs) ? datasourceConfigs : [datasourceConfigs];
106
- const normalizedDatasources = configs.map(normalizeDatasourceConfig);
109
+ const normalizedDatasources = configs.map(config => normalizeDatasourceConfig(config, openapiSpec));
107
110
  return { systemConfig: normalizedSystem, datasourceConfigs: normalizedDatasources };
108
111
  }
109
112