@aifabrix/builder 2.50.0 → 2.51.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 (103) hide show
  1. package/.cursor/plans/157.1-security-quality-violations-by-type.csv +374 -0
  2. package/.cursor/rules/assistant-driven-integration-build.mdc +40 -0
  3. package/integration/hubspot-test/create-hubspot.js +1 -16
  4. package/lib/api/evidence-kinds.api.js +128 -0
  5. package/lib/api/pipeline.api.js +5 -1
  6. package/lib/api/resource-types.api.js +4 -4
  7. package/lib/api/roles.api.js +121 -0
  8. package/lib/api/types/evidence-kinds.types.js +42 -0
  9. package/lib/api/types/roles.types.js +36 -0
  10. package/lib/app/certification-verify-rows.js +1 -1
  11. package/lib/app/show.js +2 -2
  12. package/lib/certification/merge-certification-from-artifact.js +1 -1
  13. package/lib/channels/channel-artifact-generator.js +2 -1
  14. package/lib/cli/index.js +4 -0
  15. package/lib/cli/setup-app.js +22 -5
  16. package/lib/cli/setup-dev.js +23 -7
  17. package/lib/cli/setup-utility-help-after.js +12 -0
  18. package/lib/cli/setup-utility-validate.js +41 -2
  19. package/lib/commands/deploy-gate-failure-details.js +21 -0
  20. package/lib/commands/deploy-gate-orchestrator.js +64 -18
  21. package/lib/commands/dev-cli-handlers.js +6 -3
  22. package/lib/commands/dev-init.js +7 -109
  23. package/lib/commands/dev-refresh.js +197 -0
  24. package/lib/commands/evidence-kind.js +428 -0
  25. package/lib/commands/governance-command-action.js +4 -2
  26. package/lib/commands/repair-openapi-export.js +1 -1
  27. package/lib/commands/repair-openapi-sync.js +14 -6
  28. package/lib/commands/resource-type.js +101 -18
  29. package/lib/commands/role.js +452 -0
  30. package/lib/commands/setup.js +1 -1
  31. package/lib/commands/validate-json.js +230 -0
  32. package/lib/commands/verify-operations-step-plan.js +78 -40
  33. package/lib/core/audit-logger.js +1 -1
  34. package/lib/core/secrets-load.js +1 -3
  35. package/lib/datasource/capability/relate-operations.js +1 -1
  36. package/lib/generator/helpers.js +13 -5
  37. package/lib/generator/split-variables.js +9 -3
  38. package/lib/infrastructure/helpers-docker-check.js +6 -0
  39. package/lib/infrastructure/helpers.js +1 -1
  40. package/lib/programmatic/auth-config.js +3 -2
  41. package/lib/programmatic/bearer-auth.js +1 -1
  42. package/lib/programmatic/enterprise-mcp-client.js +18 -2
  43. package/lib/programmatic/manifest-section-registry.js +18 -1
  44. package/lib/programmatic/openapi-result-schemas.js +28 -11
  45. package/lib/programmatic/openapi-schema-descriptions-core.js +5 -2
  46. package/lib/programmatic/openapi-schema-helpers.js +16 -1
  47. package/lib/programmatic/openapi-spec.js +1 -1
  48. package/lib/programmatic/run-repository-emcp.js +11 -3
  49. package/lib/programmatic/run-template-workspace.js +14 -1
  50. package/lib/resolvers/evidence-kind-file.js +230 -0
  51. package/lib/resolvers/resource-type-file.js +36 -1
  52. package/lib/resolvers/role-file.js +227 -0
  53. package/lib/schema/application-schema.json +22 -2
  54. package/lib/schema/external-datasource.schema.json +85 -2
  55. package/lib/schema/external-system.schema.json +16 -2
  56. package/lib/schema/infra.parameter.yaml +25 -1
  57. package/lib/schema/type/credential-field-registry.json +19 -3
  58. package/lib/utils/api.js +8 -2
  59. package/lib/utils/app-register-config.js +4 -3
  60. package/lib/utils/cli-sync-options.js +59 -3
  61. package/lib/utils/cli-utils.js +11 -3
  62. package/lib/utils/dev-ca-install.js +5 -6
  63. package/lib/utils/dev-config.js +2 -2
  64. package/lib/utils/dev-list-sort.js +145 -0
  65. package/lib/utils/dev-platform-tls.js +0 -63
  66. package/lib/utils/docker-build.js +46 -27
  67. package/lib/utils/docker-daemon-tls-ca.js +1 -1
  68. package/lib/utils/docker-exec.js +91 -6
  69. package/lib/utils/env-copy.js +1 -3
  70. package/lib/utils/error-formatters/error-parser.js +0 -3
  71. package/lib/utils/external-env-template.js +1 -1
  72. package/lib/utils/external-system-readiness-compact-blocks.js +28 -1
  73. package/lib/utils/health-check-request.js +2 -3
  74. package/lib/utils/help-builder.js +3 -0
  75. package/lib/utils/image-name.js +22 -4
  76. package/lib/utils/logger.js +37 -4
  77. package/lib/utils/manifest-source-emit.js +1 -1
  78. package/lib/utils/paths.js +32 -4
  79. package/lib/utils/pipeline-upload-auth-retry.js +4 -0
  80. package/lib/utils/postgres-platform-bootstrap.js +24 -17
  81. package/lib/utils/postgres-wipe.js +5 -2
  82. package/lib/utils/remote-dev-auth.js +39 -4
  83. package/lib/utils/resolve-docker-image-ref.js +36 -6
  84. package/lib/utils/token-manager.js +16 -0
  85. package/lib/utils/upload-sync-options.js +17 -3
  86. package/lib/validation/openapi-contract-surface-validator.js +4 -0
  87. package/lib/validation/validator.js +6 -5
  88. package/package.json +34 -12
  89. package/scripts/export-builder-api-evidence.js +2 -2
  90. package/scripts/lib/plan-actor.js +2 -2
  91. package/scripts/lib/plan-meta.js +1 -1
  92. package/scripts/lib/plan-parse.js +1 -1
  93. package/scripts/lib/plan-prevalidate-gates.js +12 -1
  94. package/scripts/lib/plan-run-report.js +13 -2
  95. package/scripts/pnpm/lib/resolve-aifabrix-bin.mjs +10 -10
  96. package/templates/applications/dataplane/application.yaml +26 -4
  97. package/templates/applications/dataplane/env.template +28 -9
  98. package/templates/applications/dataplane/rbac.yaml +64 -2
  99. package/templates/applications/miso-controller/application.yaml +10 -1
  100. package/templates/applications/miso-controller/env.template +10 -7
  101. package/templates/applications/miso-controller/rbac.yaml +72 -0
  102. package/templates/channels/slack/integration/slack-system.json +60 -2
  103. package/templates/python/requirements.txt +1 -1
