@aifabrix/builder 2.59.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 (145) 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/agent-kit/git-identity.js +180 -0
  14. package/lib/agent-kit/setup.js +3 -0
  15. package/lib/agent-kit/start.js +45 -1
  16. package/lib/api/configuration.api.js +131 -0
  17. package/lib/api/role-assistant-test-job.api.js +60 -0
  18. package/lib/api/system-secrets.api.js +72 -0
  19. package/lib/api/work-search.api.js +13 -6
  20. package/lib/app/deploy.js +8 -1
  21. package/lib/app/run-docker-fallback.js +6 -1
  22. package/lib/app/run-helpers.js +2 -1
  23. package/lib/app/run-parameter-sync.js +142 -0
  24. package/lib/app/show-display.js +1 -0
  25. package/lib/app/show-online.js +15 -0
  26. package/lib/build/docker-build-args.js +5 -2
  27. package/lib/build/index.js +3 -2
  28. package/lib/build/standard-docker-build.js +6 -3
  29. package/lib/cli/setup-app.js +19 -1
  30. package/lib/cli/setup-environment.js +156 -0
  31. package/lib/cli/setup-utility.js +24 -1
  32. package/lib/commands/datasource-capability-upsert-cli.js +112 -0
  33. package/lib/commands/datasource-capability.js +4 -2
  34. package/lib/commands/env-secret-context.js +113 -0
  35. package/lib/commands/env-secret-list.js +200 -0
  36. package/lib/commands/env-secret-push-confirm.js +54 -0
  37. package/lib/commands/env-secret-push-run.js +227 -0
  38. package/lib/commands/env-secret-push.js +168 -0
  39. package/lib/commands/repair-datasource-apply.js +2 -0
  40. package/lib/commands/repair-datasource-keyed-document.js +122 -0
  41. package/lib/commands/repair-datasource-run.js +1 -0
  42. package/lib/commands/role-assistant.js +7 -0
  43. package/lib/commands/setup-modes.js +1 -8
  44. package/lib/commands/setup-prompts.js +2 -180
  45. package/lib/commands/verify-operations-skip-e2e.js +25 -1
  46. package/lib/commands/verify-operations-steps.js +13 -1
  47. package/lib/commands/wizard-config-normalizer.js +7 -4
  48. package/lib/commands/wizard-core.js +5 -157
  49. package/lib/commands/wizard-file-saving.js +163 -0
  50. package/lib/core/env-platform-expand.js +97 -0
  51. package/lib/core/secrets-env-content.js +42 -4
  52. package/lib/core/secrets-env-write.js +10 -3
  53. package/lib/core/secrets-load.js +4 -2
  54. package/lib/datasource/binary-documents-validator.js +190 -0
  55. package/lib/datasource/capability/run-capability-upsert.js +202 -0
  56. package/lib/datasource/capability/upsert-ingredients.js +291 -0
  57. package/lib/datasource/capability/upsert-operations.js +138 -0
  58. package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
  59. package/lib/datasource/validate.js +12 -5
  60. package/lib/deployment/installation/azure-infra-stage.js +3 -1
  61. package/lib/deployment/installation/infra-catalog.js +2 -5
  62. package/lib/generator/builders.js +17 -0
  63. package/lib/generator/helpers.js +23 -2
  64. package/lib/generator/index.js +13 -6
  65. package/lib/lifecycle/product-model.js +4 -3
  66. package/lib/lifecycle/report-display.js +3 -2
  67. package/lib/parameters/infra-parameter-catalog.js +1 -1
  68. package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
  69. package/lib/programmatic/builder-help-governance.js +1 -1
  70. package/lib/programmatic/builder-help.js +1 -1
  71. package/lib/role-assistant/test-cases-search.js +3 -0
  72. package/lib/role-assistant/test-job-runner.js +125 -0
  73. package/lib/role-assistant/test-runner-search.js +44 -2
  74. package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
  75. package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
  76. package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
  77. package/lib/role-assistant/test-runner-workhub.js +28 -19
  78. package/lib/schema/application-schema.json +205 -2
  79. package/lib/schema/external-datasource.schema.json +23 -3
  80. package/lib/schema/infra-parameter.schema.json +139 -33
  81. package/lib/schema/infra.parameter.yaml +416 -66
  82. package/lib/schema/infrastructure-schema.json +10 -35
  83. package/lib/utils/compose-generate-docker-compose.js +16 -9
  84. package/lib/utils/datasource-binary-evidence.js +92 -0
  85. package/lib/utils/datasource-test-run-capability-scope.js +44 -1
  86. package/lib/utils/datasource-test-run-debug-display.js +2 -0
  87. package/lib/utils/datasource-test-run-display.js +8 -2
  88. package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
  89. package/lib/utils/datasource-test-run-tty-log.js +2 -0
  90. package/lib/utils/docker-build.js +29 -8
  91. package/lib/utils/docker-manifest-public-port.js +36 -0
  92. package/lib/utils/env-copy.js +11 -10
  93. package/lib/utils/external-system-system-test-tty.js +3 -2
  94. package/lib/utils/image-tags.js +2 -2
  95. package/lib/utils/platform-kv-ref.js +1 -1
  96. package/lib/utils/platform-resolution.js +226 -0
  97. package/lib/utils/prepare-local-data-mount.js +58 -0
  98. package/lib/utils/resolve-docker-image-ref.js +8 -3
  99. package/lib/utils/secrets-helpers.js +0 -1
  100. package/lib/utils/system-secret-mapping.js +125 -0
  101. package/lib/utils/test-log-writer.js +2 -1
  102. package/lib/validation/external-manifest-validator.js +5 -0
  103. package/lib/validation/openapi-contract-surface-validator.js +3 -1
  104. package/lib/validation/validate-external-file.js +5 -1
  105. package/package.json +5 -4
  106. package/templates/README.md +2 -1
  107. package/templates/agent-kit/agent-kit.yaml +4 -1
  108. package/templates/agent-kit/instructions/AGENTKIT.md +1 -1
  109. package/templates/agent-kit/instructions/root.AGENTS.md +2 -0
  110. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +35 -2
  111. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
  112. package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
  113. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +6 -2
  114. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +28 -2
  115. package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
  116. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +28 -1
  117. package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
  118. package/templates/agent-kit/skills/shared/feedback.md +31 -0
  119. package/templates/agent-kit/skills/shared/hosts.md +13 -3
  120. package/templates/agent-kit/skills/shared/interaction.md +83 -0
  121. package/templates/agent-kit/skills/shared/status.md +76 -0
  122. package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
  123. package/templates/applications/builder-api/application.yaml +1 -1
  124. package/templates/applications/builder-api/env.template +5 -1
  125. package/templates/applications/dataplane/application.yaml +30 -2
  126. package/templates/applications/dataplane/env.template +36 -5
  127. package/templates/applications/keycloak/application.yaml +6 -1
  128. package/templates/applications/miso-controller/application.yaml +96 -1
  129. package/templates/applications/miso-controller/env.template +63 -37
  130. package/templates/applications/miso-controller/rbac.yaml +17 -0
  131. package/templates/external-system/external-datasource.yaml.hbs +7 -1
  132. package/templates/marketplace/main.json +73 -225
  133. package/templates/python/Dockerfile.hbs +2 -0
  134. package/templates/python/docker-compose.hbs +1 -1
  135. package/templates/typescript/Dockerfile.hbs +2 -0
  136. package/templates/typescript/docker-compose.hbs +12 -0
  137. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -34
  138. /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
  139. /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
  140. /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
  141. /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
  142. /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
  143. /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
  144. /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
  145. /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Build a no-duplicate upsert proof from author-owned scenario facts.
