@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
@@ -0,0 +1,202 @@
1
+ /**
2
+ * File-backed upsert capability authoring with validation and atomic write.
3
+ *
4
+ * @fileoverview Run datasource capability upsert CLI operation
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const {
10
+ readDatasourceConfigFile,
11
+ writeBackup,
12
+ atomicWriteJson
13
+ } = require('./run-capability-copy');
14
+ const { validateDatasourceParsed, resolveValidateInputPath } = require('../validate');
15
+ const {
16
+ resolveVendorOpenApiSpec,
17
+ inferIntegrationContextFromPath
18
+ } = require('../../validation/openapi-contract-surface-validator');
19
+ const { analyzeUpsertIngredients } = require('./upsert-ingredients');
20
+ const { applyUpsertCapability, existingUpsertMatches } = require('./upsert-operations');
21
+ const { scaffoldUpsertTests } = require('./upsert-test-scaffold');
22
+
23
+ function parseIdentityFields(raw) {
24
+ if (raw === undefined || raw === null || String(raw).trim() === '') return [];
25
+ return [...new Set(String(raw).split(',').map((field) => field.trim()).filter(Boolean))];
26
+ }
27
+
28
+ function applyExplicitIdentity(doc, fields) {
29
+ if (fields.length === 0) return [];
30
+ const mapped = new Set(Object.keys(doc.fieldMappings?.attributes || {}));
31
+ const primary = new Set(Array.isArray(doc.primaryKey) ? doc.primaryKey : []);
32
+ const invalid = fields.filter((field) => !mapped.has(field) || primary.has(field));
33
+ if (invalid.length > 0) {
34
+ throw new Error(
35
+ `--identity must name mapped non-primary-key fields; invalid: ${invalid.join(', ')}`
36
+ );
37
+ }
38
+ if (!doc.validation) doc.validation = {};
39
+ if (!Array.isArray(doc.validation.repeatingValues)) {
40
+ doc.validation.repeatingValues = [];
41
+ }
42
+ const added = [];
43
+ fields.forEach((field) => {
44
+ const existing = doc.validation.repeatingValues.find((row) => row?.field === field);
45
+ if (existing && existing.strategy !== 'reject') {
46
+ throw new Error(`validation.repeatingValues for "${field}" already uses ${existing.strategy}`);
47
+ }
48
+ if (!existing) {
49
+ doc.validation.repeatingValues.push({ field, scope: ['global'], strategy: 'reject' });
50
+ added.push(field);
51
+ }
52
+ });
53
+ return added;
54
+ }
55
+
56
+ function resolveVendorSpec(doc, resolvedPath, providedSpec) {
57
+ if (providedSpec) return { spec: providedSpec, localPath: null };
58
+ const context = inferIntegrationContextFromPath(resolvedPath);
59
+ const documentKey = typeof doc.openapi?.documentKey === 'string'
60
+ ? doc.openapi.documentKey.trim()
61
+ : '';
62
+ if (!context || !documentKey) return { spec: null, localPath: null };
63
+ return resolveVendorOpenApiSpec(context.appPath, context.systemKey, documentKey);
64
+ }
65
+
66
+ function ineligibleError(analysis, vendor) {
67
+ const detail = analysis.issues.map((row) => `${row.code}: ${row.message}`).join('\n');
68
+ const error = new Error(detail || 'Upsert ingredients are not eligible');
69
+ error.analysis = analysis;
70
+ error.vendorOpenApiPath = vendor.localPath;
71
+ return error;
72
+ }
73
+
74
+ function applyTestScaffold(result, analysis, targetKey) {
75
+ const scaffold = scaffoldUpsertTests(result.doc, analysis, targetKey);
76
+ if (scaffold.issue) {
77
+ throw ineligibleError({
78
+ eligible: false,
79
+ status: 'author-fixable',
80
+ issues: [scaffold.issue]
81
+ }, { localPath: null });
82
+ }
83
+ if (!scaffold.changed) return false;
84
+ result.doc = scaffold.doc;
85
+ result.patchOperations.push(...scaffold.patchOperations);
86
+ result.updatedSections.push(...scaffold.updatedSections);
87
+ return true;
88
+ }
89
+
90
+ function buildEnvelope(opts, parsed, analysis, result) {
91
+ return {
92
+ changed: true,
93
+ eligible: true,
94
+ capabilityKey: result.resolvedAs,
95
+ identityFields: analysis.identityFields,
96
+ createKey: analysis.halves.create.logical,
97
+ updateKey: analysis.halves.update.logical,
98
+ lookupCandidates: analysis.lookupCandidates,
99
+ issues: analysis.issues,
100
+ patchOperations: result.patchOperations,
101
+ nextActions: [
102
+ `aifabrix datasource capability validate ${opts.fileOrKey} --capability ${result.resolvedAs}`,
103
+ `aifabrix validate ${parsed.systemKey || '<system-key>'}`,
104
+ `aifabrix datasource test-e2e ${parsed.key || '<datasource-key>'} --app ${parsed.systemKey || '<system-key>'} --json`
105
+ ]
106
+ };
107
+ }
108
+
109
+ function prependIdentityPatches(result, doc, added, state) {
110
+ if (added.length === 0) return;
111
+ let patches;
112
+ if (!state.hadValidation) {
113
+ patches = [{ op: 'add', path: '/validation', value: doc.validation }];
114
+ } else if (!state.hadRepeatingValues) {
115
+ patches = [{
116
+ op: 'add',
117
+ path: '/validation/repeatingValues',
118
+ value: doc.validation.repeatingValues
119
+ }];
120
+ } else {
121
+ patches = added.map((field) => ({
122
+ op: 'add',
123
+ path: '/validation/repeatingValues/-',
124
+ value: { field, scope: ['global'], strategy: 'reject' }
125
+ }));
126
+ }
127
+ result.patchOperations.unshift(...patches);
128
+ added.forEach((field) => result.updatedSections.push(`validation.repeatingValues: ${field}`));
129
+ }
130
+
131
+ function unchangedUpsertResult(opts, parsed, analysis, resolvedPath) {
132
+ const result = { resolvedAs: opts.as || 'upsert', patchOperations: [], updatedSections: [] };
133
+ const validation = validateDatasourceParsed(parsed);
134
+ const envelope = buildEnvelope(opts, parsed, analysis, result);
135
+ return {
136
+ ...envelope,
137
+ changed: false,
138
+ dryRun: Boolean(opts.dryRun),
139
+ resolvedPath,
140
+ backupPath: null,
141
+ updatedSections: [],
142
+ validation
143
+ };
144
+ }
145
+
146
+ function validateGeneratedDoc(doc) {
147
+ const validation = validateDatasourceParsed(doc);
148
+ if (validation.valid) return validation;
149
+ const error = new Error(validation.errors.join('\n'));
150
+ error.validationErrors = validation.errors;
151
+ throw error;
152
+ }
153
+
154
+ function finishUpsertRun(opts, resolvedPath, result, envelope, validation) {
155
+ const common = { resolvedPath, updatedSections: result.updatedSections, validation };
156
+ if (opts.dryRun) return { ...envelope, ...common, dryRun: true, backupPath: null };
157
+ const backupPath = writeBackup(resolvedPath, Boolean(opts.noBackup));
158
+ atomicWriteJson(resolvedPath, result.doc);
159
+ return { ...envelope, ...common, dryRun: false, backupPath };
160
+ }
161
+
162
+ async function runCapabilityUpsert(opts) {
163
+ const resolvedPath = resolveValidateInputPath(String(opts.fileOrKey || '').trim());
164
+ const parsed = readDatasourceConfigFile(resolvedPath);
165
+ const identityState = {
166
+ hadValidation: Boolean(parsed.validation),
167
+ hadRepeatingValues: Array.isArray(parsed.validation?.repeatingValues)
168
+ };
169
+ const identityAdded = applyExplicitIdentity(parsed, parseIdentityFields(opts.identity));
170
+ const vendor = resolveVendorSpec(parsed, resolvedPath, opts.vendorSpec);
171
+ const analysis = analyzeUpsertIngredients(parsed, {
172
+ createKey: opts.create || 'create',
173
+ updateKey: opts.update || 'update',
174
+ targetKey: opts.as || 'upsert',
175
+ vendorSpec: vendor.spec
176
+ });
177
+ if (!analysis.eligible) throw ineligibleError(analysis, vendor);
178
+ const targetKey = opts.as || 'upsert';
179
+ const targetMatches = !opts.overwrite && existingUpsertMatches(parsed, analysis, targetKey);
180
+ const result = targetMatches
181
+ ? { doc: parsed, resolvedAs: targetKey, patchOperations: [], updatedSections: [] }
182
+ : applyUpsertCapability(parsed, analysis, {
183
+ targetKey,
184
+ overwrite: Boolean(opts.overwrite)
185
+ });
186
+ prependIdentityPatches(result, parsed, identityAdded, identityState);
187
+ const testChanged = opts.test ? applyTestScaffold(result, analysis, targetKey) : false;
188
+ if (targetMatches && identityAdded.length === 0 && !testChanged) {
189
+ return unchangedUpsertResult(opts, parsed, analysis, resolvedPath);
190
+ }
191
+ const validation = validateGeneratedDoc(result.doc);
192
+ const envelope = buildEnvelope(opts, parsed, analysis, result);
193
+ return finishUpsertRun(opts, resolvedPath, result, envelope, validation);
194
+ }
195
+
196
+ module.exports = {
197
+ runCapabilityUpsert,
198
+ parseIdentityFields,
199
+ applyExplicitIdentity,
200
+ resolveVendorSpec,
201
+ applyTestScaffold
202
+ };
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Static authoring eligibility for manifest-declared upsert capabilities.
3
+ *
4
+ * @fileoverview Upsert ingredient analysis without runtime readiness claims
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { findVendorOperation } = require('../../validation/openapi-contract-vendor-validation');
10
+ const {
11
+ resolveSourceCapabilityForCopy,
12
+ resolveOpsKeysForCapability
13
+ } = require('./capability-resolve');
14
+
15
+ const FUZZY_TOKENS = new Set([
16
+ 'contains', 'like', 'ilike', 'token', 'tokenized', 'prefix', 'suffix',
17
+ 'substring', 'fuzzy', 'partial', 'startswith', 'endswith'
18
+ ]);
19
+
20
+ const ISSUE_GUIDANCE = {
21
+ 'UPSERT-HALF-INVALID': {
22
+ consequence: 'The upsert would not compose one valid create half and one valid update half.',
23
+ remediation: ['Repair or select existing create-shaped and update-shaped capabilities.'],
24
+ prevention: 'Validate each write capability before composing upsert.',
25
+ residualRisk: 'Authoring remains blocked until both halves are valid.'
26
+ },
27
+ 'UPSERT-IDENTITY-MISSING': {
28
+ consequence: 'Record existence cannot be resolved, so a retry may create a duplicate.',
29
+ remediation: ['Declare mapped business identity fields explicitly with --identity.'],
30
+ prevention: 'Model business identity before adding write orchestration.',
31
+ residualRisk: 'Do not publish or claim upsert while identity is absent.'
32
+ },
33
+ 'UPSERT-IDENTITY-UNMAPPED': {
34
+ consequence: 'The runtime cannot bind every declared identity field to source data.',
35
+ remediation: ['Add valid field mappings or select mapped non-primary-key identity fields.'],
36
+ prevention: 'Keep identity declarations and field mappings in the same review.',
37
+ residualRisk: 'An incomplete identity can select or update the wrong record.'
38
+ },
39
+ 'UPSERT-OPENAPI-UNRESOLVED': {
40
+ consequence: 'Builder cannot establish static authoring eligibility.',
41
+ remediation: ['Materialize the registered vendor OpenAPI document, then rerun the command.'],
42
+ prevention: 'Keep the vendor OpenAPI document synchronized before capability authoring.',
43
+ residualRisk: 'Source support is unknown; no capability was written.'
44
+ },
45
+ 'UPSERT-EXACT-LOOKUP-MISSING': {
46
+ consequence: 'A write may create a duplicate or update the wrong business record.',
47
+ remediation: [
48
+ 'Bind the complete identity to a real equality lookup when the source supports it.',
49
+ 'If the source cannot support exact lookup, keep separate create and update capabilities.'
50
+ ],
51
+ prevention: 'Require complete equality binding and a two-write no-duplicate E2E case.',
52
+ residualRisk: 'AI cannot repair an external API that lacks exact lookup or uniqueness.'
53
+ }
54
+ };
55
+
56
+ function uniqueStrings(values) {
57
+ return [...new Set(values.map((value) => String(value).trim()).filter(Boolean))];
58
+ }
59
+
60
+ function declaredIdentityFields(doc) {
61
+ const primary = new Set(uniqueStrings(Array.isArray(doc.primaryKey) ? doc.primaryKey : []));
62
+ const rows = Array.isArray(doc.validation?.repeatingValues)
63
+ ? doc.validation.repeatingValues
64
+ : [];
65
+ return uniqueStrings(
66
+ rows
67
+ .filter((row) => row?.strategy === 'reject' && !primary.has(row.field))
68
+ .map((row) => row.field)
69
+ );
70
+ }
71
+
72
+ function mappedFields(doc) {
73
+ const attrs = doc.fieldMappings?.attributes;
74
+ return new Set(attrs && typeof attrs === 'object' ? Object.keys(attrs) : []);
75
+ }
76
+
77
+ function assertSourceHalf(doc, logicalKey, expectedShape) {
78
+ const logical = resolveSourceCapabilityForCopy(doc, logicalKey);
79
+ const keys = resolveOpsKeysForCapability(doc, logical);
80
+ const operation = doc.execution.cip.operations[keys.cipKey];
81
+ if (operation?.enabled === false) {
82
+ throw new Error(`${expectedShape} capability "${logicalKey}" is disabled`);
83
+ }
84
+ if (operation?.shape && operation.shape !== expectedShape) {
85
+ throw new Error(
86
+ `${expectedShape} capability "${logicalKey}" declares shape "${operation.shape}"`
87
+ );
88
+ }
89
+ return { logical, ...keys };
90
+ }
91
+
92
+ function collectText(node, out = []) {
93
+ if (typeof node === 'string') {
94
+ out.push(node.toLowerCase());
95
+ return out;
96
+ }
97
+ if (Array.isArray(node)) {
98
+ node.forEach((item) => collectText(item, out));
99
+ return out;
100
+ }
101
+ if (node && typeof node === 'object') {
102
+ Object.entries(node).forEach(([key, value]) => {
103
+ out.push(String(key).toLowerCase());
104
+ collectText(value, out);
105
+ });
106
+ }
107
+ return out;
108
+ }
109
+
110
+ function resolveLocalSchema(schema, vendorSpec) {
111
+ const ref = schema?.$ref;
112
+ if (typeof ref !== 'string' || !ref.startsWith('#/')) return schema;
113
+ return ref.slice(2).split('/').reduce((node, token) => node?.[token], vendorSpec) || schema;
114
+ }
115
+
116
+ function collectVendorParameterNames(vendorOp, vendorSpec) {
117
+ const names = [];
118
+ const parameters = Array.isArray(vendorOp?.parameters) ? vendorOp.parameters : [];
119
+ parameters
120
+ .filter((parameter) => !parameter?.in || parameter.in === 'query')
121
+ .forEach((parameter) => names.push(parameter?.name));
122
+ const rawSchema = vendorOp?.requestBody?.content?.['application/json']?.schema;
123
+ const schema = resolveLocalSchema(rawSchema, vendorSpec);
124
+ if (schema?.properties && typeof schema.properties === 'object') {
125
+ names.push(...Object.keys(schema.properties));
126
+ }
127
+ return uniqueStrings(names).map((name) => name.toLowerCase());
128
+ }
129
+
130
+ function operationBinding(doc, operationKey) {
131
+ const cip = doc.execution?.cip?.operations?.[operationKey];
132
+ const steps = Array.isArray(cip?.steps) ? cip.steps : [];
133
+ const fetch = steps.map((step) => step?.fetch).find(Boolean);
134
+ return fetch || {};
135
+ }
136
+
137
+ function templateReferencesField(value, field) {
138
+ if (typeof value === 'string') {
139
+ const templates = [...value.matchAll(/\{\{([^}]+)\}\}/g)];
140
+ return templates.some((match) =>
141
+ match[1].toLowerCase().split(/[^a-z0-9_]+/).includes(field.toLowerCase())
142
+ );
143
+ }
144
+ if (Array.isArray(value)) {
145
+ return value.some((item) => templateReferencesField(item, field));
146
+ }
147
+ if (!value || typeof value !== 'object') return false;
148
+ return Object.values(value).some((item) => templateReferencesField(item, field));
149
+ }
150
+
151
+ function bindingCarriesField(binding, vendorOp, vendorSpec, field) {
152
+ const accepted = new Set(collectVendorParameterNames(vendorOp, vendorSpec));
153
+ const containers = [binding.query, binding.bodyTemplate, binding.parameters];
154
+ return containers.some((container) =>
155
+ container && typeof container === 'object' && !Array.isArray(container) &&
156
+ Object.entries(container).some(([key, value]) =>
157
+ accepted.has(key.toLowerCase()) && templateReferencesField(value, field)
158
+ )
159
+ );
160
+ }
161
+
162
+ function candidateCoverage(doc, opKey, opCfg, vendorOp, identityFields, vendorSpec) {
163
+ const binding = operationBinding(doc, opKey);
164
+ const text = collectText({
165
+ query: binding.query,
166
+ bodyTemplate: binding.bodyTemplate,
167
+ parameters: binding.parameters
168
+ });
169
+ const coveredFields = identityFields.filter((field) =>
170
+ bindingCarriesField(binding, vendorOp, vendorSpec, field)
171
+ );
172
+ const fuzzy = text.some((item) =>
173
+ [...FUZZY_TOKENS].some((token) => item === token || item.includes(`"${token}"`))
174
+ );
175
+ return {
176
+ operationKey: opKey,
177
+ operationId: opCfg.operationId || null,
178
+ method: String(opCfg.method || 'get').toUpperCase(),
179
+ coveredFields,
180
+ missingFields: identityFields.filter((field) => !coveredFields.includes(field)),
181
+ fuzzy
182
+ };
183
+ }
184
+
185
+ function analyzeLookupCandidates(doc, vendorSpec, identityFields, excludedKeys) {
186
+ const operations = doc.openapi?.operations || {};
187
+ return Object.entries(operations)
188
+ .filter(([key]) => !excludedKeys.has(key.toLowerCase()))
189
+ .map(([key, config]) => {
190
+ const vendorOp = findVendorOperation(vendorSpec, config);
191
+ if (!vendorOp) return null;
192
+ return candidateCoverage(doc, key, config, vendorOp, identityFields, vendorSpec);
193
+ })
194
+ .filter(Boolean);
195
+ }
196
+
197
+ function issue(code, message, ownership, missingFields = []) {
198
+ return {
199
+ code,
200
+ severity: 'blocking',
201
+ message,
202
+ observed: message,
203
+ ownership,
204
+ aiFixable: ownership === 'manifest',
205
+ missingFields,
206
+ ...(ISSUE_GUIDANCE[code] || {})
207
+ };
208
+ }
209
+
210
+ function blocked(status, context, finding) {
211
+ return { eligible: false, status, ...context, issues: [finding] };
212
+ }
213
+
214
+ function analyzeIdentity(doc, halves) {
215
+ const identityFields = declaredIdentityFields(doc);
216
+ const context = { halves, identityFields, lookupCandidates: [] };
217
+ if (identityFields.length === 0) {
218
+ return blocked('author-fixable', context, issue(
219
+ 'UPSERT-IDENTITY-MISSING',
220
+ 'No mapped business identity is declared with validation.repeatingValues strategy reject',
221
+ 'manifest'
222
+ ));
223
+ }
224
+ const unmapped = identityFields.filter((field) => !mappedFields(doc).has(field));
225
+ if (unmapped.length === 0) return context;
226
+ return blocked('author-fixable', context, issue(
227
+ 'UPSERT-IDENTITY-UNMAPPED',
228
+ `Business identity fields are not mapped: ${unmapped.join(', ')}`,
229
+ 'manifest',
230
+ unmapped
231
+ ));
232
+ }
233
+
234
+ function noExactLookup(halves, identityFields, lookupCandidates) {
235
+ const covered = new Set(lookupCandidates.flatMap((candidate) => candidate.coveredFields));
236
+ const missing = identityFields.filter((field) => !covered.has(field));
237
+ const suffix = missing.length ? `; uncovered: ${missing.join(', ')}` : '';
238
+ return blocked(
239
+ 'source-limited',
240
+ { halves, identityFields, lookupCandidates },
241
+ issue(
242
+ 'UPSERT-EXACT-LOOKUP-MISSING',
243
+ `No declared non-fuzzy read covers the full business identity${suffix}`,
244
+ 'manifest-or-source',
245
+ missing
246
+ )
247
+ );
248
+ }
249
+
250
+ function analyzeUpsertIngredients(doc, options = {}) {
251
+ const createKey = options.createKey || 'create';
252
+ const updateKey = options.updateKey || 'update';
253
+ let halves;
254
+ try {
255
+ halves = {
256
+ create: assertSourceHalf(doc, createKey, 'create'),
257
+ update: assertSourceHalf(doc, updateKey, 'update')
258
+ };
259
+ } catch (error) {
260
+ return blocked(
261
+ 'author-fixable',
262
+ { identityFields: [], lookupCandidates: [] },
263
+ issue('UPSERT-HALF-INVALID', error.message, 'manifest')
264
+ );
265
+ }
266
+ const identity = analyzeIdentity(doc, halves);
267
+ if (identity.eligible === false) return identity;
268
+ const { identityFields } = identity;
269
+ if (!options.vendorSpec) {
270
+ return blocked('indeterminate', identity, issue(
271
+ 'UPSERT-OPENAPI-UNRESOLVED',
272
+ 'The materialized vendor OpenAPI document could not be resolved',
273
+ 'local-openapi'
274
+ ));
275
+ }
276
+ const excluded = new Set([
277
+ halves.create.openapiKey.toLowerCase(), halves.update.openapiKey.toLowerCase(),
278
+ String(options.targetKey || 'upsert').toLowerCase()
279
+ ]);
280
+ const lookupCandidates = analyzeLookupCandidates(doc, options.vendorSpec, identityFields, excluded);
281
+ const exact = lookupCandidates.find((candidate) => !candidate.fuzzy && candidate.missingFields.length === 0);
282
+ if (!exact) return noExactLookup(halves, identityFields, lookupCandidates);
283
+ return { eligible: true, status: 'eligible', halves, identityFields, lookupCandidates, selectedLookup: exact, issues: [] };
284
+ }
285
+
286
+ module.exports = {
287
+ analyzeUpsertIngredients,
288
+ declaredIdentityFields,
289
+ collectVendorParameterNames,
290
+ candidateCoverage
291
+ };
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Create the canonical manifest-declared upsert slice after eligibility passes.
3
+ *
4
+ * @fileoverview Pure upsert manifest mutation
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { jsonPointerPath } = require('./json-pointer');
10
+ const { storageOpsKey } = require('./capability-storage-keys');
11
+ const {
12
+ deepClone,
13
+ removeCapability,
14
+ resolveTargetKey
15
+ } = require('./copy-operations');
16
+ const { resolveProfileKeyForLogical, capabilityLogicalExists } = require('./capability-resolve');
17
+ const { buildCapabilityObject } = require('./capability-manifest');
18
+
19
+ function profileFields(profile) {
20
+ if (Array.isArray(profile)) return profile;
21
+ if (profile && Array.isArray(profile.attributes)) return profile.attributes;
22
+ return [];
23
+ }
24
+
25
+ function buildUpsertProfile(doc, createLogical, updateLogical, identityFields) {
26
+ const createKey = resolveProfileKeyForLogical(doc, createLogical);
27
+ const updateKey = resolveProfileKeyForLogical(doc, updateLogical);
28
+ const createFields = profileFields(createKey ? doc.exposed.profiles[createKey] : null);
29
+ const updateSet = new Set(profileFields(updateKey ? doc.exposed.profiles[updateKey] : null));
30
+ const common = createFields.filter((field) => updateSet.has(field));
31
+ const fields = [...new Set([...common, ...identityFields])];
32
+ if (fields.length === 0) {
33
+ throw new Error('Create and update profiles have no shared input fields');
34
+ }
35
+ return fields;
36
+ }
37
+
38
+ function buildOpenApiOperation(createOperation, targetKey) {
39
+ const operation = deepClone(createOperation);
40
+ operation.operationId = targetKey;
41
+ operation.method = 'POST';
42
+ operation.restRuntime = {
43
+ pathKind: 'collectionSubresource',
44
+ subpathSegments: [targetKey]
45
+ };
46
+ return operation;
47
+ }
48
+
49
+ function buildCipOperation(doc, targetKey, createKey, updateKey, identityFields) {
50
+ return {
51
+ enabled: true,
52
+ description: `Create or update ${doc.displayName || doc.key || 'record'} by ${identityFields.join(', ')}`,
53
+ shape: 'upsert',
54
+ steps: [
55
+ { fetch: { source: 'datasource', datasource: doc.key, operation: createKey } },
56
+ { fetch: { source: 'datasource', datasource: doc.key, operation: updateKey } },
57
+ { output: { mode: 'records' } }
58
+ ]
59
+ };
60
+ }
61
+
62
+ function addCapability(doc, targetKey) {
63
+ if (!Array.isArray(doc.capabilities)) doc.capabilities = [];
64
+ const usesObjects = doc.capabilities.some((row) => row && typeof row === 'object');
65
+ const value = usesObjects
66
+ ? { ...buildCapabilityObject(targetKey, doc.displayName || doc.key), description: `Create or update ${doc.displayName || doc.key || 'record'}` }
67
+ : targetKey;
68
+ doc.capabilities.push(value);
69
+ return value;
70
+ }
71
+
72
+ function applyUpsertCapability(originalDoc, analysis, options = {}) {
73
+ if (!analysis?.eligible) throw new Error('Upsert ingredients are not eligible');
74
+ const doc = deepClone(originalDoc);
75
+ const targetKey = resolveTargetKey(doc, options.targetKey || 'upsert', Boolean(options.overwrite));
76
+ if (options.overwrite && capabilityLogicalExists(doc, targetKey)) removeCapability(doc, targetKey);
77
+ const storageKey = storageOpsKey(targetKey);
78
+ const createOperation = doc.openapi.operations[analysis.halves.create.openapiKey];
79
+ const openapiOperation = buildOpenApiOperation(createOperation, targetKey);
80
+ const cipOperation = buildCipOperation(
81
+ doc, targetKey, analysis.halves.create.logical, analysis.halves.update.logical,
82
+ analysis.identityFields
83
+ );
84
+ const profile = buildUpsertProfile(
85
+ doc, analysis.halves.create.logical, analysis.halves.update.logical,
86
+ analysis.identityFields
87
+ );
88
+ doc.openapi.operations[storageKey] = openapiOperation;
89
+ doc.execution.cip.operations[storageKey] = cipOperation;
90
+ if (!doc.exposed) doc.exposed = {};
91
+ if (!doc.exposed.profiles) doc.exposed.profiles = {};
92
+ doc.exposed.profiles[targetKey] = profile;
93
+ const capability = addCapability(doc, targetKey);
94
+ const patchOperations = [
95
+ { op: 'add', path: jsonPointerPath('openapi', 'operations', storageKey), value: openapiOperation },
96
+ { op: 'add', path: jsonPointerPath('execution', 'cip', 'operations', storageKey), value: cipOperation },
97
+ { op: 'add', path: jsonPointerPath('exposed', 'profiles', targetKey), value: profile },
98
+ { op: 'add', path: '/capabilities/-', value: capability }
99
+ ];
100
+ return {
101
+ doc,
102
+ resolvedAs: targetKey,
103
+ patchOperations,
104
+ updatedSections: [
105
+ `capabilities[]: ${targetKey}`,
106
+ `openapi.operations.${storageKey}`,
107
+ `execution.cip.operations.${storageKey}`,
108
+ `exposed.profiles.${targetKey}`
109
+ ]
110
+ };
111
+ }
112
+
113
+ function existingUpsertMatches(originalDoc, analysis, targetKey = 'upsert') {
114
+ if (!capabilityLogicalExists(originalDoc, targetKey)) return false;
115
+ const expected = applyUpsertCapability(originalDoc, analysis, {
116
+ targetKey,
117
+ overwrite: true
118
+ }).doc;
119
+ const storageKey = storageOpsKey(targetKey);
120
+ const actualSlices = {
121
+ openapi: originalDoc.openapi?.operations?.[storageKey],
122
+ cip: originalDoc.execution?.cip?.operations?.[storageKey],
123
+ profile: originalDoc.exposed?.profiles?.[targetKey]
124
+ };
125
+ const expectedSlices = {
126
+ openapi: expected.openapi?.operations?.[storageKey],
127
+ cip: expected.execution?.cip?.operations?.[storageKey],
128
+ profile: expected.exposed?.profiles?.[targetKey]
129
+ };
130
+ return JSON.stringify(actualSlices) === JSON.stringify(expectedSlices);
131
+ }
132
+
133
+ module.exports = {
134
+ applyUpsertCapability,
135
+ existingUpsertMatches,
136
+ buildUpsertProfile,
137
+ buildCipOperation
138
+ };