@aifabrix/builder 2.60.0 → 2.62.0

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 (177) 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/applications.api.js +22 -2
  14. package/lib/api/bootstrap-snapshot.api.js +115 -0
  15. package/lib/api/configuration.api.js +131 -0
  16. package/lib/api/system-secrets.api.js +72 -0
  17. package/lib/app/deploy.js +14 -5
  18. package/lib/app/run-docker-fallback.js +6 -1
  19. package/lib/app/run-env-compose.js +5 -6
  20. package/lib/app/run-helpers.js +2 -1
  21. package/lib/app/run-parameter-sync.js +142 -0
  22. package/lib/app/run.js +6 -4
  23. package/lib/build/docker-build-args.js +5 -2
  24. package/lib/build/index.js +3 -2
  25. package/lib/build/standard-docker-build.js +6 -2
  26. package/lib/channels/channel-artifact-generator.js +13 -3
  27. package/lib/cli/infra-guided-footers.js +10 -8
  28. package/lib/cli/infra-guided.js +30 -10
  29. package/lib/cli/setup-app.js +5 -0
  30. package/lib/cli/setup-environment.js +156 -0
  31. package/lib/cli/setup-infra-up-platform-action.js +14 -1
  32. package/lib/cli/setup-platform.js +2 -0
  33. package/lib/commands/auth-config.js +2 -0
  34. package/lib/commands/datasource-capability-upsert-cli.js +112 -0
  35. package/lib/commands/datasource-capability.js +4 -2
  36. package/lib/commands/env-secret-context.js +113 -0
  37. package/lib/commands/env-secret-list.js +200 -0
  38. package/lib/commands/env-secret-push-confirm.js +54 -0
  39. package/lib/commands/env-secret-push-run.js +227 -0
  40. package/lib/commands/env-secret-push.js +168 -0
  41. package/lib/commands/login.js +4 -0
  42. package/lib/commands/repair-datasource-apply.js +5 -1
  43. package/lib/commands/repair-datasource-keyed-document.js +122 -0
  44. package/lib/commands/repair-datasource-legacy.js +94 -0
  45. package/lib/commands/repair-datasource-run.js +1 -0
  46. package/lib/commands/setup-image-refresh.js +26 -0
  47. package/lib/commands/setup-modes.js +8 -9
  48. package/lib/commands/setup-prompts-platform-mode.js +34 -0
  49. package/lib/commands/setup-prompts.js +2 -180
  50. package/lib/commands/setup.js +3 -0
  51. package/lib/commands/up-builder-api.js +24 -8
  52. package/lib/commands/up-common.js +16 -1
  53. package/lib/commands/up-dataplane-bootstrap.js +70 -14
  54. package/lib/commands/up-dataplane-credentials.js +21 -0
  55. package/lib/commands/up-dataplane.js +37 -18
  56. package/lib/commands/up-integration-server.js +112 -0
  57. package/lib/commands/verify-operations-skip-e2e.js +25 -1
  58. package/lib/commands/verify-operations-steps.js +13 -1
  59. package/lib/commands/wizard-config-normalizer.js +12 -5
  60. package/lib/commands/wizard-core.js +5 -157
  61. package/lib/commands/wizard-file-saving.js +163 -0
  62. package/lib/core/admin-secrets-env-overlay.js +8 -3
  63. package/lib/core/env-platform-expand.js +5 -1
  64. package/lib/core/environment-access-policy.js +121 -0
  65. package/lib/core/environment-mode-policy.js +177 -0
  66. package/lib/core/local-env-overrides.js +149 -0
  67. package/lib/core/secrets-ensure.js +4 -2
  68. package/lib/core/secrets-env-content.js +17 -10
  69. package/lib/core/secrets-env-write.js +11 -4
  70. package/lib/core/secrets-load.js +4 -2
  71. package/lib/datasource/binary-documents-validator.js +190 -0
  72. package/lib/datasource/capability/capability-manifest.js +1 -0
  73. package/lib/datasource/capability/run-capability-upsert.js +202 -0
  74. package/lib/datasource/capability/upsert-ingredients.js +291 -0
  75. package/lib/datasource/capability/upsert-operations.js +138 -0
  76. package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
  77. package/lib/datasource/validate.js +12 -5
  78. package/lib/deployment/installation/index.js +14 -1
  79. package/lib/deployment/installation/infra-catalog.js +3 -0
  80. package/lib/deployment/installation/local-environment-stage.js +285 -0
  81. package/lib/deployment/installation/local-installation.js +76 -0
  82. package/lib/deployment/installation/stage-input.js +5 -1
  83. package/lib/generator/builders.js +3 -1
  84. package/lib/generator/index.js +3 -0
  85. package/lib/generator/split-variables.js +2 -2
  86. package/lib/lifecycle/product-model.js +4 -3
  87. package/lib/lifecycle/report-display.js +3 -2
  88. package/lib/parameters/database-adoption.js +125 -0
  89. package/lib/parameters/infra-parameter-catalog.js +1 -1
  90. package/lib/parameters/physical-database-naming.js +136 -0
  91. package/lib/parameters/physical-secret-name.js +194 -0
  92. package/lib/parameters/rsa-secret-values.js +165 -0
  93. package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
  94. package/lib/programmatic/builder-help-governance.js +1 -1
  95. package/lib/programmatic/builder-help.js +1 -1
  96. package/lib/programmatic/run-channel-install-artifacts.js +28 -2
  97. package/lib/programmatic/run-channel-package.js +3 -1
  98. package/lib/programmatic/workspace-context.js +37 -2
  99. package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
  100. package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
  101. package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
  102. package/lib/role-assistant/test-runner-workhub.js +28 -19
  103. package/lib/schema/application-schema.json +14 -2
  104. package/lib/schema/external-datasource.schema.json +23 -3
  105. package/lib/schema/infra.parameter.yaml +440 -56
  106. package/lib/schema/infrastructure-schema.json +213 -120
  107. package/lib/utils/compose-generate-docker-compose.js +9 -14
  108. package/lib/utils/compose-generator.js +4 -7
  109. package/lib/utils/compose-miso-env.js +2 -2
  110. package/lib/utils/compose-traefik-ingress-base.js +3 -4
  111. package/lib/utils/datasource-binary-evidence.js +92 -0
  112. package/lib/utils/datasource-test-run-capability-scope.js +44 -1
  113. package/lib/utils/datasource-test-run-debug-display.js +2 -0
  114. package/lib/utils/datasource-test-run-display.js +8 -2
  115. package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
  116. package/lib/utils/datasource-test-run-tty-log.js +2 -0
  117. package/lib/utils/declarative-url-ports.js +3 -0
  118. package/lib/utils/env-copy.js +11 -10
  119. package/lib/utils/env-environment-file-paths.js +105 -0
  120. package/lib/utils/environment-scoped-resources.js +48 -14
  121. package/lib/utils/external-system-system-test-tty.js +3 -2
  122. package/lib/utils/image-tags.js +2 -2
  123. package/lib/utils/paths-system-builder-keys.js +7 -1
  124. package/lib/utils/paths.js +7 -2
  125. package/lib/utils/platform-kv-ref.js +1 -1
  126. package/lib/utils/postgres-platform-bootstrap.js +35 -1
  127. package/lib/utils/prepare-local-data-mount.js +58 -0
  128. package/lib/utils/redis-env-scope.js +21 -1
  129. package/lib/utils/registry-auth-sources.js +154 -0
  130. package/lib/utils/registry-credentials.js +131 -14
  131. package/lib/utils/secrets-generator.js +30 -21
  132. package/lib/utils/secrets-helpers.js +5 -8
  133. package/lib/utils/secrets-kv-scope.js +127 -28
  134. package/lib/utils/secrets-materialize-local.js +9 -0
  135. package/lib/utils/secrets-missing-error.js +35 -1
  136. package/lib/utils/system-secret-mapping.js +125 -0
  137. package/lib/utils/test-log-writer.js +2 -1
  138. package/lib/utils/token-manager.js +14 -8
  139. package/lib/utils/url-public-path-prefix.js +6 -10
  140. package/lib/validation/external-manifest-validator.js +5 -0
  141. package/lib/validation/openapi-contract-surface-validator.js +3 -1
  142. package/lib/validation/validate-external-file.js +5 -1
  143. package/package.json +6 -4
  144. package/templates/agent-kit/agent-kit.yaml +1 -1
  145. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
  146. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
  147. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -2
  148. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +6 -2
  149. package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
  150. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +7 -1
  151. package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
  152. package/templates/agent-kit/skills/shared/interaction.md +83 -0
  153. package/templates/agent-kit/skills/shared/status.md +3 -12
  154. package/templates/applications/builder-api/application.yaml +1 -1
  155. package/templates/applications/builder-api/env.template +5 -1
  156. package/templates/applications/dataplane/application.yaml +2 -2
  157. package/templates/applications/dataplane/env.template +10 -12
  158. package/templates/applications/integration-server/application.yaml +91 -0
  159. package/templates/applications/integration-server/env.template +215 -0
  160. package/templates/applications/keycloak/application.yaml +6 -1
  161. package/templates/applications/mirrored-templates.json +52 -0
  162. package/templates/applications/miso-controller/application.yaml +116 -2
  163. package/templates/applications/miso-controller/env.template +62 -17
  164. package/templates/applications/miso-controller/rbac.yaml +17 -0
  165. package/templates/external-system/external-datasource.yaml.hbs +7 -1
  166. package/templates/marketplace/createUiDefinition.json +52 -4
  167. package/templates/marketplace/main.json +88 -10
  168. package/templates/python/docker-compose.hbs +1 -1
  169. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -41
  170. /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
  171. /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
  172. /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
  173. /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
  174. /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
  175. /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
  176. /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
  177. /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
