@aifabrix/builder 2.44.5 → 2.45.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 (249) hide show
  1. package/.cursor/rules/cli-layout.mdc +8 -4
  2. package/.cursor/rules/project-rules.mdc +1 -1
  3. package/README.md +15 -23
  4. package/integration/hubspot-test/README.md +2 -0
  5. package/integration/hubspot-test/test.js +5 -3
  6. package/jest.projects.js +104 -2
  7. package/lib/api/controller-health.api.js +49 -0
  8. package/lib/api/dimension-values.api.js +82 -0
  9. package/lib/api/dimensions.api.js +114 -0
  10. package/lib/api/external-systems.api.js +1 -0
  11. package/lib/api/integration-clients.api.js +168 -0
  12. package/lib/api/types/dimension-values.types.js +28 -0
  13. package/lib/api/types/dimensions.types.js +31 -0
  14. package/lib/api/types/integration-clients.types.js +45 -0
  15. package/lib/api/validation-runner.js +46 -25
  16. package/lib/app/deploy-config.js +11 -1
  17. package/lib/app/deploy-status-display.js +3 -3
  18. package/lib/app/deploy.js +36 -14
  19. package/lib/app/display.js +15 -11
  20. package/lib/app/helpers.js +3 -3
  21. package/lib/app/index.js +3 -3
  22. package/lib/app/push.js +46 -23
  23. package/lib/app/register.js +7 -6
  24. package/lib/app/restart-display.js +126 -0
  25. package/lib/app/rotate-secret.js +7 -6
  26. package/lib/app/run-container-start.js +12 -6
  27. package/lib/app/run-env-compose.js +30 -1
  28. package/lib/app/run-helpers.js +58 -19
  29. package/lib/app/run-reload-sync.js +148 -0
  30. package/lib/app/run-resolve-image.js +51 -1
  31. package/lib/app/run.js +148 -74
  32. package/lib/app/show-display.js +7 -0
  33. package/lib/app/show.js +87 -5
  34. package/lib/build/index.js +83 -49
  35. package/lib/cli/doctor-check.js +117 -0
  36. package/lib/cli/index.js +8 -2
  37. package/lib/cli/infra-guided.js +460 -0
  38. package/lib/cli/installation-log-command.js +73 -0
  39. package/lib/cli/setup-app.js +31 -3
  40. package/lib/cli/setup-auth.js +98 -27
  41. package/lib/cli/setup-dev-path-commands.js +50 -3
  42. package/lib/cli/setup-infra-up-dataplane-action.js +111 -0
  43. package/lib/cli/setup-infra-up-platform-action.js +131 -0
  44. package/lib/cli/setup-infra.js +132 -118
  45. package/lib/cli/setup-integration-client.js +182 -0
  46. package/lib/cli/setup-parameters.js +21 -2
  47. package/lib/cli/setup-platform.js +102 -0
  48. package/lib/cli/setup-secrets.js +18 -6
  49. package/lib/cli/setup-utility-resolve.js +132 -0
  50. package/lib/cli/setup-utility.js +143 -84
  51. package/lib/commands/app-logs.js +81 -33
  52. package/lib/commands/auth-config.js +116 -18
  53. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  54. package/lib/commands/datasource-capability-output.js +29 -0
  55. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  56. package/lib/commands/datasource-capability.js +411 -0
  57. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  58. package/lib/commands/datasource.js +53 -13
  59. package/lib/commands/dev-down.js +3 -3
  60. package/lib/commands/dev-infra-gate.js +32 -0
  61. package/lib/commands/dev-init.js +13 -7
  62. package/lib/commands/dimension-value.js +179 -0
  63. package/lib/commands/dimension.js +330 -0
  64. package/lib/commands/integration-client.js +430 -0
  65. package/lib/commands/login-device.js +65 -30
  66. package/lib/commands/login.js +21 -10
  67. package/lib/commands/parameters-validate.js +78 -13
  68. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  69. package/lib/commands/repair-datasource-keys.js +10 -5
  70. package/lib/commands/repair-datasource.js +19 -7
  71. package/lib/commands/repair-env-template.js +4 -1
  72. package/lib/commands/repair-openapi-sync.js +172 -0
  73. package/lib/commands/repair-persist.js +102 -0
  74. package/lib/commands/repair-rbac-extract.js +27 -0
  75. package/lib/commands/repair-rbac-migrate.js +186 -0
  76. package/lib/commands/repair-rbac.js +214 -31
  77. package/lib/commands/repair-system-alignment.js +246 -0
  78. package/lib/commands/repair-system-permissions.js +168 -0
  79. package/lib/commands/repair.js +120 -338
  80. package/lib/commands/secure.js +1 -1
  81. package/lib/commands/setup-modes.js +468 -0
  82. package/lib/commands/setup-prompts.js +421 -0
  83. package/lib/commands/setup.js +254 -0
  84. package/lib/commands/teardown.js +277 -0
  85. package/lib/commands/up-common.js +113 -19
  86. package/lib/commands/up-dataplane.js +44 -19
  87. package/lib/commands/up-miso.js +18 -18
  88. package/lib/commands/upload.js +111 -23
  89. package/lib/commands/wizard-core-helpers.js +14 -11
  90. package/lib/commands/wizard-core.js +6 -5
  91. package/lib/commands/wizard-dataplane.js +2 -2
  92. package/lib/commands/wizard-entity-selection.js +4 -3
  93. package/lib/commands/wizard-headless.js +2 -1
  94. package/lib/commands/wizard.js +2 -1
  95. package/lib/constants/infra-compose-service-names.js +40 -0
  96. package/lib/core/audit-logger.js +1 -34
  97. package/lib/core/config-admin-email.js +56 -0
  98. package/lib/core/config-normalize.js +60 -0
  99. package/lib/core/config-registered-controller-urls.js +54 -0
  100. package/lib/core/config.js +33 -50
  101. package/lib/core/env-reader.js +16 -3
  102. package/lib/core/secrets-admin-env.js +101 -0
  103. package/lib/core/secrets-ensure-infra.js +34 -1
  104. package/lib/core/secrets-ensure.js +88 -66
  105. package/lib/core/secrets-env-content.js +428 -0
  106. package/lib/core/secrets-env-declarative-expand.js +170 -0
  107. package/lib/core/secrets-env-write.js +29 -1
  108. package/lib/core/secrets-load.js +252 -0
  109. package/lib/core/secrets-names.js +32 -0
  110. package/lib/core/secrets.js +17 -757
  111. package/lib/datasource/capability/basic-exposure.js +76 -0
  112. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  113. package/lib/datasource/capability/capability-key.js +34 -0
  114. package/lib/datasource/capability/capability-resolve.js +172 -0
  115. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  116. package/lib/datasource/capability/copy-operations.js +348 -0
  117. package/lib/datasource/capability/copy-test-payload.js +139 -0
  118. package/lib/datasource/capability/create-operations.js +235 -0
  119. package/lib/datasource/capability/dimension-operations.js +151 -0
  120. package/lib/datasource/capability/dimension-validate.js +219 -0
  121. package/lib/datasource/capability/json-pointer.js +31 -0
  122. package/lib/datasource/capability/reference-rewrite.js +51 -0
  123. package/lib/datasource/capability/relate-operations.js +325 -0
  124. package/lib/datasource/capability/relate-validate.js +219 -0
  125. package/lib/datasource/capability/remove-operations.js +275 -0
  126. package/lib/datasource/capability/run-capability-copy.js +152 -0
  127. package/lib/datasource/capability/run-capability-diff.js +135 -0
  128. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  129. package/lib/datasource/capability/run-capability-edit.js +377 -0
  130. package/lib/datasource/capability/run-capability-relate.js +193 -0
  131. package/lib/datasource/capability/run-capability-remove.js +105 -0
  132. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  133. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  134. package/lib/datasource/list.js +136 -23
  135. package/lib/datasource/log-viewer.js +2 -4
  136. package/lib/datasource/unified-validation-run.js +51 -16
  137. package/lib/datasource/validate.js +53 -1
  138. package/lib/deployment/deploy-poll-ui.js +60 -0
  139. package/lib/deployment/deployer-status.js +29 -3
  140. package/lib/deployment/deployer.js +48 -30
  141. package/lib/deployment/environment.js +7 -2
  142. package/lib/deployment/poll-interval.js +72 -0
  143. package/lib/deployment/push.js +11 -9
  144. package/lib/external-system/deploy.js +9 -2
  145. package/lib/external-system/download.js +61 -32
  146. package/lib/external-system/sync-deploy-manifest.js +33 -0
  147. package/lib/infrastructure/index.js +49 -19
  148. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  149. package/lib/internal/node-fs.js +2 -0
  150. package/lib/parameters/infra-kv-discovery.js +29 -4
  151. package/lib/parameters/infra-parameter-catalog.js +6 -3
  152. package/lib/parameters/infra-parameter-validate.js +67 -19
  153. package/lib/resolvers/datasource-resolver.js +53 -0
  154. package/lib/resolvers/dimension-file.js +52 -0
  155. package/lib/resolvers/manifest-resolver.js +133 -0
  156. package/lib/schema/application-schema.json +4 -0
  157. package/lib/schema/external-datasource.schema.json +183 -53
  158. package/lib/schema/external-system.schema.json +23 -10
  159. package/lib/schema/infra.parameter.yaml +26 -1
  160. package/lib/schema/wizard-config.schema.json +1 -1
  161. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  162. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  163. package/lib/utils/app-config-resolver.js +24 -1
  164. package/lib/utils/app-run-containers.js +2 -2
  165. package/lib/utils/applications-config-defaults.js +206 -0
  166. package/lib/utils/auth-config-validator.js +2 -12
  167. package/lib/utils/bash-secret-env.js +59 -0
  168. package/lib/utils/cli-secrets-error-format.js +78 -0
  169. package/lib/utils/cli-test-layout-chalk.js +31 -9
  170. package/lib/utils/cli-utils.js +4 -36
  171. package/lib/utils/compose-generate-docker-compose.js +111 -6
  172. package/lib/utils/compose-generator.js +17 -8
  173. package/lib/utils/controller-url.js +50 -7
  174. package/lib/utils/datasource-test-run-display.js +8 -0
  175. package/lib/utils/dev-hosts-helper.js +3 -2
  176. package/lib/utils/dev-init-ssh-merge.js +2 -1
  177. package/lib/utils/docker-build.js +17 -9
  178. package/lib/utils/docker-reload-mount.js +127 -0
  179. package/lib/utils/env-copy.js +99 -14
  180. package/lib/utils/env-template.js +5 -1
  181. package/lib/utils/external-readme.js +71 -2
  182. package/lib/utils/external-system-local-test-tty.js +3 -2
  183. package/lib/utils/external-system-readiness-core.js +45 -12
  184. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  185. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  186. package/lib/utils/external-system-readiness-display.js +10 -1
  187. package/lib/utils/file-upload.js +40 -3
  188. package/lib/utils/health-check-db-init.js +107 -0
  189. package/lib/utils/health-check-public-warn.js +69 -0
  190. package/lib/utils/health-check-url.js +28 -10
  191. package/lib/utils/health-check.js +139 -107
  192. package/lib/utils/help-builder.js +5 -1
  193. package/lib/utils/image-name.js +34 -7
  194. package/lib/utils/infra-optional-service-flags.js +69 -0
  195. package/lib/utils/installation-log-core.js +282 -0
  196. package/lib/utils/installation-log-record.js +237 -0
  197. package/lib/utils/installation-log.js +123 -0
  198. package/lib/utils/integration-file-backup.js +74 -0
  199. package/lib/utils/log-redaction.js +105 -0
  200. package/lib/utils/manifest-location.js +164 -0
  201. package/lib/utils/manifest-source-emit.js +162 -0
  202. package/lib/utils/mutagen-install.js +30 -3
  203. package/lib/utils/paths.js +308 -76
  204. package/lib/utils/postgres-wipe.js +212 -0
  205. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  206. package/lib/utils/remote-dev-auth.js +21 -5
  207. package/lib/utils/remote-docker-env.js +9 -1
  208. package/lib/utils/remote-secrets-loader.js +49 -4
  209. package/lib/utils/resolve-docker-image-ref.js +9 -3
  210. package/lib/utils/run-cli-flags.js +29 -0
  211. package/lib/utils/secrets-ancestor-paths.js +47 -0
  212. package/lib/utils/secrets-canonical.js +10 -3
  213. package/lib/utils/secrets-helpers.js +17 -10
  214. package/lib/utils/secrets-kv-refs.js +42 -0
  215. package/lib/utils/secrets-kv-scope.js +19 -2
  216. package/lib/utils/secrets-materialize-local.js +134 -0
  217. package/lib/utils/secrets-path.js +26 -13
  218. package/lib/utils/secrets-utils.js +20 -10
  219. package/lib/utils/system-builder-root.js +42 -0
  220. package/lib/utils/url-declarative-public-base.js +80 -12
  221. package/lib/utils/url-declarative-resolve-build-urls.js +238 -0
  222. package/lib/utils/url-declarative-resolve-build.js +24 -388
  223. package/lib/utils/url-declarative-resolve-expand-token.js +189 -0
  224. package/lib/utils/url-declarative-resolve-load-doc.js +12 -3
  225. package/lib/utils/url-declarative-resolve-surface-state.js +102 -0
  226. package/lib/utils/url-declarative-resolve.js +47 -7
  227. package/lib/utils/url-declarative-runtime-base-path.js +52 -0
  228. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  229. package/lib/utils/urls-local-registry-scan.js +103 -0
  230. package/lib/utils/urls-local-registry.js +158 -76
  231. package/lib/utils/validation-poll-ui.js +81 -0
  232. package/lib/utils/validation-run-poll.js +29 -5
  233. package/lib/utils/with-muted-logger.js +53 -0
  234. package/package.json +3 -1
  235. package/templates/applications/dataplane/application.yaml +5 -1
  236. package/templates/applications/dataplane/rbac.yaml +10 -10
  237. package/templates/applications/keycloak/env.template +8 -6
  238. package/templates/applications/miso-controller/application.yaml +9 -0
  239. package/templates/applications/miso-controller/env.template +27 -29
  240. package/templates/applications/miso-controller/rbac.yaml +9 -9
  241. package/templates/external-system/README.md.hbs +83 -123
  242. package/.npmrc.token +0 -1
  243. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  244. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  245. package/.nyc_output/processinfo/index.json +0 -1
  246. package/lib/api/service-users.api.js +0 -150
  247. package/lib/api/types/service-users.types.js +0 -65
  248. package/lib/cli/setup-service-user.js +0 -187
  249. package/lib/commands/service-user.js +0 -429
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Compare two datasource files on one capability slice (+ optional profile).
3
+ *
4
+ * @fileoverview capability diff runner
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { resolveValidateInputPath } = require('../validate');
12
+ const { normalizeCapabilityKey } = require('./capability-key');
13
+ const { extractCapabilitySliceForDiff } = require('./capability-diff-slice');
14
+ const {
15
+ compareObjects,
16
+ identifyBreakingChanges,
17
+ formatDiffOutput
18
+ } = require('../../core/diff');
19
+
20
+ /**
21
+ * @typedef {object} RunCapabilityDiffOpts
22
+ * @property {string} fileA
23
+ * @property {string} fileB
24
+ * @property {string} [capability] - Same key both sides
25
+ * @property {string} [capabilityA]
26
+ * @property {string} [capabilityB]
27
+ * @property {string} [profile] - Same profile both sides
28
+ * @property {string} [profileA]
29
+ * @property {string} [profileB]
30
+ */
31
+
32
+ /**
33
+ * @param {object} comparison - compareObjects result
34
+ * @param {string} label1
35
+ * @param {string} label2
36
+ * @returns {object}
37
+ */
38
+ function buildSliceDiffResult(comparison, label1, label2) {
39
+ const breakingChanges = identifyBreakingChanges(comparison);
40
+ return {
41
+ identical: comparison.identical,
42
+ file1: label1,
43
+ file2: label2,
44
+ version1: null,
45
+ version2: null,
46
+ versionChanged: false,
47
+ added: comparison.added,
48
+ removed: comparison.removed,
49
+ changed: comparison.changed,
50
+ breakingChanges,
51
+ summary: {
52
+ totalAdded: comparison.added.length,
53
+ totalRemoved: comparison.removed.length,
54
+ totalChanged: comparison.changed.length,
55
+ totalBreaking: breakingChanges.length
56
+ }
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Resolve capability keys for left/right files.
62
+ *
63
+ * @param {RunCapabilityDiffOpts} opts
64
+ * @returns {{ capA: string, capB: string }}
65
+ */
66
+ function resolveCapabilityKeys(opts) {
67
+ const shared = opts.capability ? String(opts.capability).trim() : '';
68
+ let capA = opts.capabilityA ? String(opts.capabilityA).trim() : '';
69
+ let capB = opts.capabilityB ? String(opts.capabilityB).trim() : '';
70
+ if (shared) {
71
+ capA = shared;
72
+ capB = shared;
73
+ }
74
+ if (!capA || !capB) {
75
+ throw new Error(
76
+ 'Provide --capability <key> for both sides, or both --capability-a and --capability-b'
77
+ );
78
+ }
79
+ return {
80
+ capA: normalizeCapabilityKey(capA, '--capability-a'),
81
+ capB: normalizeCapabilityKey(capB, '--capability-b')
82
+ };
83
+ }
84
+
85
+ /**
86
+ * @param {RunCapabilityDiffOpts} opts
87
+ * @returns {{ profA: string|undefined, profB: string|undefined }}
88
+ */
89
+ function resolveProfileKeys(opts) {
90
+ const shared = opts.profile ? String(opts.profile).trim() : '';
91
+ let profA = opts.profileA ? String(opts.profileA).trim() : '';
92
+ let profB = opts.profileB ? String(opts.profileB).trim() : '';
93
+ if (shared) {
94
+ profA = shared;
95
+ profB = shared;
96
+ }
97
+ const outA = profA || undefined;
98
+ const outB = profB || undefined;
99
+ return { profA: outA, profB: outB };
100
+ }
101
+
102
+ /**
103
+ * Deep-compare capability slices; prints via formatDiffOutput.
104
+ *
105
+ * @param {RunCapabilityDiffOpts} opts
106
+ * @returns {{ identical: boolean, diffResult: object }}
107
+ */
108
+ function runCapabilityDiff(opts) {
109
+ const pathA = resolveValidateInputPath(opts.fileA.trim());
110
+ const pathB = resolveValidateInputPath(opts.fileB.trim());
111
+ const docA = JSON.parse(fs.readFileSync(pathA, 'utf8'));
112
+ const docB = JSON.parse(fs.readFileSync(pathB, 'utf8'));
113
+
114
+ const { capA, capB } = resolveCapabilityKeys(opts);
115
+ const { profA, profB } = resolveProfileKeys(opts);
116
+
117
+ const sliceA = extractCapabilitySliceForDiff(docA, capA, profA);
118
+ const sliceB = extractCapabilitySliceForDiff(docB, capB, profB);
119
+
120
+ const comparison = compareObjects(sliceA, sliceB);
121
+ const label1 = `${path.basename(pathA)} → ${capA}${profA ? ` + profile:${profA}` : ''}`;
122
+ const label2 = `${path.basename(pathB)} → ${capB}${profB ? ` + profile:${profB}` : ''}`;
123
+ const diffResult = buildSliceDiffResult(comparison, label1, label2);
124
+
125
+ formatDiffOutput(diffResult);
126
+
127
+ return { identical: comparison.identical, diffResult };
128
+ }
129
+
130
+ module.exports = {
131
+ runCapabilityDiff,
132
+ resolveCapabilityKeys,
133
+ resolveProfileKeys,
134
+ buildSliceDiffResult
135
+ };
@@ -0,0 +1,291 @@
1
+ /**
2
+ * File-backed capability dimension (root dimensions binding) with semantic validation, backup, and atomic write.
3
+ *
4
+ * @fileoverview runCapabilityDimension
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const { resolveControllerUrl } = require('../../utils/controller-url');
13
+ const { normalizeControllerUrl } = require('../../core/config');
14
+ const { getOrRefreshDeviceToken } = require('../../utils/token-manager');
15
+ const { listDimensions } = require('../../api/dimensions.api');
16
+ const {
17
+ resolveValidateInputPath,
18
+ validateDatasourceParsed
19
+ } = require('../validate');
20
+ const { applyCapabilityDimension } = require('./dimension-operations');
21
+ const { validateDimensionSemantics } = require('./dimension-validate');
22
+ const { writeBackup, atomicWriteJson } = require('./run-capability-copy');
23
+ const { tryResolveDatasourceKeyToLocalPath, readJsonFile } = require('../../resolvers/datasource-resolver');
24
+ const { tryFetchDatasourceConfig } = require('../../resolvers/manifest-resolver');
25
+
26
+ function resolveSystemKeyForAuth(sourceDoc) {
27
+ return typeof sourceDoc?.systemKey === 'string' ? sourceDoc.systemKey.trim() : '';
28
+ }
29
+
30
+ async function maybeLoadDimensionCatalogKeys() {
31
+ try {
32
+ const controllerUrl = await resolveControllerUrl();
33
+ if (!controllerUrl) {
34
+ return { ok: false, notAuthenticated: true, keys: null, reason: 'controller_url_missing' };
35
+ }
36
+ const normalized = normalizeControllerUrl(controllerUrl);
37
+ const deviceToken = await getOrRefreshDeviceToken(normalized);
38
+ if (!deviceToken || !deviceToken.token) {
39
+ return { ok: false, notAuthenticated: true, keys: null, reason: 'no_token' };
40
+ }
41
+ const authConfig = { type: 'bearer', token: deviceToken.token };
42
+ const res = await listDimensions(deviceToken.controller || normalized, authConfig, {
43
+ page: 1,
44
+ pageSize: 500
45
+ });
46
+ const items = res?.data?.items || res?.data?.data?.items || res?.items || [];
47
+ const keys = new Set(
48
+ Array.isArray(items) ? items.map((d) => String(d?.key || '').trim()).filter(Boolean) : []
49
+ );
50
+ return { ok: true, notAuthenticated: false, keys, reason: undefined };
51
+ } catch (e) {
52
+ const msg = e?.message || String(e);
53
+ const notAuthenticated = /Not authenticated|Authentication required|login/i.test(msg);
54
+ return { ok: false, notAuthenticated, keys: null, reason: msg };
55
+ }
56
+ }
57
+
58
+ function parseViaList(via) {
59
+ const out = [];
60
+ for (const raw of Array.isArray(via) ? via : []) {
61
+ const s = String(raw || '').trim();
62
+ if (!s) continue;
63
+ const idx = s.indexOf(':');
64
+ if (idx <= 0 || idx >= s.length - 1) {
65
+ throw new Error(`--via must be in form <fkName>:<dimensionKey>, got "${s}"`);
66
+ }
67
+ out.push({ fk: s.slice(0, idx).trim(), dimension: s.slice(idx + 1).trim() });
68
+ }
69
+ return out;
70
+ }
71
+
72
+ function collectTargetDatasourceKeysForVia(sourceDoc, via) {
73
+ const fks = Array.isArray(sourceDoc?.foreignKeys) ? sourceDoc.foreignKeys : [];
74
+ const byName = new Map();
75
+ for (const fk of fks) {
76
+ const name = typeof fk?.name === 'string' ? fk.name.trim() : '';
77
+ if (name) byName.set(name, fk);
78
+ }
79
+ const targets = new Set();
80
+ for (const hop of via) {
81
+ const fkRow = byName.get(String(hop?.fk || '').trim());
82
+ const t = typeof fkRow?.targetDatasource === 'string' ? fkRow.targetDatasource.trim() : '';
83
+ if (t) targets.add(t);
84
+ }
85
+ return [...targets];
86
+ }
87
+
88
+ async function loadRemoteTargetsByKey({ sourceDoc, via, systemKeyForAuth }) {
89
+ /** @type {Record<string, any>} */
90
+ const remoteTargetsByKey = {};
91
+ const meta = { attempted: false, ok: false, notAuthenticated: false, fetchedKeys: [] };
92
+
93
+ const targetDatasourceKeys = collectTargetDatasourceKeysForVia(sourceDoc, via);
94
+ if (targetDatasourceKeys.length === 0) {
95
+ return { remoteTargetsByKey, remoteFetchMeta: meta };
96
+ }
97
+
98
+ _loadTargetsFromDisk(targetDatasourceKeys, remoteTargetsByKey);
99
+ await _maybeFetchTargetsRemote(targetDatasourceKeys, remoteTargetsByKey, systemKeyForAuth, meta);
100
+ return { remoteTargetsByKey, remoteFetchMeta: meta };
101
+ }
102
+
103
+ function _loadTargetsFromDisk(targetDatasourceKeys, remoteTargetsByKey) {
104
+ targetDatasourceKeys.forEach((dsKey) => {
105
+ const local = tryResolveDatasourceKeyToLocalPath(dsKey);
106
+ if (local.ok) {
107
+ remoteTargetsByKey[dsKey] = readJsonFile(local.path);
108
+ }
109
+ });
110
+ }
111
+
112
+ async function _maybeFetchTargetsRemote(targetDatasourceKeys, remoteTargetsByKey, systemKeyForAuth, meta) {
113
+ if (!systemKeyForAuth) return;
114
+ const remaining = targetDatasourceKeys.filter((k) => !remoteTargetsByKey[k]);
115
+ if (remaining.length === 0) {
116
+ meta.ok = true;
117
+ meta.fetchedKeys = targetDatasourceKeys;
118
+ return;
119
+ }
120
+ meta.attempted = true;
121
+ await _fetchRemainingTargets(remaining, remoteTargetsByKey, systemKeyForAuth, meta);
122
+ meta.ok = meta.fetchedKeys.length > 0 && !meta.notAuthenticated;
123
+ }
124
+
125
+ async function _fetchRemainingTargets(remaining, remoteTargetsByKey, systemKeyForAuth, meta) {
126
+ for (const dsKey of remaining) {
127
+ const remote = await tryFetchDatasourceConfig(systemKeyForAuth, dsKey, { silent: true });
128
+ if (remote.ok) {
129
+ remoteTargetsByKey[dsKey] = remote.datasourceConfig;
130
+ meta.fetchedKeys.push(dsKey);
131
+ continue;
132
+ }
133
+ if (remote.code === 'not_authenticated') {
134
+ meta.notAuthenticated = true;
135
+ }
136
+ }
137
+ }
138
+
139
+ function runSemanticValidation({ sourceDoc, opts, remoteTargetsByKey, catalogKeys }) {
140
+ const semantic = validateDimensionSemantics({
141
+ localContext: {
142
+ sourceDoc,
143
+ dimensionKey: opts.dimension,
144
+ type: opts.type,
145
+ field: opts.field,
146
+ via: opts.via
147
+ },
148
+ remoteTargetsByKey,
149
+ catalogDimensionKeys: catalogKeys
150
+ });
151
+ if (!semantic.ok) {
152
+ const err = new Error(semantic.errors.join('\n'));
153
+ err.validationErrors = semantic.errors;
154
+ err.validationWarnings = semantic.warnings;
155
+ throw err;
156
+ }
157
+ return semantic;
158
+ }
159
+
160
+ function normalizeRunOptions(rawOpts) {
161
+ return {
162
+ ...rawOpts,
163
+ fileOrKey: String(rawOpts.fileOrKey || '').trim(),
164
+ dimension: String(rawOpts.dimension || '').trim(),
165
+ type: /** @type {any} */ (String(rawOpts.type || '').trim()),
166
+ field: rawOpts.field !== undefined && rawOpts.field !== null ? String(rawOpts.field).trim() : undefined,
167
+ via: parseViaList(rawOpts.via),
168
+ actor: rawOpts.actor !== undefined && rawOpts.actor !== null ? String(rawOpts.actor).trim() : undefined,
169
+ operator: rawOpts.operator !== undefined && rawOpts.operator !== null ? String(rawOpts.operator).trim() : undefined,
170
+ overwrite: Boolean(rawOpts.overwrite),
171
+ dryRun: Boolean(rawOpts.dryRun),
172
+ noBackup: Boolean(rawOpts.noBackup)
173
+ };
174
+ }
175
+
176
+ function buildSemanticWarnings({ opts, semantic, catalog }) {
177
+ const warnings = [...(Array.isArray(semantic?.warnings) ? semantic.warnings : [])];
178
+ if (opts.type === 'fk' && (!opts.actor || !opts.actor.trim())) {
179
+ warnings.push('dimension type=fk without actor; set --actor for predictable ABAC binding.');
180
+ }
181
+ if (!catalog.ok && catalog.notAuthenticated) {
182
+ warnings.push('Dimension catalog validation skipped (not authenticated).');
183
+ }
184
+ return warnings;
185
+ }
186
+
187
+ function applyBindingAndValidateSchema({ parsed, opts, semanticWarnings, remoteFetchMeta }) {
188
+ const result = applyCapabilityDimension(parsed, {
189
+ dimension: opts.dimension,
190
+ type: opts.type,
191
+ field: opts.field,
192
+ via: opts.via,
193
+ actor: opts.actor,
194
+ operator: opts.operator,
195
+ required: opts.required,
196
+ overwrite: Boolean(opts.overwrite)
197
+ });
198
+
199
+ const validation = validateDatasourceParsed(result.doc);
200
+ if (!validation.valid) {
201
+ const err = new Error(validation.errors.join('\n'));
202
+ err.validationErrors = validation.errors;
203
+ err.validationWarnings = semanticWarnings;
204
+ err.remoteValidation = remoteFetchMeta;
205
+ throw err;
206
+ }
207
+
208
+ return { result, validation };
209
+ }
210
+
211
+ /**
212
+ * @typedef {object} RunCapabilityDimensionOpts
213
+ * @property {string} fileOrKey
214
+ * @property {string} dimension
215
+ * @property {'local'|'fk'} type
216
+ * @property {string|undefined} [field]
217
+ * @property {string[]|undefined} [via] - raw CLI strings; parsed before validate
218
+ * @property {string|undefined} [actor]
219
+ * @property {string|undefined} [operator]
220
+ * @property {boolean|undefined} [required]
221
+ * @property {boolean} [dryRun=false]
222
+ * @property {boolean} [noBackup=false]
223
+ * @property {boolean} [overwrite=false]
224
+ */
225
+
226
+ /**
227
+ * @param {RunCapabilityDimensionOpts} rawOpts
228
+ * @returns {Promise<any>}
229
+ */
230
+ async function runCapabilityDimension(rawOpts) {
231
+ const opts = normalizeRunOptions(rawOpts);
232
+ const resolvedPath = resolveValidateInputPath(opts.fileOrKey);
233
+ const raw = fs.readFileSync(resolvedPath, 'utf8');
234
+ const parsed = JSON.parse(raw);
235
+
236
+ const systemKeyForAuth = resolveSystemKeyForAuth(parsed);
237
+ const { remoteTargetsByKey, remoteFetchMeta } = await loadRemoteTargetsByKey({
238
+ sourceDoc: parsed,
239
+ via: opts.via,
240
+ systemKeyForAuth
241
+ });
242
+
243
+ const catalog = await maybeLoadDimensionCatalogKeys();
244
+ const catalogKeys = catalog.ok ? catalog.keys : null;
245
+
246
+ const semantic = runSemanticValidation({
247
+ sourceDoc: parsed,
248
+ opts,
249
+ remoteTargetsByKey,
250
+ catalogKeys
251
+ });
252
+ const semanticWarnings = buildSemanticWarnings({ opts, semantic, catalog });
253
+ const { result, validation } = applyBindingAndValidateSchema({
254
+ parsed,
255
+ opts,
256
+ semanticWarnings,
257
+ remoteFetchMeta
258
+ });
259
+
260
+ if (opts.dryRun) {
261
+ return {
262
+ dryRun: true,
263
+ resolvedPath,
264
+ patchOperations: result.patchOperations,
265
+ updatedSections: result.updatedSections,
266
+ backupPath: null,
267
+ validation,
268
+ semanticWarnings,
269
+ remoteValidation: remoteFetchMeta
270
+ };
271
+ }
272
+
273
+ const backupPath = writeBackup(resolvedPath, Boolean(opts.noBackup));
274
+ atomicWriteJson(resolvedPath, result.doc);
275
+
276
+ return {
277
+ dryRun: false,
278
+ resolvedPath,
279
+ patchOperations: result.patchOperations,
280
+ updatedSections: result.updatedSections,
281
+ backupPath,
282
+ validation,
283
+ semanticWarnings,
284
+ remoteValidation: remoteFetchMeta
285
+ };
286
+ }
287
+
288
+ module.exports = {
289
+ runCapabilityDimension
290
+ };
291
+