@aifabrix/builder 2.44.0 → 2.44.1

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 (66) hide show
  1. package/.cursor/rules/cli-layout.mdc +75 -0
  2. package/.cursor/rules/project-rules.mdc +8 -0
  3. package/.npmrc.token +1 -0
  4. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
  5. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
  6. package/.nyc_output/processinfo/index.json +1 -0
  7. package/jest.projects.js +15 -2
  8. package/lib/api/certificates.api.js +62 -0
  9. package/lib/api/index.js +11 -2
  10. package/lib/api/types/certificates.types.js +48 -0
  11. package/lib/api/validation-run.api.js +16 -4
  12. package/lib/api/validation-runner.js +13 -3
  13. package/lib/app/certification-show-enrich.js +129 -0
  14. package/lib/app/certification-verify-rows.js +60 -0
  15. package/lib/app/show-display.js +43 -0
  16. package/lib/app/show.js +92 -8
  17. package/lib/certification/cli-cert-sync-skip.js +21 -0
  18. package/lib/certification/merge-certification-from-artifact.js +185 -0
  19. package/lib/certification/post-unified-cert-sync.js +33 -0
  20. package/lib/certification/sync-after-external-command.js +52 -0
  21. package/lib/certification/sync-system-certification.js +197 -0
  22. package/lib/cli/setup-app.js +4 -0
  23. package/lib/cli/setup-app.test-commands.js +24 -8
  24. package/lib/cli/setup-external-system.js +22 -1
  25. package/lib/cli/setup-secrets.js +34 -13
  26. package/lib/cli/setup-utility.js +18 -2
  27. package/lib/commands/app.js +10 -1
  28. package/lib/commands/datasource-unified-test-cli.js +50 -117
  29. package/lib/commands/datasource-unified-test-cli.options.js +44 -2
  30. package/lib/commands/datasource-unified-test-e2e-cli-helpers.js +106 -0
  31. package/lib/commands/datasource-validation-cli.js +15 -1
  32. package/lib/commands/datasource.js +25 -2
  33. package/lib/commands/upload.js +17 -6
  34. package/lib/datasource/log-viewer.js +105 -14
  35. package/lib/datasource/test-e2e.js +35 -17
  36. package/lib/datasource/unified-validation-run-body.js +3 -0
  37. package/lib/datasource/unified-validation-run.js +2 -1
  38. package/lib/external-system/deploy.js +53 -18
  39. package/lib/infrastructure/compose.js +12 -3
  40. package/lib/infrastructure/helpers-docker-check.js +67 -0
  41. package/lib/infrastructure/helpers.js +47 -58
  42. package/lib/infrastructure/index.js +3 -1
  43. package/lib/infrastructure/services.js +4 -56
  44. package/lib/schema/external-system.schema.json +25 -3
  45. package/lib/schema/type/document-storage.json +15 -2
  46. package/lib/utils/api.js +28 -3
  47. package/lib/utils/configuration-env-resolver.js +11 -8
  48. package/lib/utils/credential-secrets-env.js +5 -5
  49. package/lib/utils/datasource-test-run-certificate-tty.js +82 -0
  50. package/lib/utils/datasource-test-run-display.js +19 -2
  51. package/lib/utils/datasource-test-run-exit.js +25 -0
  52. package/lib/utils/external-system-display.js +8 -0
  53. package/lib/utils/external-system-system-test-tty-overview.js +120 -0
  54. package/lib/utils/external-system-system-test-tty.js +417 -0
  55. package/lib/utils/paths.js +14 -0
  56. package/lib/utils/validation-run-poll.js +28 -5
  57. package/lib/utils/validation-run-post-retry.js +20 -8
  58. package/lib/utils/validation-run-request.js +18 -0
  59. package/lib/validation/validate-external-cert-sync.js +23 -0
  60. package/lib/validation/validate.js +4 -1
  61. package/package.json +4 -3
  62. package/scripts/install-local.js +4 -1
  63. package/scripts/pnpm-global-remove.js +48 -0
  64. package/templates/applications/dataplane/env.template +4 -0
  65. package/templates/infra/compose.yaml.hbs +15 -14
  66. package/templates/infra/servers.json.hbs +3 -1
@@ -6,6 +6,7 @@
6
6
 
7
7
  const chalk = require('chalk');
8
8
  const { sectionTitle, headerKeyValue, colorAggregateGlyph, successGlyph, failureGlyph } = require('./cli-test-layout-chalk');