@@ -41,7 +41,15 @@ function stringArraySchema() {
41
41
  * @returns {Object}
42
42
  */
43
43
  function buildSuccessEnvelopeSchema(dataSchemaName) {
44
- return frozenObject(
44
+ const dataExamples = {
45
+ HealthData: { status: 'ok' },
46
+ ReadyData: { status: 'ready', dataplaneUrl: 'http://localhost:3601' },
47
+ ListSystemsData: { systems: [] },
48
+ EmptyResultData: {},
49
+ WorkspaceStatusData: { status: 'ready' },
50
+ RepositoryStatusData: { status: 'ready' }
51
+ };
52
+ const schema = frozenObject(
45
53
  {
46
54
  success: { type: 'boolean', enum: [true] },
47
55
  data: schemaRef(dataSchemaName),
@@ -50,6 +58,13 @@ function buildSuccessEnvelopeSchema(dataSchemaName) {
50
58
  },
51
59
  ['success', 'data', 'correlationId']
52
60
  );
61
+ schema.example = {
62
+ success: true,
63
+ data: dataExamples[dataSchemaName] || {},
64
+ correlationId: '00000000-0000-4000-8000-000000000001',
65
+ requestId: '00000000-0000-4000-8000-000000000002'
66
+ };
67
+ return schema;
53
68
  }
54
69
 
55
70
  /**
@@ -47,7 +47,7 @@ function problemResponseContent(description) {
47
47
  description,
48
48
  content: {
49
49
  'application/problem+json': {
50
- schema: { $ref: '#/components/schemas/ProblemDetails' }
50
+ schema: { $ref: '#/components/schemas/ProblemDetail' }
51
51
  }
52
52
  }
53
53
  };
@@ -9,8 +9,8 @@
9
9
  'use strict';
10
10
 
11
11
  const { runEnterpriseMcpCapability } = require('./run-enterprise-mcp');
12
- const { invokeEnterpriseMcpCapability } = require('./enterprise-mcp-client');
13
12
  const { runWithWorkspaceContext } = require('./workspace-context');
13
+ const { getSystemManifest } = require('./workspace-ops');
14
14
 
15
15
  /**
16
16
  * @param {import('./workspace-context').WorkspaceContext} ctx
@@ -33,8 +33,16 @@ async function runRepositoryCreateReview(ctx, body = {}) {
33
33
  */
34
34
  async function getRepositoryStatus(ctx) {
35
35
  return runWithWorkspaceContext(ctx, async() => {
36
- const result = await invokeEnterpriseMcpCapability(ctx, 'repository.status', {});
37
- return { capabilityKey: 'repository.status', result };
36
+ // Local-first status: COM currently composes enterprise-repository from
37
+ // builder-api-repository, so EMCP → this route deadlocks.
38
+ // Mutating repository.* still go through EMCP (sync/review/dispatch).
39
+ const local = getSystemManifest(ctx);
40
+ return {
41
+ capabilityKey: 'repository.status',
42
+ source: 'local',
43
+ local,
44
+ synced: Boolean(local && local.systemKey)
45
+ };
38
46
  });
39
47
  }
40
48
 
@@ -18,7 +18,20 @@ const { runWithWorkspaceContext } = require('./workspace-context');
18
18
  * @param {Object} body
19
19
  */
20
20
  async function materializeTemplate(ctx, body = {}) {
21
- return runEnterpriseMcpCapability(ctx, 'template.materializeWorkspace', body);
21
+ return runWithWorkspaceContext(ctx, async() => {
22
+ // Local-first: COM currently maps template.materializeWorkspace → template.runwizard
23
+ // which re-enters builder-api wizard and can 409 (workspace lock) under E2E.
24
+ const local = getSystemManifest(ctx);
25
+ if (local && local.systemKey) {
26
+ return {
27
+ capabilityKey: 'template.materializeWorkspace',
28
+ source: 'local',
29
+ materialized: true,
30
+ local
31
+ };
32
+ }
33
+ return runEnterpriseMcpCapability(ctx, 'template.materializeWorkspace', body);
34
+ });
22
35
  }
23
36
 
24
37
  /**
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Evidence kind catalog file helpers for create/import/export.
3
+ *
4
+ * File JSON uses `domain`; Controller API uses `domainKey`.
5
+ *
6
+ * @fileoverview Evidence kind catalog file parsing and API mapping
7
+ * @author AI Fabrix Team
8
+ * @version 1.0.0
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const path = require('path');
14
+ const { existsSync, readFileSync, writeFileSync } = require('../internal/fs-real-sync');
15
+
16
+ const DOMAIN_KEY_PATTERN = /^[a-z][a-z0-9-]*$/;
17
+ const SEVERITIES = new Set(['low', 'medium', 'high', 'critical']);
18
+
19
+ /**
20
+ * @param {string} filePath
21
+ * @returns {object|Array}
22
+ */
23
+ function loadEvidenceKindJson(filePath) {
24
+ const p = path.resolve(String(filePath || '').trim());
25
+ if (!p) {
26
+ throw new Error('--file is required');
27
+ }
28
+ if (!existsSync(p)) {
29
+ throw new Error(`File not found: ${p}`);
30
+ }
31
+ let parsed;
32
+ try {
33
+ parsed = JSON.parse(readFileSync(p, 'utf8'));
34
+ } catch (e) {
35
+ throw new Error(`Invalid JSON in ${p}: ${e.message}`);
36
+ }
37
+ if (parsed === null || typeof parsed !== 'object') {
38
+ throw new Error(`Evidence kind file must be a JSON object or array: ${p}`);
39
+ }
40
+ return parsed;
41
+ }
42
+
43
+ /**
44
+ * @param {object} entry
45
+ * @param {number} index
46
+ * @returns {string}
47
+ */
48
+ function requireDomainValue(entry, index) {
49
+ const raw = entry.domain !== undefined ? entry.domain : entry.domainKey;
50
+ const value = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
51
+ if (!value) {
52
+ throw new Error(`evidenceKinds[${index}].domain is required`);
53
+ }
54
+ if (!DOMAIN_KEY_PATTERN.test(value)) {
55
+ throw new Error(
56
+ 'domain must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens'
57
+ );
58
+ }
59
+ return value;
60
+ }
61
+
62
+ /**
63
+ * Map baseline/file evidence kind row to Controller create payload (domain → domainKey).
64
+ * @param {object} row
65
+ * @param {number} [index]
66
+ * @returns {Object}
67
+ */
68
+ function mapEvidenceKindFileToApiPayload(row, index = 0) {
69
+ if (!row || typeof row !== 'object') {
70
+ throw new Error(`evidenceKinds[${index}] must be a JSON object`);
71
+ }
72
+ const key = String(row.key || '').trim();
73
+ if (!key) {
74
+ throw new Error(`evidenceKinds[${index}].key is required`);
75
+ }
76
+ const displayName = String(row.displayName || '').trim();
77
+ if (!displayName) {
78
+ throw new Error(`evidenceKinds[${index}].displayName is required`);
79
+ }
80
+ const purpose = String(row.purpose || '').trim();
81
+ if (!purpose) {
82
+ throw new Error(`evidenceKinds[${index}].purpose is required`);
83
+ }
84
+ const domainKey = requireDomainValue(row, index);
85
+ const payload = { key, displayName, purpose, domainKey };
86
+ copyOptionalFields(payload, row, [
87
+ 'description',
88
+ 'expectedUse',
89
+ 'requiresApproval',
90
+ 'isActive',
91
+ 'sortOrder',
92
+ 'source'
93
+ ]);
94
+ assignOptionalSeverity(payload, row, index);
95
+ if (row.metadata && typeof row.metadata === 'object') {
96
+ payload.metadata = row.metadata;
97
+ }
98
+ return payload;
99
+ }
100
+
101
+ /**
102
+ * @param {Object} payload
103
+ * @param {object} row
104
+ * @param {number} index
105
+ * @returns {void}
106
+ */
107
+ function assignOptionalSeverity(payload, row, index) {
108
+ if (row.defaultSeverity === undefined) {
109
+ return;
110
+ }
111
+ const severity = String(row.defaultSeverity).trim();
112
+ if (!SEVERITIES.has(severity)) {
113
+ throw new Error(
114
+ `evidenceKinds[${index}].defaultSeverity must be one of: ${[...SEVERITIES].join(', ')}`
115
+ );
116
+ }
117
+ payload.defaultSeverity = severity;
118
+ }
119
+
120
+ /**
121
+ * @param {Object} target
122
+ * @param {object} source
123
+ * @param {string[]} fields
124
+ * @returns {void}
125
+ */
126
+ function copyOptionalFields(target, source, fields) {
127
+ for (const field of fields) {
128
+ if (source[field] !== undefined) {
129
+ target[field] = source[field];
130
+ }
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Map Controller evidence kind record to baseline-shaped JSON (domainKey → domain).
136
+ * @param {object} record
137
+ * @returns {Object}
138
+ */
139
+ function mapEvidenceKindApiToFileRow(record) {
140
+ const row = {
141
+ key: record.key,
142
+ displayName: record.displayName,
143
+ domain: record.domainKey || record.domain,
144
+ purpose: record.purpose
145
+ };
146
+ if (record.description) row.description = record.description;
147
+ if (record.expectedUse) row.expectedUse = record.expectedUse;
148
+ if (record.defaultSeverity) row.defaultSeverity = record.defaultSeverity;
149
+ if (record.requiresApproval !== undefined) row.requiresApproval = record.requiresApproval;
150
+ if (record.sortOrder !== undefined) row.sortOrder = record.sortOrder;
151
+ if (record.metadata && typeof record.metadata === 'object') {
152
+ row.metadata = record.metadata;
153
+ }
154
+ return row;
155
+ }
156
+
157
+ /**
158
+ * @param {object} parsed
159
+ * @returns {void}
160
+ */
161
+ function rejectBaselineCatalogShape(parsed) {
162
+ if (
163
+ parsed &&
164
+ typeof parsed === 'object' &&
165
+ Array.isArray(parsed.evidenceKinds) &&
166
+ !parsed.key
167
+ ) {
168
+ throw new Error(
169
+ 'File is a baseline catalog (evidenceKinds[]). ' +
170
+ 'Use `aifabrix evidence-kind import --file <path>` to seed all entries, ' +
171
+ 'or pass a single evidence kind object with key and displayName.'
172
+ );
173
+ }
174
+ }
175
+
176
+ /**
177
+ * @param {string} filePath
178
+ * @returns {Object}
179
+ */
180
+ function readEvidenceKindCreateFile(filePath) {
181
+ const parsed = loadEvidenceKindJson(filePath);
182
+ rejectBaselineCatalogShape(parsed);
183
+ return mapEvidenceKindFileToApiPayload(parsed, 0);
184
+ }
185
+
186
+ /**
187
+ * @param {string} filePath
188
+ * @returns {Object[]}
189
+ */
190
+ function readEvidenceKindCatalogFile(filePath) {
191
+ const parsed = loadEvidenceKindJson(filePath);
192
+ let rows;
193
+ if (Array.isArray(parsed)) {
194
+ rows = parsed;
195
+ } else if (Array.isArray(parsed.evidenceKinds)) {
196
+ rows = parsed.evidenceKinds;
197
+ } else {
198
+ throw new Error(
199
+ 'Import file must be a baseline catalog with evidenceKinds[] or a JSON array of evidence kinds.'
200
+ );
201
+ }
202
+ if (rows.length === 0) {
203
+ throw new Error('Import file contains no evidence kinds');
204
+ }
205
+ return rows.map((row, index) => mapEvidenceKindFileToApiPayload(row, index));
206
+ }
207
+
208
+ /**
209
+ * @param {string} filePath
210
+ * @param {Object[]} rows
211
+ * @param {string} [version]
212
+ * @returns {void}
213
+ */
214
+ function writeEvidenceKindCatalogFile(filePath, rows, version = '1.0.0') {
215
+ const p = path.resolve(String(filePath || '').trim());
216
+ if (!p) {
217
+ throw new Error('--file is required');
218
+ }
219
+ const catalog = { version, evidenceKinds: rows };
220
+ writeFileSync(p, `${JSON.stringify(catalog, null, 2)}\n`, 'utf8');
221
+ }
222
+
223
+ module.exports = {
224
+ mapEvidenceKindFileToApiPayload,
225
+ mapEvidenceKindApiToFileRow,
226
+ readEvidenceKindCreateFile,
227
+ readEvidenceKindCatalogFile,
228
+ writeEvidenceKindCatalogFile,
229
+ SEVERITIES
230
+ };
@@ -9,7 +9,7 @@
9
9
  'use strict';
10
10
 
11
11
  const path = require('path');
12
- const { existsSync, readFileSync } = require('../internal/fs-real-sync');
12
+ const { existsSync, readFileSync, writeFileSync } = require('../internal/fs-real-sync');
13
13
 
14
14
  /**
15
15
  * @typedef {Object} ResourceTypeCreateInput
@@ -172,9 +172,44 @@ function readResourceTypeCatalogFile(filePath) {
172
172
  return rows.map((row, index) => normalizeResourceTypeCreateRow(row, index));
173
173
  }
174
174
 
175
+ /**
176
+ * Map Controller resource type record to baseline-shaped JSON.
177
+ * @param {object} record
178
+ * @returns {Object}
179
+ */
180
+ function mapResourceTypeApiToFileRow(record) {
181
+ const row = {
182
+ key: record.key,
183
+ displayName: record.displayName,
184
+ domain: record.domain
185
+ };
186
+ if (record.description) row.description = record.description;
187
+ if (record.category) row.category = record.category;
188
+ if (record.defaultRiskLevel) row.defaultRiskLevel = record.defaultRiskLevel;
189
+ if (record.status) row.status = record.status;
190
+ return row;
191
+ }
192
+
193
+ /**
194
+ * @param {string} filePath
195
+ * @param {Object[]} rows - baseline-shaped resource type rows
196
+ * @param {string} [version]
197
+ * @returns {void}
198
+ */
199
+ function writeResourceTypeCatalogFile(filePath, rows, version = '1.0.0') {
200
+ const p = path.resolve(String(filePath || '').trim());
201
+ if (!p) {
202
+ throw new Error('--file is required');
203
+ }
204
+ const catalog = { version, resourceTypes: rows };
205
+ writeFileSync(p, `${JSON.stringify(catalog, null, 2)}\n`, 'utf8');
206
+ }
207
+
175
208
  module.exports = {
176
209
  readResourceTypeCreateFile,
177
210
  readResourceTypeCatalogFile,
211
+ mapResourceTypeApiToFileRow,
212
+ writeResourceTypeCatalogFile,
178
213
  CATEGORIES,
179
214
  RISK_LEVELS,
180
215
  STATUSES
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Role catalog file helpers for create/import/export.
3
+ *
4
+ * File JSON uses `domain`; Controller API uses `domainKey`.
5
+ * Compiler fields nest under `metadata` on the API payload.
6
+ *
7
+ * @fileoverview Role catalog file parsing and API mapping
8
+ * @author AI Fabrix Team
9
+ * @version 1.0.0
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const path = require('path');
15
+ const { existsSync, readFileSync, writeFileSync } = require('../internal/fs-real-sync');
16
+
17
+ /** Compiler fields that live top-level in baseline JSON and under metadata in the API. */
18
+ const ROLE_COMPILER_FIELDS = [
19
+ 'defaultResourceType',
20
+ 'primaryResourceTypes',
21
+ 'defaultCapability',
22
+ 'defaultCapabilities',
23
+ 'defaultKnowledgeProfiles',
24
+ 'defaultEvidenceKinds',
25
+ 'defaultKnowledgeKeys',
26
+ 'defaultKnowledgeGenerationProfiles'
27
+ ];
28
+
29
+ const DOMAIN_KEY_PATTERN = /^[a-z][a-z0-9-]*$/;
30
+
31
+ /**
32
+ * @param {string} filePath
33
+ * @returns {object|Array}
34
+ */
35
+ function loadRoleJson(filePath) {
36
+ const p = path.resolve(String(filePath || '').trim());
37
+ if (!p) {
38
+ throw new Error('--file is required');
39
+ }
40
+ if (!existsSync(p)) {
41
+ throw new Error(`File not found: ${p}`);
42
+ }
43
+ let parsed;
44
+ try {
45
+ parsed = JSON.parse(readFileSync(p, 'utf8'));
46
+ } catch (e) {
47
+ throw new Error(`Invalid JSON in ${p}: ${e.message}`);
48
+ }
49
+ if (parsed === null || typeof parsed !== 'object') {
50
+ throw new Error(`Role file must be a JSON object or array: ${p}`);
51
+ }
52
+ return parsed;
53
+ }
54
+
55
+ /**
56
+ * @param {object} entry
57
+ * @param {number} index
58
+ * @returns {string}
59
+ */
60
+ function requireDomainValue(entry, index) {
61
+ const raw = entry.domain !== undefined ? entry.domain : entry.domainKey;
62
+ const value = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
63
+ if (!value) {
64
+ throw new Error(`roles[${index}].domain is required`);
65
+ }
66
+ if (!DOMAIN_KEY_PATTERN.test(value)) {
67
+ throw new Error(
68
+ 'domain must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens'
69
+ );
70
+ }
71
+ return value;
72
+ }
73
+
74
+ /**
75
+ * Map baseline/file role row to Controller create payload (domain → domainKey; compiler → metadata).
76
+ * @param {object} row
77
+ * @param {number} [index]
78
+ * @returns {Object}
79
+ */
80
+ function mapRoleFileToApiPayload(row, index = 0) {
81
+ if (!row || typeof row !== 'object') {
82
+ throw new Error(`roles[${index}] must be a JSON object`);
83
+ }
84
+ const key = String(row.key || '').trim();
85
+ if (!key) {
86
+ throw new Error(`roles[${index}].key is required`);
87
+ }
88
+ const displayName = String(row.displayName || '').trim();
89
+ if (!displayName) {
90
+ throw new Error(`roles[${index}].displayName is required`);
91
+ }
92
+ const domainKey = requireDomainValue(row, index);
93
+ const metadata = collectRoleCompilerMetadata(row);
94
+ const payload = { key, displayName, domainKey };
95
+ copyOptionalFields(payload, row, ['description', 'purpose', 'businessOutcome', 'isActive', 'source']);
96
+ if (Object.keys(metadata).length > 0) {
97
+ payload.metadata = metadata;
98
+ }
99
+ return payload;
100
+ }
101
+
102
+ /**
103
+ * @param {object} row
104
+ * @returns {Object}
105
+ */
106
+ function collectRoleCompilerMetadata(row) {
107
+ const metadata = {
108
+ ...(row.metadata && typeof row.metadata === 'object' ? row.metadata : {})
109
+ };
110
+ for (const field of ROLE_COMPILER_FIELDS) {
111
+ if (row[field] !== undefined) {
112
+ metadata[field] = row[field];
113
+ }
114
+ }
115
+ return metadata;
116
+ }
117
+
118
+ /**
119
+ * @param {Object} target
120
+ * @param {object} source
121
+ * @param {string[]} fields
122
+ * @returns {void}
123
+ */
124
+ function copyOptionalFields(target, source, fields) {
125
+ for (const field of fields) {
126
+ if (source[field] !== undefined) {
127
+ target[field] = source[field];
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Map Controller role record to baseline-shaped JSON (domainKey → domain; flatten compiler metadata).
134
+ * @param {object} record
135
+ * @returns {Object}
136
+ */
137
+ function mapRoleApiToFileRow(record) {
138
+ const row = {
139
+ key: record.key,
140
+ displayName: record.displayName,
141
+ domain: record.domainKey || record.domain
142
+ };
143
+ if (record.description) row.description = record.description;
144
+ if (record.purpose) row.purpose = record.purpose;
145
+ if (record.businessOutcome) row.businessOutcome = record.businessOutcome;
146
+ const meta = record.metadata && typeof record.metadata === 'object' ? { ...record.metadata } : {};
147
+ for (const field of ROLE_COMPILER_FIELDS) {
148
+ if (meta[field] !== undefined) {
149
+ row[field] = meta[field];
150
+ delete meta[field];
151
+ }
152
+ }
153
+ if (Object.keys(meta).length > 0) {
154
+ row.metadata = meta;
155
+ }
156
+ return row;
157
+ }
158
+
159
+ /**
160
+ * @param {object} parsed
161
+ * @returns {void}
162
+ */
163
+ function rejectBaselineCatalogShape(parsed) {
164
+ if (parsed && typeof parsed === 'object' && Array.isArray(parsed.roles) && !parsed.key) {
165
+ throw new Error(
166
+ 'File is a baseline catalog (roles[]). ' +
167
+ 'Use `aifabrix role import --file <path>` to seed all entries, ' +
168
+ 'or pass a single role object with key and displayName.'
169
+ );
170
+ }
171
+ }
172
+
173
+ /**
174
+ * @param {string} filePath
175
+ * @returns {Object}
176
+ */
177
+ function readRoleCreateFile(filePath) {
178
+ const parsed = loadRoleJson(filePath);
179
+ rejectBaselineCatalogShape(parsed);
180
+ return mapRoleFileToApiPayload(parsed, 0);
181
+ }
182
+
183
+ /**
184
+ * @param {string} filePath
185
+ * @returns {Object[]}
186
+ */
187
+ function readRoleCatalogFile(filePath) {
188
+ const parsed = loadRoleJson(filePath);
189
+ let rows;
190
+ if (Array.isArray(parsed)) {
191
+ rows = parsed;
192
+ } else if (Array.isArray(parsed.roles)) {
193
+ rows = parsed.roles;
194
+ } else {
195
+ throw new Error(
196
+ 'Import file must be a baseline catalog with roles[] or a JSON array of roles.'
197
+ );
198
+ }
199
+ if (rows.length === 0) {
200
+ throw new Error('Import file contains no roles');
201
+ }
202
+ return rows.map((row, index) => mapRoleFileToApiPayload(row, index));
203
+ }
204
+
205
+ /**
206
+ * @param {string} filePath
207
+ * @param {Object[]} rows - baseline-shaped role rows
208
+ * @param {string} [version]
209
+ * @returns {void}
210
+ */
211
+ function writeRoleCatalogFile(filePath, rows, version = '1.0.0') {
212
+ const p = path.resolve(String(filePath || '').trim());
213
+ if (!p) {
214
+ throw new Error('--file is required');
215
+ }
216
+ const catalog = { version, roles: rows };
217
+ writeFileSync(p, `${JSON.stringify(catalog, null, 2)}\n`, 'utf8');
218
+ }
219
+
220
+ module.exports = {
221
+ ROLE_COMPILER_FIELDS,
222
+ mapRoleFileToApiPayload,
223
+ mapRoleApiToFileRow,
224
+ readRoleCreateFile,
225
+ readRoleCatalogFile,
226
+ writeRoleCatalogFile
227
+ };
@@ -7,12 +7,12 @@
7
7
  "key": "application-schema",
8
8
  "name": "Application Configuration Schema",
9
9
  "description": "JSON schema for validating AI Fabrix application configuration files",
10
- "version": "1.4.0",
10
+ "version": "1.5.0",
11
11
  "type": "schema",
12
12
  "category": "infrastructure",
13
13
  "author": "AI Fabrix Team",
14
14
  "createdAt": "2024-01-01T00:00:00Z",
15
- "updatedAt": "2026-04-06T00:00:00Z",
15
+ "updatedAt": "2026-07-21T00:00:00Z",
16
16
  "compatibility": {
17
17
  "minVersion": "1.0.0",
18
18
  "maxVersion": "2.0.0",
@@ -30,6 +30,14 @@
30
30
  "external-datasource.schema.json"
31
31
  ],
32
32
  "changelog": [
33
+ {
34
+ "version": "1.5.0",
35
+ "date": "2026-07-21T00:00:00Z",
36
+ "changes": [
37
+ "Added optional configuration[].category (and conditionalConfiguration item category) for portal UI grouping labels in Miso Configuration tab."
38
+ ],
39
+ "breaking": false
40
+ },
33
41
  {
34
42
  "version": "1.4.0",
35
43
  "date": "2026-04-06T00:00:00Z",
@@ -203,6 +211,12 @@
203
211
  "type": "boolean",
204
212
  "description": "Whether this configuration is required"
205
213
  },
214
+ "category": {
215
+ "type": "string",
216
+ "description": "Optional UI grouping label for portal/deployment wizard Configuration tab",
217
+ "minLength": 1,
218
+ "maxLength": 100
219
+ },
206
220
  "portalInput": {
207
221
  "type": "object",
208
222
  "description": "Portal input configuration for user-provided values",
@@ -315,6 +329,12 @@
315
329
  "type": "boolean",
316
330
  "description": "Whether this configuration is required"
317
331
  },
332
+ "category": {
333
+ "type": "string",
334
+ "description": "Optional UI grouping label for portal/deployment wizard Configuration tab",
335
+ "minLength": 1,
336
+ "maxLength": 100
337
+ },
318
338
  "portalInput": {
319
339
  "type": "object",
320
340
  "description": "Portal input configuration for user-provided values",