3
+ *
4
+ * @fileoverview Safe testPayload scaffolding for datasource capability upsert
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { deepClone } = require('./copy-operations');
10
+
11
+ const RUN_TOKEN = /\{\{\s*testRunId\s*\}\}/i;
12
+
13
+ function scenarioInput(scenario) {
14
+ const input = scenario?.input;
15
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return null;
16
+ const properties = input.properties;
17
+ return properties && typeof properties === 'object' && !Array.isArray(properties)
18
+ ? properties
19
+ : input;
20
+ }
21
+
22
+ function scalar(value) {
23
+ return value !== null && ['string', 'number', 'boolean'].includes(typeof value);
24
+ }
25
+
26
+ function identityFromInput(input, identityFields) {
27
+ if (!input) return null;
28
+ const entries = identityFields.map((field) => [field, input[field]]);
29
+ if (entries.some(([, value]) => !scalar(value) || value === '')) return null;
30
+ return Object.fromEntries(entries);
31
+ }
32
+
33
+ function sameIdentity(first, second, identityFields) {
34
+ return identityFields.every((field) => first[field] === second[field]);
35
+ }
36
+
37
+ function isCollisionSafe(identity) {
38
+ return Object.values(identity).some((value) =>
39
+ typeof value === 'string' && RUN_TOKEN.test(value)
40
+ );
41
+ }
42
+
43
+ function profileFields(doc, targetKey) {
44
+ const profile = doc.exposed?.profiles?.[targetKey];
45
+ if (Array.isArray(profile)) return profile;
46
+ return Array.isArray(profile?.attributes) ? profile.attributes : [];
47
+ }
48
+
49
+ function changedPublishedField(doc, targetKey, first, second, identityFields) {
50
+ const identity = new Set(identityFields);
51
+ const published = new Set(profileFields(doc, targetKey));
52
+ return Object.keys(first).find((field) =>
53
+ !identity.has(field) && published.has(field) && scalar(first[field]) &&
54
+ scalar(second[field]) && first[field] !== second[field]
55
+ ) || null;
56
+ }
57
+
58
+ function analyzeWritePair(doc, targetKey, identityFields, firstRow, secondRow) {
59
+ const first = scenarioInput(firstRow);
60
+ const second = scenarioInput(secondRow);
61
+ const firstIdentity = identityFromInput(first, identityFields);
62
+ const secondIdentity = identityFromInput(second, identityFields);
63
+ if (!firstIdentity || !secondIdentity) return { reason: 'both writes must contain every business identity field' };
64
+ if (!sameIdentity(firstIdentity, secondIdentity, identityFields)) {
65
+ return { reason: 'the two writes must use the same business identity values' };
66
+ }
67
+ if (!isCollisionSafe(firstIdentity)) {
68
+ return { reason: 'the business identity must include {{testRunId}} to isolate each live run' };
69
+ }
70
+ const changedField = changedPublishedField(
71
+ doc, targetKey, first, second, identityFields
72
+ );
73
+ if (!changedField) {
74
+ return { reason: 'the second write must change one published non-identity scalar field' };
75
+ }
76
+ return { first, second, identity: firstIdentity, changedField };
77
+ }
78
+
79
+ function exactLookup(operation, input, expectedRows, identityKey) {
80
+ return {
81
+ operation,
82
+ input: deepClone(input),
83
+ identityKey,
84
+ matchMode: 'exactIdentities',
85
+ expectedRows: deepClone(expectedRows)
86
+ };
87
+ }
88
+
89
+ function expectedRow(identity, changedField, value) {
90
+ return { ...deepClone(identity), [changedField]: value };
91
+ }
92
+
93
+ function hasExactProof(scenarios, index, targetKey, lookupKey, facts) {
94
+ const rows = scenarios.slice(index, index + 5);
95
+ if (rows.length !== 5) return false;
96
+ const operations = rows.map((row) => row?.operation);
97
+ if (operations.join('|') !== [lookupKey, targetKey, lookupKey, targetKey, lookupKey].join('|')) {
98
+ return false;
99
+ }
100
+ const [before, , afterFirst, , afterSecond] = rows;
101
+ return [before, afterFirst, afterSecond].every((row) =>
102
+ row?.matchMode === 'exactIdentities' && row?.identityKey === facts.changedField
103
+ ) && Array.isArray(before.expectedRows) && before.expectedRows.length === 0 &&
104
+ afterFirst.expectedRows?.length === 1 && afterSecond.expectedRows?.length === 1 &&
105
+ afterFirst.expectedRows?.[0]?.[facts.changedField] === facts.first[facts.changedField] &&
106
+ afterSecond.expectedRows?.[0]?.[facts.changedField] === facts.second[facts.changedField];
107
+ }
108
+
109
+ function findExistingProof(doc, scenarios, targetKey, lookupKey, identityFields) {
110
+ for (let index = 0; index <= scenarios.length - 5; index += 1) {
111
+ const facts = analyzeWritePair(
112
+ doc, targetKey, identityFields, scenarios[index + 1], scenarios[index + 3]
113
+ );
114
+ if (!facts.reason && hasExactProof(scenarios, index, targetKey, lookupKey, facts)) return true;
115
+ }
116
+ return false;
117
+ }
118
+
119
+ function findAdjacentWrites(doc, scenarios, targetKey, identityFields) {
120
+ let lastReason = 'two adjacent enabled upsert scenarios are required';
121
+ for (let index = 0; index < scenarios.length - 1; index += 1) {
122
+ const first = scenarios[index];
123
+ const second = scenarios[index + 1];
124
+ if (first?.operation !== targetKey || second?.operation !== targetKey) continue;
125
+ if (first.enabled === false || second.enabled === false) continue;
126
+ const facts = analyzeWritePair(doc, targetKey, identityFields, first, second);
127
+ if (!facts.reason) return { index, facts };
128
+ lastReason = facts.reason;
129
+ }
130
+ return { reason: lastReason };
131
+ }
132
+
133
+ function insufficientIssue(reason) {
134
+ return {
135
+ code: 'UPSERT-TEST-DATA-INSUFFICIENT',
136
+ severity: 'blocking',
137
+ ownership: 'test-payload',
138
+ aiFixable: false,
139
+ message: `Safe --test scaffolding was not generated: ${reason}; no values were fabricated`,
140
+ consequence: 'A weaker generated case could provide false no-duplicate confidence.',
141
+ remediation: ['Author two adjacent upsert inputs with the same isolated identity and one changed published field, then rerun --test.'],
142
+ prevention: 'Keep collision-safe two-write facts with the datasource test payload.',
143
+ residualRisk: 'Upsert remains unproven until the exact live chain passes.'
144
+ };
145
+ }
146
+
147
+ function buildProofRows(lookupKey, facts) {
148
+ const firstExpected = expectedRow(
149
+ facts.identity, facts.changedField, facts.first[facts.changedField]
150
+ );
151
+ const secondExpected = expectedRow(
152
+ facts.identity, facts.changedField, facts.second[facts.changedField]
153
+ );
154
+ return [
155
+ exactLookup(lookupKey, facts.identity, [], facts.changedField),
156
+ exactLookup(lookupKey, facts.identity, [firstExpected], facts.changedField),
157
+ exactLookup(lookupKey, facts.identity, [secondExpected], facts.changedField)
158
+ ];
159
+ }
160
+
161
+ function scaffoldUpsertTests(originalDoc, analysis, targetKey = 'upsert') {
162
+ const scenarios = originalDoc.testPayload?.scenarios;
163
+ const lookupKey = analysis?.selectedLookup?.operationKey;
164
+ const identities = Array.isArray(analysis?.identityFields) ? analysis.identityFields : [];
165
+ if (!Array.isArray(scenarios) || !lookupKey || identities.length === 0) {
166
+ return { changed: false, issue: insufficientIssue('required scenarios or exact lookup facts are missing') };
167
+ }
168
+ if (findExistingProof(originalDoc, scenarios, targetKey, lookupKey, identities)) {
169
+ return { changed: false, doc: originalDoc, patchOperations: [], updatedSections: [] };
170
+ }
171
+ const pair = findAdjacentWrites(originalDoc, scenarios, targetKey, identities);
172
+ if (pair.reason) return { changed: false, issue: insufficientIssue(pair.reason) };
173
+ const [before, afterFirst, afterSecond] = buildProofRows(lookupKey, pair.facts);
174
+ const doc = deepClone(originalDoc);
175
+ doc.testPayload.scenarios.splice(pair.index, 2,
176
+ before, scenarios[pair.index], afterFirst, scenarios[pair.index + 1], afterSecond);
177
+ return {
178
+ changed: true,
179
+ doc,
180
+ patchOperations: [{ op: 'replace', path: '/testPayload/scenarios', value: doc.testPayload.scenarios }],
181
+ updatedSections: ['testPayload.scenarios: upsert no-duplicate proof']
182
+ };
183
+ }
184
+
185
+ module.exports = {
186
+ scaffoldUpsertTests,
187
+ analyzeWritePair,
188
+ insufficientIssue
189
+ };
@@ -23,6 +23,8 @@ const { validateFieldReferences } = require('./field-reference-validator');
23
23
  const { validateAbac } = require('./abac-validator');