9
+ const { appendCertificateTTY } = require('./datasource-test-run-certificate-tty');
9
10
 
10
11
  const SEP = '────────────────────────────────';
11
12
 
@@ -235,6 +236,16 @@ function appendDebugPayloadRefLines(lines, dbg, maxRefChars) {
235
236
  */
236
237
  function appendDebugMetaLines(lines, dbg, maxRefChars) {
237
238
  let added = false;
239
+ function formatSecondsText(s) {
240
+ const txt = String(s);
241
+ // UI-only: reduce noisy float seconds to 3 decimals, e.g. "1.1713749s" -> "1.171s".
242
+ // Applies only to number tokens immediately followed by "s".
243
+ return txt.replace(/(\d+\.\d+)(?=s\b)/g, m => {
244
+ const n = Number(m);
245
+ if (!Number.isFinite(n)) return m;
246
+ return n.toFixed(3);
247
+ });
248
+ }
238
249
  if (dbg.mode) {
239
250
  lines.push(
240
251
  `${chalk.blue.bold('debug.mode:')} ${chalk.white(String(dbg.mode))}`
@@ -244,7 +255,7 @@ function appendDebugMetaLines(lines, dbg, maxRefChars) {
244
255
  if (dbg.executionSummary) {
245
256
  lines.push(
246
257
  `${chalk.blue.bold('debug.executionSummary:')} ${chalk.white(
247
- truncateRefLine(String(dbg.executionSummary), maxRefChars)
258
+ truncateRefLine(formatSecondsText(dbg.executionSummary), maxRefChars)
248
259
  )}`
249
260
  );
250
261
  added = true;
@@ -326,9 +337,14 @@ function appendIntegrationStepLines(lines, envelope) {
326
337
 
327
338
  function pickExecutiveVerdictLine(envelope) {
328
339
  const dev = envelope.developer;
340
+ const cert = envelope.certificate;
341
+ if (envelope.runType === 'e2e' && cert && typeof cert === 'object') {
342
+ const cs = cert.summary;
343
+ if (cs && String(cs).trim()) return String(cs).trim();
344
+ }
329
345
  return (
330
346
  (dev && dev.executiveSummary) ||
331
- (envelope.certificate && envelope.certificate.summary) ||
347
+ (cert && cert.summary) ||
332
348
  (envelope.validation && envelope.validation.summary) ||
333
349
  (envelope.integration && envelope.integration.summary) ||
334
350
  `Run finished with status ${envelope.status}.`
@@ -416,6 +432,7 @@ function formatDatasourceTestRunTTY(envelope, options = {}) {
416
432
  lines.push('');
417
433
  lines.push(sectionTitle('Verdict:'));
418
434
  lines.push(chalk.white(pickExecutiveVerdictLine(envelope)));
435
+ appendCertificateTTY(lines, envelope);
419
436
  lines.push('');
420
437
  lines.push(chalk.gray(SEP));
421
438
  if (appendReferenceLayoutLines(lines, envelope, { maxRefChars: 160 })) {
@@ -52,7 +52,32 @@ function exitCodeForPollTimeout(lastBody) {
52
52
  return 3;
53
53
  }
54
54
 
55
+ const {
56
+ deriveSystemStatus,
57
+ systemCertStatus
58
+ } = require('./external-system-system-test-tty');
59
+
60
+ /**
61
+ * System-level exit code from per-datasource result rows (same matrix as §3.1 on synthetic rollup).
62
+ * @param {Array<{ skipped?: boolean, success?: boolean, datasourceTestRun?: Object|null }>} rows
63
+ * @param {Object} [opts]
64
+ * @param {boolean} [opts.warningsAsErrors]
65
+ * @param {boolean} [opts.requireCert]
66
+ * @returns {number}
67
+ */
68
+ function computeSystemExitCodeFromDatasourceRows(rows, opts = {}) {
69
+ const list = Array.isArray(rows) ? rows : [];
70
+ const status = deriveSystemStatus(list);
71
+ const certAgg = systemCertStatus(list);
72
+ const body = {
73
+ status,
74
+ certificate: certAgg ? { status: certAgg === 'passed' ? 'passed' : 'not_passed' } : undefined
75
+ };
76
+ return computeExitCodeFromDatasourceTestRun(body, opts);
77
+ }
78
+
55
79
  module.exports = {
56
80
  computeExitCodeFromDatasourceTestRun,
81
+ computeSystemExitCodeFromDatasourceRows,
57
82
  exitCodeForPollTimeout
58
83
  };
@@ -24,6 +24,7 @@ const {
24
24
  successGlyph,
25
25
  failureGlyph
26
26
  } = require('./cli-test-layout-chalk');
27
+ const { displaySystemAggregateDatasourceTestRuns } = require('./external-system-system-test-tty');
27
28
 
28
29
  /**
29
30
  * Displays formatted test results (local external `aifabrix test` — structured report layout).
@@ -269,6 +270,13 @@ function displayServerDatasourceTestRunResults(results, verbose, opts = {}) {
269
270
  return;
270
271
  }
271
272
 
273
+ // Plan §17: system-level overview for multi-datasource results (no full §16 dump per datasource by default).
274
+ // Keep legacy per-datasource full envelope available only via datasource commands.
275
+ if (integrationResultsHaveEnvelope(results)) {
276
+ displaySystemAggregateDatasourceTestRuns(results, { runType, verbose: Boolean(verbose) });
277
+ return;
278
+ }
279
+
272
280
  const agg = deriveAggregateServerStatus(results);
273
281
  logServerDatasourceTestRunHeader(results, runType, agg);
274
282
 
@@ -0,0 +1,120 @@
1
+ /**
2
+ * @fileoverview Plan §17 blocks: capabilities overview and integration health (split for file-size limits).
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ /**
8
+ * @param {Object|null|undefined} env
9
+ * @returns {'ok'|'warn'|'fail'}
10
+ */
11
+ function integrationRollupStatus(env) {
12
+ const integ = env && env.integration;
13
+ if (!integ || typeof integ !== 'object') return 'ok';
14
+ if (typeof integ.status === 'string') {
15
+ const s = integ.status.toLowerCase();
16
+ if (s === 'fail' || s === 'failed' || s === 'error') return 'fail';
17
+ if (s === 'warn' || s === 'warning') return 'warn';
18
+ if (s === 'ok' || s === 'passed' || s === 'success') return 'ok';
19
+ }
20
+ const steps = Array.isArray(integ.stepResults) ? integ.stepResults : [];
21
+ if (steps.some(s => s && (s.success === false || s.error))) return 'fail';
22
+ return 'ok';
23
+ }
24
+
25
+ function buildCapabilityBlocks(rows, statusGlyph) {
26
+ const blocks = [];
27
+ for (const r of rows) {
28
+ if (r && r.skipped) continue;
29
+ const env = r && r.datasourceTestRun;
30
+ if (!env || !Array.isArray(env.capabilities) || env.capabilities.length === 0) continue;
31
+ const dkey = env.datasourceKey || r.key || 'datasource';
32
+ const caps = [...env.capabilities]
33
+ .filter(c => c && c.key)
34
+ .sort((a, b) => String(a.key).localeCompare(String(b.key)))
35
+ .slice(0, 4);
36
+ const parts = caps.map(c => `${statusGlyph(c.status)} ${c.key}`);
37
+ if (parts.length === 0) continue;
38
+ blocks.push({ dkey: String(dkey), line: parts.join(' ') });
39
+ }
40
+ return blocks;
41
+ }
42
+
43
+ function emitSectionHeader(io, title) {
44
+ const { log, metaGray, sectionTitle, SEP } = io;
45
+ log('');
46
+ log(metaGray(SEP));
47
+ log('');
48
+ log(sectionTitle(title));
49
+ log('');
50
+ }
51
+
52
+ /**
53
+ * @param {Array} rows
54
+ * @param {{ log: Function, chalk: object, metaGray: Function, sectionTitle: Function, statusGlyph: Function, SEP: string }} io
55
+ * @returns {boolean}
56
+ */
57
+ function logCapabilitiesOverview(rows, io) {
58
+ const blocks = buildCapabilityBlocks(rows, io.statusGlyph);
59
+ if (blocks.length === 0) return false;
60
+ emitSectionHeader(io, 'Capabilities overview:');
61
+ for (const b of blocks) {
62
+ io.log(io.chalk.white(`${b.dkey}:`));
63
+ io.log(io.chalk.white(` ${b.line}`));
64
+ }
65
+ return true;
66
+ }
67
+
68
+ function badIntegrationSteps(integ) {
69
+ if (!integ || !Array.isArray(integ.stepResults)) return [];
70
+ return integ.stepResults.filter(s => s && (s.success === false || s.error)).slice(0, 2);
71
+ }
72
+
73
+ function buildIntegrationBlocks(rows) {
74
+ const blocks = [];
75
+ for (const r of rows) {
76
+ if (r && r.skipped) continue;
77
+ const env = r && r.datasourceTestRun;
78
+ if (!env) continue;
79
+ const dkey = env.datasourceKey || r.key || 'datasource';
80
+ const st = integrationRollupStatus(env);
81
+ const integ = env.integration && typeof env.integration === 'object' ? env.integration : null;
82
+ blocks.push({ dkey: String(dkey), st, badSteps: badIntegrationSteps(integ) });
83
+ }
84
+ return blocks;
85
+ }
86
+
87
+ function emitFailedSteps(block, io) {
88
+ if (block.st !== 'fail' && block.st !== 'warn') return;
89
+ const { log, chalk, statusGlyph } = io;
90
+ for (const stp of block.badSteps) {
91
+ const nm = stp.name || stp.step || 'step';
92
+ const hint = stp.error || stp.message || '';
93
+ log(chalk.white(` ${statusGlyph('fail')} ${nm}${hint ? `: ${hint}` : ''}`));
94
+ }
95
+ }
96
+
97
+ /**
98
+ * @param {Array} rows
99
+ * @param {'integration'|'e2e'} runType
100
+ * @param {{ log: Function, chalk: object, metaGray: Function, sectionTitle: Function, statusGlyph: Function, SEP: string }} io
101
+ * @returns {boolean}
102
+ */
103
+ function logIntegrationHealthSection(rows, runType, io) {
104
+ if (runType !== 'integration') return false;
105
+ const blocks = buildIntegrationBlocks(rows);
106
+ if (blocks.length === 0) return false;
107
+ emitSectionHeader(io, 'Integration health:');
108
+ const { log, chalk, statusGlyph } = io;
109
+ for (const b of blocks) {
110
+ log(chalk.white(`${b.dkey}: ${statusGlyph(b.st)} ${b.st}`));
111
+ emitFailedSteps(b, io);
112
+ }
113
+ return true;
114
+ }
115
+
116
+ module.exports = {
117
+ integrationRollupStatus,
118
+ logCapabilitiesOverview,
119
+ logIntegrationHealthSection
120
+ };
@@ -0,0 +1,417 @@
1
+ /**
2
+ * @fileoverview System-level TTY renderer for DatasourceTestRun fan-out results (plan §17).
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const chalk = require('chalk');
8
+ const logger = require('./logger');
9
+ const {
10
+ SEP,
11
+ statusGlyph
12
+ } = require('./datasource-test-run-display');
13
+ const {
14
+ sectionTitle,
15
+ headerKeyValue,
16
+ formatStatusKeyValue,
17
+ integrationFooterLine,
18
+ colorRollupPrefixedLine,
19
+ metadata: metaGray
20
+ } = require('./cli-test-layout-chalk');
21
+ const {
22
+ formatDataQualityLines,
23
+ readinessLineFromDataReadiness,
24
+ verdictLineFromEnvelope
25
+ } = require('./validation-report-tty-kit');
26
+ const {
27
+ logCapabilitiesOverview: logCapabilitiesOverviewSection,
28
+ logIntegrationHealthSection: logIntegrationHealthSectionBlock
29
+ } = require('./external-system-system-test-tty-overview');
30
+
31
+ /**
32
+ * @param {'ok'|'warn'|'fail'|'skipped'|null} st
33
+ * @returns {number}
34
+ */
35
+ function statusRank(st) {
36
+ if (st === 'fail') return 0;
37
+ if (st === 'warn') return 1;
38
+ if (st === 'ok') return 2;
39
+ if (st === 'skipped') return 3;
40
+ return 4;
41
+ }
42
+
43
+ /**
44
+ * Per-row status for system rollup and tables: CLI/transport failure overrides envelope-only OK.
45
+ * @param {{ skipped?: boolean, success?: boolean, datasourceTestRun?: { status?: string }|null }} r
46
+ * @returns {'ok'|'warn'|'fail'|'skipped'}
47
+ */
48
+ function rollupRowStatus(r) {
49
+ if (r && r.skipped) return 'skipped';
50
+ if (r && r.success === false) return 'fail';
51
+ const env = r && r.datasourceTestRun;
52
+ return env && typeof env.status === 'string' ? env.status : 'ok';
53
+ }
54
+
55
+ /**
56
+ * @param {Array<{ key: string, skipped?: boolean, datasourceTestRun?: Object|null, success?: boolean }>} rows
57
+ * @returns {'ok'|'warn'|'fail'|'skipped'}
58
+ */
59
+ function deriveSystemStatus(rows) {
60
+ if (!Array.isArray(rows) || rows.length === 0) return 'ok';
61
+ const statuses = rows.map(rollupRowStatus);
62
+ if (statuses.some(s => s === 'fail')) return 'fail';
63
+ if (statuses.some(s => s === 'warn')) return 'warn';
64
+ if (statuses.every(s => s === 'skipped')) return 'skipped';
65
+ // Mixed ok/skipped => warn per plan.
66
+ return statuses.every(s => s === 'ok') ? 'ok' : 'warn';
67
+ }
68
+
69
+ function bucketIssueSeverity(issue) {
70
+ const sev = issue && issue.severity ? String(issue.severity).toLowerCase() : '';
71
+ if (sev === 'error' || sev === 'critical' || sev === 'high' || sev === 'fatal') return 'fail';
72
+ if (sev === 'warn' || sev === 'warning' || sev === 'medium') return 'warn';
73
+ return null;
74
+ }
75
+
76
+ /**
77
+ * Minimal heuristic rollups backed by envelope fields (no engine codenames).
78
+ * @param {Object|null|undefined} env
79
+ * @returns {'ok'|'warn'|'fail'}
80
+ */
81
+ function dqFromEnvelope(env) {
82
+ const v = env && env.validation;
83
+ const st = v && typeof v.status === 'string' ? String(v.status) : null;
84
+ if (st === 'fail') return 'fail';
85
+ if (st === 'warn') return 'warn';
86
+
87
+ const issues = v && Array.isArray(v.issues) ? v.issues : [];
88
+ if (issues.some(i => bucketIssueSeverity(i) === 'fail')) return 'fail';
89
+ if (issues.some(i => bucketIssueSeverity(i) === 'warn')) return 'warn';
90
+ return 'ok';
91
+ }
92
+
93
+ /**
94
+ * @param {Array} rows
95
+ * @returns {{ schema: 'ok'|'warn'|'fail', consistency: 'ok'|'warn'|'fail', reliability: 'ok'|'warn'|'fail' }}
96
+ */
97
+ function deriveSystemDataQuality(rows) {
98
+ const envs = rows
99
+ .map(r => (r && r.datasourceTestRun && typeof r.datasourceTestRun === 'object' ? r.datasourceTestRun : null))
100
+ .filter(Boolean);
101
+ if (envs.length === 0) {
102
+ return { schema: 'warn', consistency: 'warn', reliability: 'warn' };
103
+ }
104
+
105
+ const picks = envs.map(dqFromEnvelope);
106
+ const agg = picks.some(x => x === 'fail') ? 'fail' : picks.some(x => x === 'warn') ? 'warn' : 'ok';
107
+ return { schema: agg, consistency: agg, reliability: agg };
108
+ }
109
+
110
+ /**
111
+ * @param {Array} rows
112
+ * @returns {'ready'|'partial'|'not_ready'|null}
113
+ */
114
+ function deriveSystemReadiness(rows) {
115
+ const envs = rows
116
+ .map(r => (r && r.datasourceTestRun && typeof r.datasourceTestRun === 'object' ? r.datasourceTestRun : null))
117
+ .filter(Boolean);
118
+ const drs = envs
119
+ .map(e => e.validation && e.validation.dataReadiness)
120
+ .filter(Boolean);
121
+ if (drs.length === 0) return null;
122
+ if (drs.some(x => x === 'not_ready')) return 'not_ready';
123
+ if (drs.some(x => x === 'partial')) return 'partial';
124
+ return 'ready';
125
+ }
126
+
127
+ function countByStatus(rows) {
128
+ const counts = { ok: 0, warn: 0, fail: 0, skipped: 0 };
129
+ for (const r of rows) {
130
+ const s = rollupRowStatus(r);
131
+ if (counts[s] !== undefined) counts[s] += 1;
132
+ }
133
+ return counts;
134
+ }
135
+
136
+ function pickBlockingDatasourceKey(rows) {
137
+ const keys = rows
138
+ .map(r => {
139
+ const env = r && r.datasourceTestRun;
140
+ const st = rollupRowStatus(r);
141
+ const key =
142
+ env && env.datasourceKey
143
+ ? String(env.datasourceKey)
144
+ : r && r.key
145
+ ? String(r.key)
146
+ : '';
147
+ return { key, st };
148
+ })
149
+ .filter(x => x.key);
150
+ keys.sort((a, b) => {
151
+ const d = statusRank(a.st) - statusRank(b.st);
152
+ if (d !== 0) return d;
153
+ return a.key.localeCompare(b.key);
154
+ });
155
+ return keys.length ? keys[0].key : null;
156
+ }
157
+
158
+ function issueKey(issue) {
159
+ const code = issue && issue.code ? String(issue.code) : '';
160
+ const msg = issue && issue.message ? String(issue.message) : '';
161
+ return `${code}::${msg}`.toLowerCase();
162
+ }
163
+
164
+ function issueSortKey(it) {
165
+ return `${statusRank(it.severity)}::${it.datasourceKey}::${it.message}`.toLowerCase();
166
+ }
167
+
168
+ function extractRowIssues(row) {
169
+ const env = row && row.datasourceTestRun;
170
+ const datasourceKey = (env && env.datasourceKey) || (row && row.key) || 'datasource';
171
+ const issues = env && env.validation && Array.isArray(env.validation.issues) ? env.validation.issues : [];
172
+ const envStatus = env && typeof env.status === 'string' ? env.status : null;
173
+ return { datasourceKey: String(datasourceKey), issues, envStatus };
174
+ }
175
+
176
+ function toIssueItem(datasourceKey, envStatus, iss) {
177
+ const sev =
178
+ bucketIssueSeverity(iss) ||
179
+ (envStatus === 'fail' ? 'fail' : envStatus === 'warn' ? 'warn' : 'ok');
180
+ const msg = iss && iss.message ? String(iss.message) : iss && iss.code ? String(iss.code) : 'Issue';
181
+ return { datasourceKey, message: msg, severity: sev };
182
+ }
183
+
184
+ function collectKeyIssues(rows, cap) {
185
+ /** @type {{ datasourceKey: string, message: string, severity: 'fail'|'warn'|'ok' }[]} */
186
+ const out = [];
187
+ const seen = new Set();
188
+ for (const row of rows) {
189
+ const { datasourceKey, issues, envStatus } = extractRowIssues(row);
190
+ for (const iss of issues) {
191
+ const k = `${datasourceKey}::${issueKey(iss)}`;
192
+ if (seen.has(k)) continue;
193
+ seen.add(k);
194
+ out.push(toIssueItem(datasourceKey, envStatus, iss));
195
+ }
196
+ }
197
+ out.sort((a, b) => issueSortKey(a).localeCompare(issueSortKey(b)));
198
+ return out.slice(0, Math.max(0, cap));
199
+ }
200
+
201
+ function certificateBucket(env) {
202
+ const cert = env && env.certificate;
203
+ if (!cert || typeof cert !== 'object') return { status: null, level: null };
204
+ return {
205
+ status: cert.status ? String(cert.status) : null,
206
+ level: cert.level ? String(cert.level) : null
207
+ };
208
+ }
209
+
210
+ function systemCertStatus(rows) {
211
+ const certs = rows
212
+ .map(r => (r && r.datasourceTestRun ? certificateBucket(r.datasourceTestRun) : { status: null, level: null }))
213
+ .filter(c => c.status !== null);
214
+ if (certs.length === 0) return null;
215
+ if (certs.some(c => c.status === 'not_passed')) return 'not_passed';
216
+ return 'passed';
217
+ }
218
+
219
+ function drillDownCommand(runType, datasourceKey) {
220
+ if (!datasourceKey) return null;
221
+ if (runType === 'e2e') return `aifabrix datasource test-e2e ${datasourceKey}`;
222
+ if (runType === 'integration') return `aifabrix datasource test-integration ${datasourceKey}`;
223
+ return `aifabrix datasource test ${datasourceKey}`;
224
+ }
225
+
226
+ function logSystemHeader(results, runType, systemStatus) {
227
+ logger.log('');
228
+ logger.log(sectionTitle('Server test results'));
229
+ logger.log('');
230
+ logger.log(headerKeyValue('System:', results.systemKey));
231
+ logger.log(
232
+ headerKeyValue(
233
+ 'Run:',
234
+ runType === 'e2e' ? 'test-e2e (dataplane)' : 'test-integration (dataplane)'
235
+ )
236
+ );
237
+ logger.log(formatStatusKeyValue(systemStatus, statusGlyph(systemStatus)));
238
+ logger.log('');
239
+ }
240
+
241
+ function logVerdictAndSummary(runType, systemStatus, certStatus, counts) {
242
+ logger.log(sectionTitle('Verdict:'));
243
+ logger.log(chalk.white(verdictLineFromEnvelope(systemStatus, certStatus, runType)));
244
+ logger.log('');
245
+ logger.log(sectionTitle('Summary:'));
246
+ logger.log(
247
+ chalk.white(
248
+ `${counts.ok + counts.warn + counts.fail} datasource(s): ${counts.ok} ok, ${counts.warn} warn, ${counts.fail} fail${counts.skipped ? `, ${counts.skipped} skipped` : ''}`
249
+ )
250
+ );
251
+ logger.log('');
252
+ logger.log(metaGray(SEP));
253
+ logger.log('');
254
+ }
255
+
256
+ function logDataQualityAndReadiness(rows) {
257
+ logger.log(sectionTitle('Data Quality:'));
258
+ const dq = deriveSystemDataQuality(rows);
259
+ const dqLines = formatDataQualityLines(dq, {
260
+ schema: 'structural coverage aggregated across datasources.',
261
+ consistency: 'issues aggregated across datasources.',
262
+ reliability: 'issues aggregated across datasources.'
263
+ });
264
+ dqLines.map(colorRollupPrefixedLine).forEach(l => logger.log(l));
265
+
266
+ const readiness = deriveSystemReadiness(rows);
267
+ const readinessLine = readinessLineFromDataReadiness(readiness);
268
+ if (readinessLine) {
269
+ logger.log('');
270
+ logger.log(colorRollupPrefixedLine(readinessLine));
271
+ }
272
+
273
+ logger.log('');
274
+ logger.log(metaGray(SEP));
275
+ logger.log('');
276
+ }
277
+
278
+ function logDatasourceTable(rows, counts, verbose) {
279
+ logger.log(sectionTitle('Datasources:'));
280
+ logger.log('');
281
+ if (!verbose && counts.ok > 0) {
282
+ logger.log(chalk.gray(`✔ ${counts.ok} datasource(s) fully ready`));
283
+ }
284
+
285
+ function rowStatus(r) {
286
+ return rollupRowStatus(r);
287
+ }
288
+
289
+ function readinessLabel(env, st) {
290
+ const ready = env && env.validation ? env.validation.dataReadiness : null;
291
+ if (ready === 'not_ready') return 'Not ready';
292
+ if (ready === 'partial') return 'Partial';
293
+ if (ready === 'ready') return 'Ready';
294
+ if (st === 'fail') return 'Not ready';
295
+ if (st === 'warn') return 'Partial';
296
+ return 'Ready';
297
+ }
298
+
299
+ function shouldListRow(r) {
300
+ if (verbose) return true;
301
+ const st = rowStatus(r);
302
+ return st === 'warn' || st === 'fail';
303
+ }
304
+
305
+ const listRows = rows.filter(shouldListRow);
306
+ for (const r of listRows) {
307
+ const env = r && r.datasourceTestRun;
308
+ const key = (env && env.datasourceKey) || (r && r.key) || 'datasource';
309
+ const st = rowStatus(r);
310
+ const readyLabel = readinessLabel(env, st);
311
+ logger.log(`${statusGlyph(st)} ${chalk.white(String(key).padEnd(22))} (${readyLabel})`);
312
+ }
313
+ }
314
+
315
+ function logBlockingDatasource(blocking) {
316
+ if (!blocking) return;
317
+ logger.log('');
318
+ logger.log(chalk.white(`Blocking datasource: ${blocking}`));
319
+ }
320
+
321
+ function logKeyIssuesSection(rows) {
322
+ const issues = collectKeyIssues(rows, 5);
323
+ if (issues.length === 0) return false;
324
+ logger.log('');
325
+ logger.log(metaGray(SEP));
326
+ logger.log('');
327
+ logger.log(sectionTitle('Key issues:'));
328
+ logger.log('');
329
+ let cur = null;
330
+ for (const it of issues) {
331
+ if (cur !== it.datasourceKey) {
332
+ cur = it.datasourceKey;
333
+ logger.log(chalk.white(cur));
334
+ }
335
+ logger.log(chalk.white(`- ${it.message}`));
336
+ }
337
+ return true;
338
+ }
339
+
340
+ function logCertificationSection(rows) {
341
+ const certSt = systemCertStatus(rows);
342
+ if (!certSt) return;
343
+ logger.log('');
344
+ logger.log(metaGray(SEP));
345
+ logger.log('');
346
+ logger.log(sectionTitle('Certification:'));
347
+ logger.log('');
348
+ logger.log(chalk.white(`System level: ${certSt === 'passed' ? '✔ Achieved' : '✖ Not achieved'}`));
349
+ logger.log('');
350
+ logger.log(chalk.white('Breakdown:'));
351
+ for (const r of rows) {
352
+ const env = r && r.datasourceTestRun;
353
+ if (!env) continue;
354
+ const cert = certificateBucket(env);
355
+ if (!cert.status) continue;
356
+ const g = cert.status === 'passed' ? '✔' : '✖';
357
+ const tier = cert.level ? cert.level : '(no level)';
358
+ logger.log(chalk.white(`- ${env.datasourceKey}: ${g} ${tier}`));
359
+ }
360
+ }
361
+
362
+ function logUseAndFooter(results, runType, systemStatus, blocking) {
363
+ logger.log('');
364
+ logger.log(metaGray(SEP));
365
+ logger.log('');
366
+ logger.log(sectionTitle('Use:'));
367
+ const cmd = drillDownCommand(runType, blocking);
368
+ logger.log(chalk.white(cmd || 'aifabrix datasource test <datasourceKey>'));
369
+ logger.log(
370
+ integrationFooterLine(
371
+ results.success,
372
+ systemStatus,
373
+ 'All server tests passed.',
374
+ 'Server tests completed with warnings.',
375
+ 'Some server tests failed.'
376
+ )
377
+ );
378
+ }
379
+
380
+ /**
381
+ * Render system-level aggregate (plan §17) for DatasourceTestRun wrapper results.
382
+ * @param {Object} results
383
+ * @param {Object} opts
384
+ * @param {'integration'|'e2e'} opts.runType
385
+ * @param {boolean} opts.verbose
386
+ */
387
+ function displaySystemAggregateDatasourceTestRuns(results, opts) {
388
+ const rows = Array.isArray(results.datasourceResults) ? results.datasourceResults : [];
389
+ const runType = opts.runType === 'e2e' ? 'e2e' : 'integration';
390
+ const systemStatus = deriveSystemStatus(rows);
391
+ const counts = countByStatus(rows);
392
+ const blocking = pickBlockingDatasourceKey(rows);
393
+ logSystemHeader(results, runType, systemStatus);
394
+ logVerdictAndSummary(runType, systemStatus, systemCertStatus(rows), counts);
395
+ logDataQualityAndReadiness(rows);
396
+ logDatasourceTable(rows, counts, Boolean(opts.verbose));
397
+ logBlockingDatasource(blocking);
398
+ logKeyIssuesSection(rows);
399
+ const ttyIo = { log: logger.log.bind(logger), chalk, metaGray, sectionTitle, statusGlyph, SEP };
400
+ logCapabilitiesOverviewSection(rows, ttyIo);
401
+ logIntegrationHealthSectionBlock(rows, runType, ttyIo);
402
+ logCertificationSection(rows);
403
+ logUseAndFooter(results, runType, systemStatus, blocking);
404
+ }
405
+
406
+ module.exports = {
407
+ displaySystemAggregateDatasourceTestRuns,
408
+ deriveSystemStatus,
409
+ deriveSystemDataQuality,
410
+ deriveSystemReadiness,
411
+ rollupRowStatus,
412
+ pickBlockingDatasourceKey,
413
+ collectKeyIssues,
414
+ systemCertStatus,
415
+ drillDownCommand
416
+ };
417
+
@@ -332,9 +332,23 @@ function getAppPath(appName, appType) {
332
332
 
333
333
  /**
334
334
  * Base directory for integration/builder: project root when cwd is inside project, else cwd.
335
+ * When `aifabrix-work` / `AIFABRIX_WORK` points at a repo that contains `integration/`, use that
336
+ * root even if cwd is elsewhere (e.g. global CLI install + cwd under `integration/<app>/`).
335
337
  * @returns {string} Directory to resolve integration/ and builder/ from
336
338
  */
337
339
  function getIntegrationBuilderBaseDir() {
340
+ const work = getAifabrixWork();
341
+ if (work) {
342
+ const workNorm = path.resolve(work);
343
+ const integrationUnderWork = path.join(workNorm, 'integration');
344
+ try {
345
+ if (fs.existsSync(integrationUnderWork)) {
346
+ return workNorm;
347
+ }
348
+ } catch {
349
+ // ignore fs errors
350
+ }
351
+ }
338
352
  const root = getProjectRoot();
339
353
  const cwd = path.resolve(process.cwd());
340
354
  const rootNorm = path.resolve(root);