@@ -11,6 +11,8 @@
11
11
  const fsSync = require('fs');
12
12
  const path = require('path');
13
13
  const buildCopy = require('./build-copy');
14
+ const { prepareLocalDataMount } = require('./prepare-local-data-mount');
15
+ const { isScopeActiveForOptions } = require('./environment-scoped-resources');
14
16
  const paths = require('./paths');
15
17
  const { isSystemBuilderAppName } = require('./paths-system-builder-keys');
16
18
  const {
@@ -187,14 +189,14 @@ function resolveStorageMountSourceRoot(appName, buildContext) {
187
189
  }
188
190
 
189
191
  /**
190
- * Host `mount/` directory bind-mounted to `/mnt/data` when storage is required.
192
+ * Host `mount/data/<appName>/` directory bind-mounted to `/mnt/data` when storage is required.
191
193
  *
192
194
  * @param {object} ctx - Compose template context (`devMountPath`, `appName`, `appConfig`, `serviceConfig`)
193
195
  * @returns {string|null}
194
196
  */
195
197
  function resolveLocalDataMountPathForCompose(ctx) {
196
198
  if (ctx.devMountPath && typeof ctx.devMountPath === 'string') {
197
- return path.join(ctx.devMountPath, 'mount').replace(/\\/g, '/');
199
+ return path.join(ctx.devMountPath, 'mount', 'data', ctx.appName).replace(/\\/g, '/');
198
200
  }
199
201
  const requiresStorage =
200
202
  ctx.serviceConfig?.requiresStorage ||
@@ -212,7 +214,7 @@ function resolveLocalDataMountPathForCompose(ctx) {
212
214
  if (!sourceRoot) {
213
215
  return null;
214
216
  }
215
- return path.join(sourceRoot, 'mount').replace(/\\/g, '/');
217
+ return path.join(sourceRoot, 'mount', 'data', ctx.appName).replace(/\\/g, '/');
216
218
  } catch {
217
219
  return null;
218
220
  }
@@ -229,12 +231,8 @@ function buildComposeTemplateContext(ctx) {
229
231
  ctx.appConfig?.requires?.storage ||
230
232
  ctx.appConfig?.services?.storage ||
231
233
  false;
232
- if (localDataMountPath && requiresStorage && !fsSync.existsSync(localDataMountPath)) {
233
- try {
234
- fsSync.mkdirSync(localDataMountPath, { recursive: true });
235
- } catch {
236
- // Non-fatal: tests may use non-writable devMountPath; host must create mount/ before run.
237
- }
234
+ if (localDataMountPath && requiresStorage) {
235
+ prepareLocalDataMount(localDataMountPath, ctx.envFileAbsolutePath);
238
236
  }
239
237
  return {
240
238
  ...ctx.serviceConfig,
@@ -253,9 +251,7 @@ function buildComposeTemplateContext(ctx) {
253
251
  // An app that administers its own operating system — creating accounts, managing
254
252
  // sshd — cannot work that way and declares `runAsRoot: true` to opt out.
255
253
  runAsRoot: ctx.appConfig?.runAsRoot === true,
256
- // Extra ports for an application serving a second protocol from the same container,
257
- // such as SSH beside HTTP. Each host port uses the main port's formula so developers
258
- // on one host do not collide.
254
+ // Extra protocol ports use the main port offset formula to avoid developer collisions.
259
255
  additionalPorts: (ctx.appConfig?.additionalPorts ?? []).map((entry) => ({
260
256
  name: entry.name,
261
257
  containerPort: entry.containerPort,
@@ -284,8 +280,7 @@ function resolveScopedComposeOpts(options) {
284
280
  effectiveEnvironmentScopedResources: options.effectiveEnvironmentScopedResources === true,
285
281
  runEnvKey
286
282
  };
287
- const useScope =
288
- scopeOpts.effectiveEnvironmentScopedResources && (runEnvKey === 'dev' || runEnvKey === 'tst');
283
+ const useScope = isScopeActiveForOptions(scopeOpts);
289
284
  return { scoped: useScope ? scopeOpts : null };
290
285
  }
291
286
 
@@ -22,7 +22,8 @@ const { isVectorDatabaseName } = require('./compose-vector-helper');
22
22
  const { resolveMisoEnvironment } = require('./compose-miso-env');
23
23
  const {
24
24
  buildScopedLocalContainerName,
25
- composeTraefikServiceKey
25
+ composeTraefikServiceKey,
26
+ isScopeActiveForOptions
26
27
  } = require('./environment-scoped-resources');
27
28
  const {
28
29
  derivePathFromPattern,
@@ -308,9 +309,7 @@ function buildServiceConfig(appName, config, port, devId, runExtras = {}) {
308
309
  const hostPort = port;
309
310
  const useTraefikScope =
310
311
  scopeOpts &&
311
- scopeOpts.effectiveEnvironmentScopedResources &&
312
- scopeOpts.runEnvKey &&
313
- (scopeOpts.runEnvKey === 'dev' || scopeOpts.runEnvKey === 'tst');
312
+ isScopeActiveForOptions(scopeOpts);
314
313
  const composeAppKey = useTraefikScope
315
314
  ? composeTraefikServiceKey(appName, scopeOpts.runEnvKey)
316
315
  : null;
@@ -380,9 +379,7 @@ function buildNetworkAndContainerNames(appName, devId, idNum, scopeOpts = null)
380
379
  let containerName;
381
380
  if (
382
381
  scopeOpts &&
383
- scopeOpts.effectiveEnvironmentScopedResources &&
384
- scopeOpts.runEnvKey &&
385
- (scopeOpts.runEnvKey === 'dev' || scopeOpts.runEnvKey === 'tst')
382
+ isScopeActiveForOptions(scopeOpts)
386
383
  ) {
387
384
  containerName = buildScopedLocalContainerName(appName, devId, idNum, scopeOpts.runEnvKey);
388
385
  } else {
@@ -8,11 +8,11 @@
8
8
 
9
9
  /**
10
10
  * @param {Object} options - Run options
11
- * @returns {'dev'|'tst'|'pro'}
11
+ * @returns {'miso'|'dev'|'tst'|'pro'}
12
12
  */
13
13
  function resolveMisoEnvironment(options) {
14
14
  const env = (options.env && typeof options.env === 'string') ? options.env.toLowerCase() : 'dev';
15
- return (env === 'tst' || env === 'pro') ? env : 'dev';
15
+ return (env === 'miso' || env === 'tst' || env === 'pro') ? env : 'dev';
16
16
  }
17
17
 
18
18
  module.exports = { resolveMisoEnvironment };
@@ -7,7 +7,8 @@
7
7
  */
8
8
 
9
9
  const {
10
- buildEnvScopedTraefikPath
10
+ buildEnvScopedTraefikPath,
11
+ isScopeActiveForOptions
11
12
  } = require('./environment-scoped-resources');
12
13
  const { parseDeveloperIdNum } = require('./declarative-url-ports');
13
14
  const {
@@ -122,9 +123,7 @@ function buildTraefikIngressBase(config, devId, scopeOpts, remoteServer, develop
122
123
  let pathOut = basePath;
123
124
  if (
124
125
  scopeOpts &&
125
- scopeOpts.effectiveEnvironmentScopedResources &&
126
- scopeOpts.runEnvKey &&
127
- (scopeOpts.runEnvKey === 'dev' || scopeOpts.runEnvKey === 'tst')
126
+ isScopeActiveForOptions(scopeOpts)
128
127
  ) {
129
128
  pathOut = buildEnvScopedTraefikPath(basePath, scopeOpts.runEnvKey);
130
129
  }
@@ -0,0 +1,92 @@
1
+ /** @fileoverview Safe binary metadata and keyed document persistence evidence. */
2
+ 'use strict';
3
+
4
+ const { keyedGetEvidence, hasExecutedKeyedGet } = require('./datasource-test-run-capability-scope');
5
+ const BINARY_FIELDS = new Set(['contentType', 'size', 'filename', 'sha256']);
6
+
7
+ /** @param {*} value @returns {Object} */
8
+ function binarySummary(value) {
9
+ if (!value || typeof value !== 'object') return {};
10
+ return Object.fromEntries(Object.entries(value).filter(([key, item]) =>
11
+ BINARY_FIELDS.has(key) && ['string', 'number'].includes(typeof item)));
12
+ }
13
+
14
+ /**
15
+ * Limit binary containers to metadata before printing or saving a report.
16
+ * Ordinary JSON record fields remain intact.
17
+ * @param {*} value
18
+ * @param {WeakSet} seen
19
+ * @returns {*}
20
+ */
21
+ function sanitizeBinaryEvidence(value, seen = new WeakSet()) {
22
+ if (!value || typeof value !== 'object') return value;
23
+ if (Buffer.isBuffer(value) || ArrayBuffer.isView(value)) return '[binary omitted]';
24
+ if (seen.has(value)) return '[Circular]';
25
+ seen.add(value);
26
+ if (Array.isArray(value)) {
27
+ const items = value.map(item => sanitizeBinaryEvidence(item, seen));
28
+ seen.delete(value);
29
+ return items;
30
+ }
31
+ const out = {};
32
+ for (const [key, item] of Object.entries(value)) {
33
+ if (['getBinary', 'listBinary', '_binary'].includes(key)) out[key] = binarySummary(item);
34
+ else if (value.keyedGet === true && key === 'document') {
35
+ out[key] = { ...binarySummary(item) };
36
+ if (typeof item?.key === 'string') out[key].key = item.key;
37
+ if (typeof item?.persisted === 'boolean') out[key].persisted = item.persisted;
38
+ } else if (value.keyedGet === true && ['data', 'bytes', 'body', 'content', 'base64'].includes(key)) continue;
39
+ else out[key] = sanitizeBinaryEvidence(item, seen);
40
+ }
41
+ seen.delete(value);
42
+ return out;
43
+ }
44
+
45
+ /** @param {Object} value @param {string[]} lines */
46
+ function appendBinaryAndPersistence(value, lines) {
47
+ if (!value || typeof value !== 'object') return;
48
+ if (value.getBinary) lines.push(` getBinary: ${JSON.stringify(binarySummary(value.getBinary))}`);
49
+ if (value.scope?.expectedSource === 'get') {
50
+ const observed = value.observed || {};
51
+ lines.push(` Document persistence (expectedSource: get): documents=${Number(observed.documents) || 0}, chunks=${Number(observed.chunks) || 0}`);
52
+ }
53
+ for (const [key, child] of Object.entries(value)) {
54
+ if (key !== 'getBinary') appendBinaryAndPersistence(child, lines);
55
+ }
56
+ }
57
+
58
+ /** @param {Object} envelope @returns {string[]} */
59
+ function binaryEvidenceLines(envelope) {
60
+ const lines = [];
61
+ for (const job of keyedGetEvidence(envelope)) {
62
+ const status = job.skipped === true ? 'skipped' : job.ok === true ? 'executed' : 'failed';
63
+ lines.push(` Keyed get: ${status} (${job.datasourceKey || envelope.datasourceKey || 'unknown'})`);
64
+ if (job.document) lines.push(` Document: ${JSON.stringify(binarySummary(job.document))}`);
65
+ }
66
+ appendBinaryAndPersistence(envelope?.integration?.stepResults, lines);
67
+ return lines;
68
+ }
69
+
70
+ /** @param {Object} envelope @param {string} key @returns {boolean} */
71
+ function hasKeyedDocumentPersistence(envelope, key) {
72
+ if (!hasExecutedKeyedGet(envelope, key)) return false;
73
+ const steps = envelope?.integration?.stepResults || [];
74
+ return steps.some(step => {
75
+ if (step.name !== 'document_persistence' || step.success === false || step.status === 'failed') return false;
76
+ const row = step.evidence?.datasources?.[key];
77
+ const model = row?.model;
78
+ return model?.scope?.expectedSource === 'get' && model.observed?.documents > 0 &&
79
+ model.observed?.chunks > 0 && (!Array.isArray(row.failures) || row.failures.length === 0);
80
+ });
81
+ }
82
+
83
+ /** @param {Object} result @param {string[]} keys @returns {boolean} */
84
+ function hasAllKeyedDocumentPersistence(result, keys) {
85
+ return keys.length > 0 && keys.every(key => {
86
+ const row = result?.results?.find(item => item.key === key);
87
+ return row?.success === true && hasKeyedDocumentPersistence(row.datasourceTestRun, key);
88
+ });
89
+ }
90
+
91
+ module.exports = { binarySummary, sanitizeBinaryEvidence, binaryEvidenceLines, hasKeyedDocumentPersistence,
92
+ hasAllKeyedDocumentPersistence };
@@ -4,6 +4,43 @@
4
4
  * @version 2.0.0
5
5
  */
6
6
 
7
+ /**
8
+ * Keyed document retrieval has no list operation, so the `get` runs once against a business
9
+ * key instead of over a sync page. The dataplane records that run as step evidence
10
+ * (`sync_step_keyed_get.py`), which is why execution is read from evidence rather than from
11
+ * a row count. `IntegrationStepResult.evidence` is an open object by contract.
12
+ * @param {Object|null|undefined} envelope - DatasourceTestRun-like
13
+ * @returns {Array<Object>} Keyed-get evidence entries, newest first as reported
14
+ */
15
+ function keyedGetEvidence(envelope) {
16
+ const integration = envelope && typeof envelope === 'object' ? envelope.integration : null;
17
+ const steps = integration && Array.isArray(integration.stepResults) ? integration.stepResults : [];
18
+ return steps
19
+ .map(step => (step && typeof step.evidence === 'object' ? step.evidence : null))
20
+ .flatMap(evidence => evidence && Array.isArray(evidence.jobs) ? evidence.jobs : [evidence])
21
+ .filter(evidence => evidence && evidence.keyedGet === true);
22
+ }
23
+
24
+ /**
25
+ * A keyed `get` counts as executed only when it actually returned a document. A skipped run
26
+ * (no primary key value) or a failed one is not execution, and reporting it as such would
27
+ * turn `--strict-capability-scope` into a rubber stamp.
28
+ * @param {Object|null|undefined} envelope - DatasourceTestRun-like
29
+ * @param {string|undefined|null} datasourceKey - Restrict to one datasource when given
30
+ * @returns {boolean} True when a keyed get ran successfully
31
+ */
32
+ function hasExecutedKeyedGet(envelope, datasourceKey) {
33
+ const wanted = datasourceKey === undefined || datasourceKey === null || String(datasourceKey).trim() === ''
34
+ ? ''
35
+ : String(datasourceKey).trim();
36
+ return keyedGetEvidence(envelope).some(evidence => {
37
+ if (evidence.ok !== true || evidence.skipped === true) {
38
+ return false;
39
+ }
40
+ return wanted === '' || String(evidence.datasourceKey || '') === wanted;
41
+ });
42
+ }
43
+
7
44
  /**
8
45
  * @param {Object|null|undefined} envelope - DatasourceTestRun-like
9
46
  * @param {string|undefined|null} requestedCapabilityKey - From positional [capabilityKey]
@@ -23,6 +60,10 @@ function analyzeCapabilityScope(envelope, requestedCapabilityKey) {
23
60
  envelope && typeof envelope === 'object' && Array.isArray(envelope.capabilities)
24
61
  ? envelope.capabilities
25
62
  : [];
63
+ if (key === 'get' && keyedGetEvidence(envelope).length > 0 &&
64
+ !hasExecutedKeyedGet(envelope, envelope.datasourceKey)) {
65
+ return { violated: true, message: 'Capabilities scope: keyed get was not executed successfully.' };
66
+ }
26
67
  if (caps.length <= 1) {
27
68
  return { violated: false };
28
69
  }
@@ -39,5 +80,7 @@ function analyzeCapabilityScope(envelope, requestedCapabilityKey) {
39
80
  }
40
81
 
41
82
  module.exports = {
42
- analyzeCapabilityScope
83
+ analyzeCapabilityScope,
84
+ keyedGetEvidence,
85
+ hasExecutedKeyedGet
43
86
  };
@@ -4,6 +4,7 @@
4
4
  * @version 2.0.0
5
5
  */
6
6
 
7
+ const { sanitizeBinaryEvidence } = require('./datasource-binary-evidence');
7
8
  const { SEP, appendReferenceLayoutLines } = require('./datasource-test-run-display');
8
9
  const { buildDebugEnvelopeSlice } = require('./datasource-test-run-debug-slice');
9
10
  const {
@@ -250,6 +251,7 @@ function applyRawLineCap(text) {
250
251
  * @returns {string}
251
252
  */
252
253
  function formatDatasourceTestRunDebugBlock(envelope, mode, isTTY) {
254
+ envelope = sanitizeBinaryEvidence(envelope);
253
255
  if (!envelope || typeof envelope !== 'object') return '';
254
256
 
255
257
  const lines = ['', SEP, `Debug (${mode})`];
@@ -5,9 +5,11 @@
5
5
  */
6
6
 
7
7
  const chalk = require('chalk');
8
+ const { binaryEvidenceLines } = require('./datasource-binary-evidence');
8
9
  const { sectionTitle, headerKeyValue, colorAggregateGlyph, successGlyph, failureGlyph } = require('./cli-test-layout-chalk');
9
10
  const { appendCertificateTTY, appendCertificateIssuanceTTY } = require('./datasource-test-run-certificate-tty');
10
11
  const { buildTtyMetaLines: buildTtyMetaLinesCore } = require('./datasource-test-run-tty-meta-lines');
12
+ const { appendStructuredIssueGuidance } = require('./datasource-test-run-issue-guidance');
11
13
 
12
14
  const SEP = '────────────────────────────────';
13
15
 
@@ -297,9 +299,12 @@ function appendValidationIssueLines(lines, envelope, maxIssues = 5) {
297
299
  const cap = Math.min(maxIssues, issues.length);
298
300
  for (let i = 0; i < cap; i += 1) {
299
301
  const iss = issues[i];
300
- const code = iss && iss.code ? chalk.red(`[${iss.code}] `) : '';
302
+ const issueCode = iss && (iss.stableCode || iss.code);
303
+ const code = issueCode ? chalk.red(`[${issueCode}] `) : '';
304
+ const severity = iss?.severity ? chalk.gray(`[${String(iss.severity)}] `) : '';
301
305
  const msg = iss && iss.message ? String(iss.message) : JSON.stringify(iss);
302
- lines.push(` ${code}${chalk.yellow(msg)}`);
306
+ lines.push(` ${code}${severity}${chalk.yellow(msg)}`);
307
+ appendStructuredIssueGuidance(lines, iss);
303
308
  appendDpSec013Details(lines, iss);
304
309
  }
305
310
  if (issues.length > cap) {
@@ -448,6 +453,7 @@ function formatDatasourceTestRunTTY(envelope, options = {}) {
448
453
  appendRefsSectionIfEnabled(lines, envelope, options);
449
454
  appendValidationIssueLines(lines, envelope);
450
455
  appendIntegrationStepLines(lines, envelope);
456
+ lines.push(...binaryEvidenceLines(envelope));
451
457
  const focus = normalizedFocusCapabilityKey(options.focusCapabilityKey);
452
458
  if (focus) {
453
459
  lines.push(formatCapabilityFocusSection(envelope, focus));
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Structured human guidance for DatasourceTestRun validation issues.
3
+ *
4
+ * @fileoverview Preserve actionable issue fields in TTY output
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const chalk = require('chalk');
10
+
11
+ const STEP_VERBS = {
12
+ locate_file: 'open',
13
+ update_path: 'update',
14
+ add_mapping: 'add mapping',
15
+ set_value: 'set',
16
+ remove_value: 'remove',
17
+ add_item: 'add to',
18
+ remove_item: 'remove from'
19
+ };
20
+
21
+ const UPSERT_GUIDANCE = {
22
+ 'DP-CIP-030': {
23
+ observed: 'The declared upsert does not contain the required create, update, and records-output composition.',
24
+ consequence: 'The runtime cannot execute one deterministic create-or-update operation.',
25
+ aiFixable: true,
26
+ remediation: [
27
+ 'Run aifabrix datasource capability upsert <file-or-key> --overwrite --dry-run, review the patch, then apply and validate it.'
28
+ ],
29
+ prevention: 'Author upsert through the capability command and keep both source halves valid.',
30
+ residualRisk: 'Publication remains blocked while the composition is malformed.',
31
+ rerun: 'Rerun the same focused datasource validation or E2E case.'
32
+ },
33
+ 'DP-CIP-031': {
34
+ observed: 'The upsert declares no usable business identity.',
35
+ consequence: 'Existence cannot be resolved, so a retry may create a duplicate or update the wrong record.',
36
+ aiFixable: true,
37
+ remediation: [
38
+ 'Run aifabrix datasource capability upsert <file-or-key> --identity <fields> --dry-run with author-approved mapped non-primary fields.'
39
+ ],
40
+ prevention: 'Model business identity before exposing a composed write.',
41
+ residualRisk: 'Upsert must remain unavailable until identity is explicit and validated.',
42
+ rerun: 'Rerun the same focused datasource validation or E2E case.'
43
+ },
44
+ 'DP-CIP-040': {
45
+ observed: 'No declared read operation can constrain the complete business identity.',
46
+ consequence: 'The source record may be missed, creating a duplicate or selecting the wrong update target.',
47
+ aiFixable: false,
48
+ remediation: [
49
+ 'Declare a real exact lookup only if the source API supports it.',
50
+ 'Otherwise keep separate create and update capabilities and withhold upsert readiness.'
51
+ ],
52
+ prevention: 'Require complete equality lookup coverage before authoring upsert.',
53
+ residualRisk: 'AI cannot repair a source API that lacks exact lookup or uniqueness.',
54
+ rerun: 'After a supported lookup is declared, rerun the same focused validation and E2E proof.'
55
+ },
56
+ 'DP-CIP-041': {
57
+ observed: 'A covering identity read is declared, but live filtering has not been proven.',
58
+ consequence: 'Static coverage alone cannot show that the source applies the constraint or exposes writes in time.',
59
+ aiFixable: false,
60
+ remediation: ['Run the focused live validation and two-write no-duplicate E2E proof; do not add a duplicate read declaration.'],
61
+ prevention: 'Keep exact lookup and no-duplicate proof scenarios with every upsert datasource.',
62
+ residualRisk: 'Read-after-write delay can leave a bounded duplicate window until runtime protection is proven.',
63
+ rerun: 'Rerun the same focused datasource validation and upsert E2E case.'
64
+ }
65
+ };
66
+
67
+ function normalizeOptionalString(value) {
68
+ return typeof value === 'string' ? value.trim() : '';
69
+ }
70
+
71
+ function hasGuidanceValue(value) {
72
+ if (Array.isArray(value)) return value.some(hasGuidanceValue);
73
+ if (typeof value === 'boolean') return true;
74
+ if (value && typeof value === 'object') return Object.keys(value).length > 0;
75
+ return normalizeOptionalString(value) !== '';
76
+ }
77
+
78
+ function firstGuidanceValue(...values) {
79
+ return values.find(hasGuidanceValue);
80
+ }
81
+
82
+ function guidancePayload(item) {
83
+ return normalizeOptionalString(item.value) || normalizeOptionalString(item.template);
84
+ }
85
+
86
+ function guidanceVerb(step) {
87
+ return STEP_VERBS[step] || step.replace(/_/g, ' ') || 'update';
88
+ }
89
+
90
+ /**
91
+ * Render one guidance entry, which may be a string or a structured fix instruction.
92
+ *
93
+ * `fix.instructions[]` entries are objects ({step, path, value, template, note}),
94
+ * so stringifying them printed `[object Object]` and the remediation — the part a
95
+ * developer acts on — was lost.
96
+ *
97
+ * @param {unknown} item guidance entry
98
+ * @returns {string|null} display text, or null when there is nothing to show
99
+ */
100
+ function formatGuidanceItem(item) {
101
+ if (typeof item === 'string') return item.trim() || null;
102
+ if (!item || typeof item !== 'object') return null;
103
+
104
+ const step = normalizeOptionalString(item.step);
105
+ const note = normalizeOptionalString(item.note);
106
+ const target = normalizeOptionalString(item.path);
107
+ const payload = guidancePayload(item);
108
+
109
+ // `explain` carries no path or value; its note *is* the instruction.
110
+ if (step === 'explain' || (!target && !payload)) {
111
+ return note || (step ? step.replace(/_/g, ' ') : null);
112
+ }
113
+
114
+ const verb = guidanceVerb(step);
115
+ // With no path the payload *is* the target (e.g. locate_file carries a path in
116
+ // `value`), so it reads as "open <file>" rather than "open: <file>".
117
+ let text = target ? `${verb} ${target}` : `${verb} ${payload}`;
118
+ if (target && payload) text += step === 'set_value' ? ` = ${payload}` : `: ${payload}`;
119
+ return note ? `${text} — ${note}` : text;
120
+ }
121
+
122
+ function appendGuidanceValue(lines, label, value) {
123
+ const values = Array.isArray(value) ? value : [value];
124
+ values
125
+ .map(formatGuidanceItem)
126
+ .filter(Boolean)
127
+ .forEach((text) => {
128
+ lines.push(` ${chalk.gray(`${label}:`)} ${chalk.white(text)}`);
129
+ });
130
+ }
131
+
132
+ function enrichUpsertIssueGuidance(issue) {
133
+ if (!issue || typeof issue !== 'object') return issue;
134
+ const code = issue.stableCode || issue.code;
135
+ const fallback = UPSERT_GUIDANCE[code];
136
+ if (!fallback) return issue;
137
+ const enriched = { ...issue };
138
+ Object.entries(fallback).forEach(([key, value]) => {
139
+ if (!hasGuidanceValue(enriched[key])) enriched[key] = value;
140
+ });
141
+ return enriched;
142
+ }
143
+
144
+ function appendStructuredIssueGuidance(lines, issue) {
145
+ if (!issue || typeof issue !== 'object') return;
146
+ const display = enrichUpsertIssueGuidance(issue);
147
+ appendGuidanceValue(lines, 'Observed', firstGuidanceValue(
148
+ issue.observed, issue.details?.diagnosis, display.observed
149
+ ));
150
+ appendGuidanceValue(lines, 'Consequence', firstGuidanceValue(
151
+ issue.impact, issue.consequence, issue.reason, display.consequence
152
+ ));
153
+ if (typeof display.aiFixable === 'boolean') {
154
+ appendGuidanceValue(lines, 'Can AI fix?', display.aiFixable
155
+ ? 'Yes — only as an authorized deterministic local manifest change.'
156
+ : 'No automatic fix is safe for this source or live-proof limitation.');
157
+ }
158
+ appendGuidanceValue(lines, 'Safe action', firstGuidanceValue(
159
+ issue.fix?.instructions, issue.remediation, issue.hint, display.remediation
160
+ ));
161
+ appendGuidanceValue(lines, 'Prevention', firstGuidanceValue(
162
+ issue.prevention, issue.fix?.prevention, display.prevention
163
+ ));
164
+ appendGuidanceValue(lines, 'Residual risk', firstGuidanceValue(
165
+ issue.residualRisk, issue.risk, display.residualRisk
166
+ ));
167
+ appendGuidanceValue(lines, 'Rerun', firstGuidanceValue(
168
+ issue.rerun, issue.fix?.rerun, display.rerun
169
+ ));
170
+ }
171
+
172
+ module.exports = {
173
+ appendStructuredIssueGuidance,
174
+ enrichUpsertIssueGuidance,
175
+ formatGuidanceItem
176
+ };
@@ -5,6 +5,7 @@
5
5
  'use strict';
6
6
 
7
7
  const chalk = require('chalk');
8
+ const { sanitizeBinaryEvidence } = require('./datasource-binary-evidence');
8
9
  const logger = require('./logger');
9
10
  const { getReportVersionStderrMessage } = require('./datasource-test-run-report-version');
10
11
  const {
@@ -42,6 +43,7 @@ function emitCapabilityScopeDiagnostics(envelope, opts = {}) {
42
43
  * @param {string} [options.requestedCapabilityKey]
43
44
  */
44
45
  function printDatasourceTestRunForTTY(envelope, options = {}) {
46
+ envelope = sanitizeBinaryEvidence(envelope);
45
47
  const mode = resolveDebugDisplayMode(options.debug);
46
48
  const displayOpts = {
47
49
  focusCapabilityKey: options.requestedCapabilityKey,
@@ -59,6 +59,9 @@ function dataplaneDockerHostPort(appPort, developerIdNum, environmentKey = 'dev'
59
59
  if (env === 'tst') {
60
60
  return baseHostPort + 20;
61
61
  }
62
+ if (env === 'pro') {
63
+ return baseHostPort + 30;
64
+ }
62
65
  return id === 0 ? appPort : baseHostPort;
63
66
  }
64
67
 
@@ -56,16 +56,17 @@ function readDeveloperIdFromConfig(config) {
56
56
  * Creates the mount folder on the local filesystem (next to the .env file) when it does not exist.
57
57
  * @param {string} content - Env file content
58
58
  * @param {string} outputPath - Resolved path of the .env file being written
59
+ * @param {string} [appName] - System key (defaults to output directory name)
59
60
  * @returns {string} Content with /mnt/data replaced by path to mount directory
60
61
  */
61
- function substituteMntDataForLocal(content, outputPath) {
62
+ function substituteMntDataForLocal(content, outputPath, appName) {
62
63
  const outputDir = path.dirname(outputPath);
63
64
  // Avoid introducing a drive letter for root-relative paths (e.g. "\tmp\out"),
64
65
  // which are common in unit tests and some Windows path constructions.
65
66
  const isWinRootRelative = process.platform === 'win32' && /^\\(?!\\)/.test(outputDir);
66
67
  const localMountPath = isWinRootRelative
67
- ? path.win32.normalize(path.win32.join(outputDir, 'mount'))
68
- : path.resolve(outputDir, 'mount');
68
+ ? path.win32.normalize(path.win32.join(outputDir, 'mount', 'data', appName || path.basename(outputDir)))
69
+ : path.resolve(outputDir, 'mount', 'data', appName || path.basename(outputDir));
69
70
  if (!fs.existsSync(localMountPath)) {
70
71
  fs.mkdirSync(localMountPath, { recursive: true });
71
72
  }
@@ -140,12 +141,12 @@ async function writeEnvOutputForReload(outputPath, runEnvPath, appName) {
140
141
  baseContent = await fsp.readFile(outputPath, 'utf8');
141
142
  } else if (appName) {
142
143
  baseContent = await generateEnvContent(appName, null, 'local', false);
143
- baseContent = substituteMntDataForLocal(baseContent, outputPath);
144
+ baseContent = substituteMntDataForLocal(baseContent, outputPath, appName);
144
145
  } else {
145
146
  baseContent = runContent;
146
147
  }
147
148
  let toWrite = mergeEnvMapIntoContent(baseContent, runMap, { appendMissingFromNewMap: false });
148
- toWrite = substituteMntDataForLocal(toWrite, outputPath);
149
+ toWrite = substituteMntDataForLocal(toWrite, outputPath, appName);
149
150
  await fsp.writeFile(outputPath, toWrite, { mode: 0o600 });
150
151
  logger.log(
151
152
  formatSuccessLine(
@@ -163,7 +164,7 @@ async function writeEnvOutputForReload(outputPath, runEnvPath, appName) {
163
164
  async function writeEnvOutputForLocal(appName, outputPath) {
164
165
  const { generateEnvContent, parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
165
166
  let localContent = await generateEnvContent(appName, null, 'local', false);
166
- localContent = substituteMntDataForLocal(localContent, outputPath);
167
+ localContent = substituteMntDataForLocal(localContent, outputPath, appName);
167
168
  let toWrite = localContent;
168
169
  if (fs.existsSync(outputPath)) {
169
170
  const existingContent = await fsp.readFile(outputPath, 'utf8');
@@ -319,7 +320,7 @@ async function patchEnvContentForLocal(envContent, variables) {
319
320
  async function syncWrittenBuilderEnvToOutputPath(envPath, outputPath, envOutputPathLabel, copyOptions = {}) {
320
321
  const { parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
321
322
  const raw = fs.readFileSync(envPath, 'utf8');
322
- const synced = substituteMntDataForLocal(raw, outputPath);
323
+ const synced = substituteMntDataForLocal(raw, outputPath, copyOptions.appName || path.basename(path.dirname(envPath)));
323
324
  let toWrite = synced;
324
325
  if (!copyOptions.freshEnv && fs.existsSync(outputPath)) {
325
326
  const existingContent = fs.readFileSync(outputPath, 'utf8');
@@ -344,7 +345,7 @@ async function writeLocalEnvToOutputPath(outputPath, appName, secretsPath, envOu
344
345
  const { generateEnvContent, parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
345
346
  const genOpts = extraOpts.appPath ? { appPath: extraOpts.appPath } : {};
346
347
  let localEnvContent = await generateEnvContent(appName, secretsPath, 'local', false, genOpts);
347
- localEnvContent = substituteMntDataForLocal(localEnvContent, outputPath);
348
+ localEnvContent = substituteMntDataForLocal(localEnvContent, outputPath, appName);
348
349
  let toWrite = localEnvContent;
349
350
  if (!extraOpts.freshEnv && fs.existsSync(outputPath)) {
350
351
  const existingContent = fs.readFileSync(outputPath, 'utf8');
@@ -366,7 +367,7 @@ async function writeLocalEnvToOutputPath(outputPath, appName, secretsPath, envOu
366
367
  async function writePatchedEnvToOutputPath(envPath, outputPath, variables, envOutputPathLabel) {
367
368
  const envContent = fs.readFileSync(envPath, 'utf8');
368
369
  let patchedContent = await patchEnvContentForLocal(envContent, variables);
369
- patchedContent = substituteMntDataForLocal(patchedContent, outputPath);
370
+ patchedContent = substituteMntDataForLocal(patchedContent, outputPath, variables.app?.key || path.basename(path.dirname(envPath)));
370
371
  fs.writeFileSync(outputPath, patchedContent, { mode: 0o600 });
371
372
  logger.log(formatSuccessLine(`Copied .env to: ${envOutputPathLabel}`));
372
373
  }
@@ -405,7 +406,7 @@ async function copyOrRegenerateEnvForNamedApp(opts) {
405
406
  return;
406
407
  }
407
408
  if (fs.existsSync(envPath)) {
408
- await syncWrittenBuilderEnvToOutputPath(envPath, outputPath, label, { freshEnv: freshEnv === true });
409
+ await syncWrittenBuilderEnvToOutputPath(envPath, outputPath, label, { freshEnv: freshEnv === true, appName });
409
410
  return;
410
411
  }
411
412
  await writeLocalEnvToOutputPath(outputPath, appName, secretsPath, label, localOpts);