24
24
  const { buildDatasourceValidateSummary } = require('./datasource-validate-summary');
25
25
 
26
+ const { validateBinaryDocumentsContract } = require('./binary-documents-validator');
27
+
26
28
  const DATASOURCE_CONFIG_EXTENSIONS = new Set(['.json', '.yaml', '.yml']);
27
29
  const EXCLUDE_CONFIG_NAMES = new Set([
28
30
  'application.json',
@@ -242,12 +244,14 @@ async function validateDatasourceFile(filePathOrKey) {
242
244
  };
243
245
  }
244
246
 
247
+ const binary = validateBinaryDocumentsContract(parsed);
248
+ const warnings = binary.warnings.map(issue => `${issue.code}: ${issue.message}`);
245
249
  const postSchemaErrors = collectPostSchemaErrors(parsed);
246
250
  if (postSchemaErrors.length > 0) {
247
251
  return {
248
252
  valid: false,
249
253
  errors: postSchemaErrors,
250
- warnings: [],
254
+ warnings,
251
255
  resolvedPath: filePath,
252
256
  summary
253
257
  };
@@ -256,7 +260,7 @@ async function validateDatasourceFile(filePathOrKey) {
256
260
  return {
257
261
  valid: true,
258
262
  errors: [],
259
- warnings: [],
263
+ warnings,
260
264
  resolvedPath: filePath,
261
265
  summary
262
266
  };
@@ -269,7 +273,8 @@ async function validateDatasourceFile(filePathOrKey) {
269
273
  function collectPostSchemaErrors(parsed) {
270
274
  return [
271
275
  ...validateFieldReferences(parsed),
272
- ...validateAbac(parsed)
276
+ ...validateAbac(parsed),
277
+ ...validateBinaryDocumentsContract(parsed).errors.map(issue => `${issue.code}: ${issue.message}`)
273
278
  ];
274
279
  }
275
280
 
@@ -303,12 +308,14 @@ function validateDatasourceParsed(parsed) {
303
308
  };
304
309
  }
305
310
 
311
+ const binary = validateBinaryDocumentsContract(parsed);
312
+ const warnings = binary.warnings.map(issue => `${issue.code}: ${issue.message}`);
306
313
  const postSchemaErrors = collectPostSchemaErrors(parsed);
307
314
  if (postSchemaErrors.length > 0) {
308
315
  return {
309
316
  valid: false,
310
317
  errors: postSchemaErrors,
311
- warnings: [],
318
+ warnings,
312
319
  summary
313
320
  };
314
321
  }
@@ -316,7 +323,7 @@ function validateDatasourceParsed(parsed) {
316
323
  return {
317
324
  valid: true,
318
325
  errors: [],
319
- warnings: [],
326
+ warnings,
320
327
  summary
321
328
  };
322
329
  }
@@ -24,7 +24,9 @@ const preflight = require('./azure-preflight');
24
24
  const readiness = require('./azure-readiness');
25
25
  const registryAuth = require('./registry-auth-mode');
26
26
 
27
- const INFRA_SECRETS = Object.freeze(['ADMIN_PASSWORD', 'ENCRYPTION_KEY', 'MORI_CALLBACK_API_KEY']);
27
+ // The Marketplace template stopped accepting moriCallbackApiKey: the installation receives its
28
+ // Mori key at licence registration instead, so infra no longer provisions or projects one.
29
+ const INFRA_SECRETS = Object.freeze(['ADMIN_PASSWORD', 'ENCRYPTION_KEY']);
28
30
 
29
31
  function missingInputError(file, fields) {
30
32
  const error = new Error(
@@ -100,8 +100,6 @@ const PARAMETER_CATALOG = Object.freeze({
100
100
  moriBaseUrl: { kind: 'file', source: 'registration', required: true, description: 'Mori service base URL.' },
101
101
  moriLicensePublicKey: { kind: 'file', source: 'registration', required: false, description: 'Mori licence issuer public key (SPKI PEM); empty leaves the installation unpinned and it then refuses every licence.' },
102
102
  moriLicenseIssuer: { kind: 'file', source: 'registration', required: false, description: 'Expected iss claim on a Mori licence.' },
103
- moriCallbackApiKey: { kind: 'secretEnv', env: 'MORI_CALLBACK_API_KEY', required: true, description: 'Mori callback authentication key.' },
104
- entraCallbackStateSecret: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
105
103
  entraIdClientId: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
106
104
  entraIdClientSecret: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
107
105
  entraIdTenantId: { kind: 'deferred', description: 'Customer Entra provider tenant; distinct from the Azure subscription directory tenant and configured later.' },
@@ -146,14 +144,13 @@ const MORI_SERVICE_KEY = 'mori-controller-api-keyKeyVault';
146
144
  const SECRET_BINDINGS = Object.freeze({
147
145
  ADMIN_PASSWORD: { kv: 'miso-controller-admin-passwordKeyVault', required: true, generate: false, comment: 'Required installation administrator password; same canonical secret used by Miso onboarding.' },
148
146
  ENCRYPTION_KEY: { kv: 'miso-controller-secrets-encryptionKeyVault', required: true, generate: true, comment: 'Application/data encryption key.' },
149
- MORI_CALLBACK_API_KEY: { kv: 'mori-callback-api-keyKeyVault', required: true, generate: true, comment: 'Mori callback credential.' },
150
147
  MISO_API_KEY: { kv: 'miso-controller-secrets-apiKeyVault', required: false, generate: false, comment: 'Temporary Miso deployment key; an existing login may be used instead.' },
151
148
  MORI_API_KEY: { kv: MORI_SERVICE_KEY, required: true, generate: false, comment: 'Mori installation service key stored by DOCKER registration.' }
152
149
  });
153
150
 
154
151
  const ENV_TEMPLATE_KEYS = Object.freeze({
155
- AZURE: ['ADMIN_PASSWORD', 'ENCRYPTION_KEY', 'MORI_CALLBACK_API_KEY', 'MISO_API_KEY'],
156
- DOCKER: ['ADMIN_PASSWORD', 'ENCRYPTION_KEY', 'MORI_CALLBACK_API_KEY', 'MORI_API_KEY']
152
+ AZURE: ['ADMIN_PASSWORD', 'ENCRYPTION_KEY', 'MISO_API_KEY'],
153
+ DOCKER: ['ADMIN_PASSWORD', 'ENCRYPTION_KEY', 'MORI_API_KEY']
157
154
  });
158
155
 
159
156
  /**
@@ -362,6 +362,23 @@ function addSimpleOptionalFields(deployment, variables) {
362
362
  if (variables.required !== undefined && !(Array.isArray(variables.required) && variables.required.length === 0)) {
363
363
  deployment.required = variables.required;
364
364
  }
365
+ addPlatformSections(deployment, variables);
366
+ }
367
+
368
+ /**
369
+ * Carries the platform/environment declarations into the deployment manifest so the Controller can
370
+ * resolve `{NAME}` values per installation, which is the case Builder cannot resolve: one uploaded
371
+ * application serving many customer resource groups.
372
+ * @param {Object} deployment - Deployment manifest (mutated)
373
+ * @param {Object} variables - Application manifest
374
+ */
375
+ function addPlatformSections(deployment, variables) {
376
+ if (Array.isArray(variables.platforms) && variables.platforms.length > 0) {
377
+ deployment.platforms = variables.platforms;
378
+ }
379
+ if (Array.isArray(variables.environments) && variables.environments.length > 0) {
380
+ deployment.environments = variables.environments;
381
+ }
365
382
  }
366
383
 
367
384
  function buildOptionalFields(deployment, variables, rbac) {
@@ -243,6 +243,27 @@ function determineVariableLocation(value) {
243
243
  return value.startsWith('kv://') ? 'keyvault' : 'variable';
244
244
  }
245
245
 
246
+ /**
247
+ * Expands manifest `{NAME}` values into the env template before it becomes configuration items.
248
+ * Lenient by design: with no deployment target the tokens stay in the manifest for the Controller
249
+ * to resolve per installation, which is what the SaaS case needs. A known target resolves fully.
250
+ * @param {string} envTemplate - Raw env template
251
+ * @param {Object|null} variablesConfig - Application manifest
252
+ * @param {Object|null} target - `environment` and `resourceGroup` when known
253
+ * @returns {string}
254
+ */
255
+ function expandManifestNames(envTemplate, variablesConfig, target) {
256
+ const { expandPlatformNamesIfPresent } = require('../core/env-platform-expand');
257
+ const known = target && (target.resourceGroup || target.environment);
258
+ if (!known) return envTemplate;
259
+ return expandPlatformNamesIfPresent(envTemplate, {
260
+ manifest: variablesConfig,
261
+ environment: target.environment,
262
+ resourceGroup: target.resourceGroup,
263
+ strict: Boolean(target.resourceGroup)
264
+ });
265
+ }
266
+
246
267
  /**
247
268
  * Creates a configuration item from parsed variable
248
269
  * @function createConfigItem
@@ -282,9 +303,9 @@ function createConfigItem(key, value, location, applicationConfigMap) {
282
303
  return configItem;
283
304
  }
284
305
 
285
- function parseEnvironmentVariables(envTemplate, variablesConfig = null) {
306
+ function parseEnvironmentVariables(envTemplate, variablesConfig = null, target = null) {
286
307
  const configuration = [];
287
- const lines = envTemplate.split('\n');
308
+ const lines = expandManifestNames(envTemplate, variablesConfig, target).split('\n');
288
309
  const applicationConfigMap = createApplicationConfigMap(variablesConfig);
289
310
  for (const line of lines) {
290
311
  const parsed = parseEnvironmentVariableLine(line);
@@ -22,6 +22,7 @@ const { generateControllerManifest } = require('./external-controller-manifest')
22
22
  const { resolveVersionForApp } = require('../utils/image-version');
23
23
  const { getContainerPort } = require('../utils/port-resolver');
24
24
  const { buildEnvVarMap } = require('../utils/env-map');
25
+ const { mergeAdditionalPublishedPorts } = require('../utils/docker-manifest-public-port');
25
26
 
26
27
  /**
27
28
  * Generates deployment JSON from application configuration files
@@ -175,8 +176,9 @@ function substituteEnvVarsInDeployment(obj, envVarMap, fieldPath = '') {
175
176
  * @throws {Error} If validation fails
176
177
  */
177
178
  function buildAndValidateDeployment(appName, variables, envTemplate, rbac, options = null) {
178
- // Parse environment variables from template and merge portalInput from application config
179
- const configuration = parseEnvironmentVariables(envTemplate, variables);
179
+ // Parse environment variables from template and merge portalInput from application config.
180
+ // A known deployment target resolves manifest {NAME} values; otherwise they stay for the Controller.
181
+ const configuration = parseEnvironmentVariables(envTemplate, variables, options && options.target);
180
182
 
181
183
  // Build deployment manifest (Controller computes deploymentKey from schema)
182
184
  const deployment = builders.buildManifestStructure(appName, variables, configuration, rbac);
@@ -236,7 +238,7 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
236
238
  ...variables,
237
239
  app: { ...variables.app, version: resolved.version }
238
240
  };
239
- const configuration = parseEnvironmentVariables(envTemplate, variables);
241
+ const configuration = parseEnvironmentVariables(envTemplate, variables, options && options.target);
240
242
  const deployment = builders.buildManifestStructure(appName, variablesWithVersion, configuration, rbac);
241
243
 
242
244
  const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
@@ -246,6 +248,7 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
246
248
  ? parseInt(deployment.port, 10) : (typeof deployment.port === 'number' ? deployment.port : effectivePort);
247
249
  }
248
250
  const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
251
+ await mergeAdditionalPublishedPorts(envVarMap, variablesWithVersion);
249
252
  substituteEnvVarsInDeployment(deployment, envVarMap);
250
253
  _validator.validateNoUnresolvedVariablesInDeployment(deployment);
251
254
  return { deployment, appPath };
@@ -290,7 +293,7 @@ async function writeExternalDeployJson(appName, appPath, options) {
290
293
  * @param {string} appType - Application type
291
294
  * @returns {Promise<string>} Path to written deploy JSON
292
295
  */
293
- async function writeRegularDeployJson(appName, appPath, appType) {
296
+ async function writeRegularDeployJson(appName, appPath, appType, options = {}) {
294
297
  const { variables, envTemplate, rbac, jsonPath } = loadDeploymentConfigFiles(appPath, appType, appName);
295
298
  const resolved = await resolveVersionForApp(appName, variables, { updateBuilder: false });
296
299
  const variablesWithVersion = {
@@ -299,7 +302,11 @@ async function writeRegularDeployJson(appName, appPath, appType) {
299
302
  };
300
303
  const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
301
304
  const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
302
- const deployment = buildAndValidateDeployment(appName, variablesWithVersion, envTemplate, rbac, { envVarMap });
305
+ await mergeAdditionalPublishedPorts(envVarMap, variablesWithVersion);
306
+ const deployment = buildAndValidateDeployment(appName, variablesWithVersion, envTemplate, rbac, {
307
+ envVarMap,
308
+ target: options.target
309
+ });
303
310
  const jsonContent = JSON.stringify(deployment, null, 2);
304
311
  fs.writeFileSync(jsonPath, jsonContent, { mode: 0o644 });
305
312
  return jsonPath;
@@ -333,7 +340,7 @@ async function generateDeployJson(appName, options = {}) {
333
340
  if (isExternal) {
334
341
  return writeExternalDeployJson(appName, appPath, options);
335
342
  }
336
- return await writeRegularDeployJson(appName, appPath, appType);
343
+ return await writeRegularDeployJson(appName, appPath, appType, options);
337
344
  }
338
345
 
339
346
  async function generateDeployJsonWithValidation(appName, options = {}) {
@@ -134,6 +134,7 @@ function computeOperationalReadinessPercent(checks) {
134
134
  let earned = 0;
135
135
  let total = 0;
136
136
  for (const [key, weight] of Object.entries(OPERATIONS_WEIGHTS)) {
137
+ if (checks[key] === null) continue;
137
138
  total += weight;
138
139
  if (checks[key] === true) {
139
140
  earned += weight;
@@ -159,9 +160,9 @@ function buildOperationsChecksFromSteps(steps) {
159
160
  authorization: integration || e2e,
160
161
  openApi: validation,
161
162
  mappings: validation,
162
- sync: e2e,
163
- crud: e2e,
164
- execution: e2e,
163
+ sync: steps.keyedDocumentGet === true ? null : e2e,
164
+ crud: steps.keyedDocumentGet === true ? steps.documentPersistence === true : e2e,
165
+ execution: steps.keyedDocumentGet === true ? steps.documentPersistence === true : e2e,
165
166
  validation,
166
167
  unitTests: unit,
167
168
  integrationTests: integration,
@@ -132,8 +132,9 @@ function displayOperationsVerificationSteps(detailsGrouped) {
132
132
  for (const [groupName, keys] of groups) {
133
133
  logger.log(chalk.white(groupName));
134
134
  for (const key of keys) {
135
- const passed = detailsGrouped[groupName.toLowerCase()]?.[key] ?? detailsGrouped[key];
136
- const glyph = passed ? successGlyph() : failureGlyph();
135
+ const group = detailsGrouped[groupName.toLowerCase()];
136
+ const passed = group && Object.hasOwn(group, key) ? group[key] : detailsGrouped[key];
137
+ const glyph = passed === null ? chalk.gray('⏭ Not applicable') : passed ? successGlyph() : failureGlyph();
137
138
  logger.log(` ${labels[key] || key}`.padEnd(24) + glyph);
138
139
  }
139
140
  }
@@ -135,7 +135,7 @@ function createCatalogApi(doc, exact, patterns) {
135
135
  const entry = findEntryForKey(key);
136
136
  const gen = entry && entry.generator;
137
137
  // emptyAllowed: e.g. Redis password when no requirepass.
138
- // emptyString: optional user-supplied keys (OpenAI / Azure OpenAI) — absent resolves like ''.
138
+ // emptyString: optional user-supplied catalog keys — absent resolves like ''.
139
139
  return Boolean(gen && (gen.type === 'emptyAllowed' || gen.type === 'emptyString'));
140
140
  }
141
141
 
@@ -10,7 +10,7 @@ const fs = require('fs');
10
10
  const path = require('path');
11
11
  const { buildHelpEnvelope } = require('./builder-help-envelope');
12
12
 
13
- const HELP_MD_DIR = path.join(__dirname, 'help-content');
13
+ const HELP_MD_DIR = path.join(__dirname, '..', '..', 'docs', 'builder-help', 'content');
14
14
 
15
15
  /** @type {ReadonlyArray<string>} */
16
16
  const REPAIR_COMMANDS = Object.freeze([
@@ -15,7 +15,7 @@ const { listPlatformRoles } = require('./platform-rbac-catalog');
15
15
  const { CHANNEL_TARGET_KEYS, CHANNEL_TARGETS } = require('../channels/channel-targets');
16
16
 
17
17
  const BUILDER_HELP_DIR = path.join(__dirname, '..', '..', 'docs', 'builder-help');
18
- const HELP_MD_DIR = path.join(__dirname, 'help-content');
18
+ const HELP_MD_DIR = path.join(BUILDER_HELP_DIR, 'content');
19
19
 
20
20
  /**
21
21
  * @param {string} fileName
@@ -65,7 +65,7 @@ const PROSE_TOPIC_FILES = Object.freeze({
65
65
  enterpriseSyncFabrix: 'enterprise-sync-fabrix.md'
66
66
  });
67
67
 
68
- const HELP_MD_DIR = path.join(__dirname, 'help-content');
68
+ const HELP_MD_DIR = path.join(__dirname, '..', '..', 'docs', 'builder-help', 'content');
69
69
 
70
70
  /**
71
71
  * @param {string} message
@@ -142,6 +142,9 @@ function evaluateSearchExpectations(expectedSearch, observation) {
142
142
 
143
143
  function skillsResourceEntry(skillsPayload, resourceType) {
144
144
  const root = skillsEnvelope(skillsPayload) || asObject(skillsPayload);
145
+ if (String(root.resourceType || '').trim() === resourceType) {
146
+ return root;
147
+ }
145
148
  const rts = asObject(asObject(root).search).resourceTypes;
146
149
  return asObject(asObject(rts)[resourceType]);
147
150
  }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Run one published Role Assistant case as an explicitly selected subject.
3
+ *
4
+ * @fileoverview Async test-job isolation for the Role Assistant CLI
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { resolveControllerUrl } = require('../utils/controller-url');
10
+ const { resolveUserIdOrEmail } = require('../identity/resolve-user-id');
11
+ const {
12
+ enqueueRoleAssistantTestJob,
13
+ getRoleAssistantTestJob
14
+ } = require('../api/role-assistant-test-job.api');
15
+ const { roleAssistantSystemKey } = require('../utils/role-assistant-paths');
16
+
17
+ function selectedSubject(opts) {
18
+ const ids = Array.isArray(opts.subjectUserId)
19
+ ? opts.subjectUserId.map((value) => String(value || '').trim()).filter(Boolean)
20
+ : [];
21
+ const emails = Array.isArray(opts.subjectEmail)
22
+ ? opts.subjectEmail.map((value) => String(value || '').trim()).filter(Boolean)
23
+ : [];
24
+ if (ids.length + emails.length > 1) {
25
+ throw new Error('Role Assistant case execution accepts exactly one isolation subject.');
26
+ }
27
+ return { userId: ids[0] || '', email: emails[0] || '' };
28
+ }
29
+
30
+ function hasRoleAssistantTestSubject(opts) {
31
+ const subject = selectedSubject(opts);
32
+ return Boolean(subject.userId || subject.email);
33
+ }
34
+
35
+ async function resolveSubjectUserId(opts, authConfig) {
36
+ const subject = selectedSubject(opts);
37
+ if (subject.userId) {
38
+ return subject.userId;
39
+ }
40
+ const controllerUrl = await resolveControllerUrl();
41
+ return resolveUserIdOrEmail(controllerUrl, authConfig, subject.email);
42
+ }
43
+
44
+ function waitFor(milliseconds) {
45
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
46
+ }
47
+
48
+ async function pollJob(roleKey, authContext, jobId, opts) {
49
+ const timeoutMs = Number(opts.pollTimeoutMs) || 180000;
50
+ const intervalMs = Number(opts.pollIntervalMs) || 750;
51
+ const startedAt = Date.now();
52
+ let job = null;
53
+ while (Date.now() - startedAt < timeoutMs) {
54
+ job = await getRoleAssistantTestJob(
55
+ authContext.dataplaneUrl,
56
+ authContext.authConfig,
57
+ roleAssistantSystemKey(roleKey),
58
+ jobId
59
+ );
60
+ const status = String(job?.status || '').trim().toLowerCase();
61
+ if (['completed', 'failed', 'cancelled'].includes(status)) {
62
+ return job;
63
+ }
64
+ await waitFor(intervalMs);
65
+ }
66
+ throw new Error(`Role Assistant test job ${jobId} did not finish within ${timeoutMs} ms.`);
67
+ }
68
+
69
+ function evaluationFromJobCase(roleCase, job, row, fallbackJobId = null) {
70
+ const assertions = Array.isArray(row?.assertions)
71
+ ? row.assertions.map((assertion) => ({
72
+ ...assertion,
73
+ passed: String(assertion?.kind || '').toLowerCase() !== 'fail'
74
+ }))
75
+ : [];
76
+ return {
77
+ caseId: row?.caseId || roleCase.caseId,
78
+ sourceFile: roleCase.sourceFile || null,
79
+ passed: row?.passed === true,
80
+ lifecycleState: row?.lifecycleState || String(job?.status || 'failed'),
81
+ assertions,
82
+ failedAssertions: assertions.filter((assertion) => assertion.passed === false),
83
+ warnings: assertions.filter((assertion) => assertion.kind === 'warn'),
84
+ execution: {
85
+ processExecutionId: row?.processExecutionId || null,
86
+ finalStatus: row?.lifecycleState || job?.status || null,
87
+ waitReason: null,
88
+ interactions: [],
89
+ workResultError: job?.errorMessage || null,
90
+ testJobId: job?.jobId || job?.id || fallbackJobId || null
91
+ }
92
+ };
93
+ }
94
+
95
+ async function runPublishedCaseAsSubject(roleKey, roleCase, opts, authContext) {
96
+ const subjectUserId = await resolveSubjectUserId(opts, authContext.authConfig);
97
+ const accepted = await enqueueRoleAssistantTestJob(
98
+ authContext.dataplaneUrl,
99
+ authContext.authConfig,
100
+ roleAssistantSystemKey(roleKey),
101
+ { caseId: roleCase.caseId, subjectUserId }
102
+ );
103
+ const jobId = String(accepted?.jobId || '').trim();
104
+ if (!jobId) {
105
+ throw new Error('Role Assistant test job enqueue did not return jobId.');
106
+ }
107
+ const job = await pollJob(roleKey, authContext, jobId, opts);
108
+ const row = Array.isArray(job?.cases)
109
+ ? job.cases.find((candidate) => candidate?.caseId === roleCase.caseId)
110
+ : null;
111
+ if (!row) {
112
+ throw new Error(
113
+ job?.errorMessage ||
114
+ `Published Role Assistant case '${roleCase.caseId}' returned no result in job ${jobId}.`
115
+ );
116
+ }
117
+ return evaluationFromJobCase(roleCase, job, row, jobId);
118
+ }
119
+
120
+ module.exports = {
121
+ hasRoleAssistantTestSubject,
122
+ runPublishedCaseAsSubject,
123
+ evaluationFromJobCase,
124
+ selectedSubject
125
+ };