@aifabrix/builder 2.44.3 → 2.44.5

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 (72) hide show
  1. package/.npmrc.token +1 -1
  2. package/integration/roundtrip-test-local/README.md +1 -2
  3. package/integration/roundtrip-test-local2/README.md +1 -2
  4. package/jest.projects.js +31 -15
  5. package/lib/api/certificates.api.js +21 -3
  6. package/lib/api/types/wizard.types.js +2 -1
  7. package/lib/certification/post-unified-cert-sync.js +13 -2
  8. package/lib/certification/sync-after-external-command.js +6 -3
  9. package/lib/certification/sync-system-certification.js +60 -14
  10. package/lib/cli/setup-app.help.js +1 -1
  11. package/lib/cli/setup-app.test-commands.js +75 -39
  12. package/lib/cli/setup-infra.js +6 -2
  13. package/lib/cli/setup-utility.js +20 -1
  14. package/lib/commands/datasource-unified-test-cli.js +81 -46
  15. package/lib/commands/datasource-unified-test-cli.options.js +4 -2
  16. package/lib/commands/datasource.js +3 -31
  17. package/lib/commands/repair-datasource-keys.js +1 -1
  18. package/lib/commands/repair-datasource-openapi.js +57 -0
  19. package/lib/commands/repair-datasource.js +5 -0
  20. package/lib/commands/repair-internal.js +2 -4
  21. package/lib/commands/repair-rbac.js +25 -2
  22. package/lib/commands/repair.js +2 -19
  23. package/lib/commands/test-e2e-external.js +9 -9
  24. package/lib/commands/up-common.js +25 -0
  25. package/lib/commands/upload.js +18 -4
  26. package/lib/commands/wizard-core.js +53 -11
  27. package/lib/commands/wizard-dataplane.js +14 -6
  28. package/lib/commands/wizard-entity-selection.js +71 -14
  29. package/lib/commands/wizard-headless.js +5 -2
  30. package/lib/commands/wizard-helpers.js +13 -1
  31. package/lib/commands/wizard.js +208 -60
  32. package/lib/datasource/datasource-validate-display.js +162 -0
  33. package/lib/datasource/datasource-validate-summary.js +194 -0
  34. package/lib/datasource/test-e2e.js +65 -37
  35. package/lib/datasource/unified-validation-run-body.js +1 -2
  36. package/lib/datasource/validate.js +14 -6
  37. package/lib/external-system/test.js +12 -8
  38. package/lib/generator/external-controller-manifest.js +12 -2
  39. package/lib/generator/wizard-prompts.js +7 -1
  40. package/lib/generator/wizard.js +34 -0
  41. package/lib/schema/cip-capacity-display.fallback.json +7 -0
  42. package/lib/schema/datasource-test-run.schema.json +79 -1
  43. package/lib/schema/external-datasource.schema.json +94 -2
  44. package/lib/schema/flag-map-validation-run.json +1 -2
  45. package/lib/schema/type/document-storage.json +83 -3
  46. package/lib/schema/wizard-config.schema.json +1 -1
  47. package/lib/utils/configuration-env-resolver.js +38 -0
  48. package/lib/utils/dataplane-resolver.js +3 -2
  49. package/lib/utils/datasource-test-run-capacity-operations.js +149 -0
  50. package/lib/utils/datasource-test-run-debug-display.js +143 -1
  51. package/lib/utils/datasource-test-run-display.js +46 -33
  52. package/lib/utils/datasource-test-run-tty-log.js +6 -2
  53. package/lib/utils/datasource-test-run-tty-meta-lines.js +123 -0
  54. package/lib/utils/error-formatter.js +32 -2
  55. package/lib/utils/external-readme.js +47 -3
  56. package/lib/utils/external-system-readiness-core.js +39 -0
  57. package/lib/utils/external-system-readiness-deploy-display.js +2 -3
  58. package/lib/utils/external-system-readiness-display-internals.js +3 -2
  59. package/lib/utils/external-system-system-test-tty.js +33 -9
  60. package/lib/utils/external-system-validators.js +62 -5
  61. package/lib/utils/load-cip-capacity-display-config.js +130 -0
  62. package/lib/utils/paths.js +10 -3
  63. package/lib/utils/schema-resolver.js +98 -2
  64. package/lib/utils/urls-local-registry.js +52 -10
  65. package/lib/utils/validation-run-poll.js +15 -4
  66. package/lib/utils/validation-run-request.js +4 -6
  67. package/lib/validation/dimension-display-helpers.js +60 -0
  68. package/lib/validation/validate-display-log-helpers.js +39 -0
  69. package/lib/validation/validate-display.js +89 -83
  70. package/package.json +1 -1
  71. package/templates/applications/miso-controller/env.template +6 -6
  72. package/templates/external-system/README.md.hbs +58 -32
@@ -6,6 +6,12 @@
6
6
 
7
7
  const { SEP, appendReferenceLayoutLines } = require('./datasource-test-run-display');
8
8
  const { buildDebugEnvelopeSlice } = require('./datasource-test-run-debug-slice');
9
+ const {
10
+ pushCapacityOperationsSummaryLines,
11
+ parseCapacityScenarioOp,
12
+ parseCapacityDetailKey,
13
+ formatCapacityOperationLabel
14
+ } = require('./datasource-test-run-capacity-operations');
9
15
 
10
16
  const FULL_MAX_BYTES_PER_STRING = 8192;
11
17
  const RAW_MAX_STRING_TTY = 65536;
@@ -74,9 +80,140 @@ function redactDebugText(text) {
74
80
  .replace(/("authorization"\s*:\s*")[^"]*(")/g, '$1[REDACTED]$2');
75
81
  }
76
82
 
83
+ function getBestEffortStepList(envelope) {
84
+ const v = envelope && envelope.validation;
85
+ const metrics = v && v.metricsOutput;
86
+ const steps = metrics && Array.isArray(metrics.steps) ? metrics.steps : [];
87
+ if (steps.length) return steps;
88
+ const dbg = envelope && envelope.debug;
89
+ const stepDebug =
90
+ dbg && dbg.e2eAsyncDebug && Array.isArray(dbg.e2eAsyncDebug.stepDebug) ? dbg.e2eAsyncDebug.stepDebug : [];
91
+ return stepDebug;
92
+ }
93
+
94
+ function findStepByName(steps, name) {
95
+ return steps.find(s => s && String(s.name || s.step) === name);
96
+ }
97
+
98
+ function sumNumber(rows, key) {
99
+ return rows.reduce((acc, r) => acc + (Number(r && r[key]) || 0), 0);
100
+ }
101
+
102
+ function appendE2eWorkerHeadLine(lines, timing) {
103
+ const work = timing.durationSeconds;
104
+ if (work === undefined || work === null || work === '') return;
105
+ const w = Number(work);
106
+ if (Number.isNaN(w)) return;
107
+ let head = `E2E worker: ~${w.toFixed(3)}s`;
108
+ const wall = timing.wallClockSeconds;
109
+ if (wall !== undefined && wall !== null && wall !== '') {
110
+ const wl = Number(wall);
111
+ if (!Number.isNaN(wl)) {
112
+ head += ` (wall ~${wl.toFixed(3)}s)`;
113
+ }
114
+ }
115
+ lines.push(head);
116
+ }
117
+
118
+ function appendE2eStepDurationLines(lines, timing) {
119
+ const sd = timing.stepDurations;
120
+ if (!Array.isArray(sd) || !sd.length) return;
121
+ for (const row of sd) {
122
+ if (!row || typeof row !== 'object') continue;
123
+ const step = row.step !== undefined && row.step !== null ? String(row.step) : '?';
124
+ const sec =
125
+ row.durationSeconds !== undefined && row.durationSeconds !== null
126
+ ? Number(row.durationSeconds)
127
+ : NaN;
128
+ if (!Number.isNaN(sec)) {
129
+ lines.push(` ${step}: ~${sec.toFixed(3)}s`);
130
+ }
131
+ }
132
+ }
133
+
134
+ function getFirstSyncJobPhaseTimings(e2e) {
135
+ const stepDebug = Array.isArray(e2e.stepDebug) ? e2e.stepDebug : [];
136
+ const syncStep = stepDebug.find(s => s && String(s.name) === 'sync');
137
+ const jobs = syncStep && syncStep.evidence && syncStep.evidence.jobs;
138
+ const first = Array.isArray(jobs) && jobs.length ? jobs[0] : null;
139
+ const audit = first && first.audit;
140
+ const pt = audit && typeof audit === 'object' ? audit.phaseTimingsSeconds : null;
141
+ return pt && typeof pt === 'object' ? pt : null;
142
+ }
143
+
144
+ function formatPhaseTimingParts(phaseTimings) {
145
+ const order = ['phase1', 'phase2', 'phase3', 'phase4'];
146
+ const parts = [];
147
+ for (const k of order) {
148
+ const raw = phaseTimings[k];
149
+ if (raw === undefined || raw === null) continue;
150
+ const v = Number(raw);
151
+ if (!Number.isNaN(v)) {
152
+ parts.push(`${k} ~${v.toFixed(3)}s`);
153
+ }
154
+ }
155
+ return parts;
156
+ }
157
+
158
+ function appendSyncPhaseTimingsFromE2e(lines, e2e) {
159
+ const phaseTimings = getFirstSyncJobPhaseTimings(e2e);
160
+ if (!phaseTimings) return;
161
+ const parts = formatPhaseTimingParts(phaseTimings);
162
+ if (parts.length) {
163
+ lines.push(`Sync phases (first job): ${parts.join(', ')}`);
164
+ }
165
+ }
166
+
167
+ /**
168
+ * E2E poll envelope: `debug.e2eAsyncDebug` from dataplane run store (timing + stepDebug).
169
+ * @param {string[]} lines
170
+ * @param {Object} envelope
171
+ */
172
+ function pushE2eTimingSummaryLines(lines, envelope) {
173
+ const dbg = envelope && envelope.debug;
174
+ const e2e = dbg && dbg.e2eAsyncDebug;
175
+ if (!e2e || typeof e2e !== 'object') return;
176
+
177
+ const timing = e2e.timing;
178
+ if (timing && typeof timing === 'object') {
179
+ appendE2eWorkerHeadLine(lines, timing);
180
+ appendE2eStepDurationLines(lines, timing);
181
+ }
182
+ appendSyncPhaseTimingsFromE2e(lines, e2e);
183
+ }
184
+
185
+ function pushSyncSummaryLines(lines, envelope) {
186
+ const steps = getBestEffortStepList(envelope);
187
+ const syncStep = findStepByName(steps, 'sync');
188
+ const syncStatusStep = findStepByName(steps, 'sync_status');
189
+ const persistenceStep = findStepByName(steps, 'persistence');
190
+
191
+ if (syncStep && syncStep.evidence && Array.isArray(syncStep.evidence.jobs)) {
192
+ const jobs = syncStep.evidence.jobs;
193
+ const processed = sumNumber(jobs, 'recordsProcessed');
194
+ const total = sumNumber(jobs, 'totalRecords');
195
+ lines.push(`Sync: ${processed}/${total} processed`);
196
+ }
197
+ if (syncStatusStep && syncStatusStep.evidence && Array.isArray(syncStatusStep.evidence.datasources)) {
198
+ const rows = syncStatusStep.evidence.datasources;
199
+ const total = sumNumber(rows, 'totalRecords');
200
+ const active = sumNumber(rows, 'activeRecords');
201
+ lines.push(`Sync status: ${active}/${total} active`);
202
+ }
203
+ if (persistenceStep && persistenceStep.evidence && persistenceStep.evidence.recordCount !== undefined) {
204
+ lines.push(`Persistence: ${persistenceStep.evidence.recordCount} record(s)`);
205
+ }
206
+ }
207
+
77
208
  function pushSummaryRefLines(lines, envelope) {
78
209
  const before = lines.length;
79
- appendReferenceLayoutLines(lines, envelope, { maxRefChars: 600 });
210
+ try {
211
+ pushSyncSummaryLines(lines, envelope);
212
+ pushE2eTimingSummaryLines(lines, envelope);
213
+ } catch {
214
+ // ignore best-effort debug summary extraction
215
+ }
216
+ appendReferenceLayoutLines(lines, envelope, { maxRefChars: 600, includeDebugMeta: true });
80
217
  if (lines.length === before) {
81
218
  lines.push('(No audit or debug references on this report.)');
82
219
  }
@@ -130,6 +267,11 @@ module.exports = {
130
267
  resolveDebugDisplayMode,
131
268
  truncateUtf8String,
132
269
  formatDatasourceTestRunDebugBlock,
270
+ pushE2eTimingSummaryLines,
271
+ pushCapacityOperationsSummaryLines,
272
+ parseCapacityScenarioOp,
273
+ parseCapacityDetailKey,
274
+ formatCapacityOperationLabel,
133
275
  FULL_MAX_BYTES_PER_STRING,
134
276
  RAW_MAX_STRING_TTY,
135
277
  RAW_MAX_STRING_PIPE,
@@ -7,12 +7,23 @@
7
7
  const chalk = require('chalk');
8
8
  const { sectionTitle, headerKeyValue, colorAggregateGlyph, successGlyph, failureGlyph } = require('./cli-test-layout-chalk');
9
9
  const { appendCertificateTTY } = require('./datasource-test-run-certificate-tty');
10
+ const { buildTtyMetaLines: buildTtyMetaLinesCore } = require('./datasource-test-run-tty-meta-lines');
10
11
 
11
12
  const SEP = '────────────────────────────────';
12
13
 
13
14
  /** @type {number} */
14
15
  const DEFAULT_MAX_REF_CHARS = 200;
15
16
 
17
+ // UI-only: round float seconds tokens like "1.1713749s" -> "1.171s".
18
+ function formatSecondsText(text) {
19
+ const txt = String(text);
20
+ return txt.replace(/(\d+\.\d+)(?=s\b)/g, m => {
21
+ const n = Number(m);
22
+ if (!Number.isFinite(n)) return m;
23
+ return n.toFixed(3);
24
+ });
25
+ }
26
+
16
27
  /**
17
28
  * @param {'ok'|'warn'|'fail'|'skipped'} status
18
29
  * @returns {string}
@@ -144,23 +155,11 @@ function formatCapabilityFocusSection(envelope, focusKey) {
144
155
 
145
156
  /**
146
157
  * @param {Object} envelope
158
+ * @param {{ includeDebugExecutionSummary?: boolean }} [ttyOptions]
147
159
  * @returns {string[]}
148
160
  */
149
- function buildTtyMetaLines(envelope) {
150
- const lines = [];
151
- lines.push(
152
- headerKeyValue('Datasource:', `${envelope.datasourceKey} (${envelope.systemKey})`)
153
- );
154
- lines.push(headerKeyValue('Run:', String(envelope.runType)));
155
- lines.push(formatEnvelopeStatusLine(envelope));
156
- const rid = envelope.runId || envelope.testRunId;
157
- if (rid) lines.push(`${chalk.gray('Run ID:')} ${chalk.cyan(String(rid))}`);
158
- if (envelope.reportCompleteness && envelope.reportCompleteness !== 'full') {
159
- lines.push(
160
- `${chalk.gray('Report:')} ${chalk.yellow(String(envelope.reportCompleteness))}`
161
- );
162
- }
163
- return lines;
161
+ function buildTtyMetaLines(envelope, ttyOptions = {}) {
162
+ return buildTtyMetaLinesCore(envelope, formatEnvelopeStatusLine, ttyOptions);
164
163
  }
165
164
 
166
165
  /**
@@ -236,16 +235,6 @@ function appendDebugPayloadRefLines(lines, dbg, maxRefChars) {
236
235
  */
237
236
  function appendDebugMetaLines(lines, dbg, maxRefChars) {
238
237
  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
- }
249
238
  if (dbg.mode) {
250
239
  lines.push(
251
240
  `${chalk.blue.bold('debug.mode:')} ${chalk.white(String(dbg.mode))}`
@@ -272,6 +261,7 @@ function appendDebugMetaLines(lines, dbg, maxRefChars) {
272
261
  */
273
262
  function appendReferenceLayoutLines(lines, envelope, opts = {}) {
274
263
  const maxRefChars = opts.maxRefChars ?? DEFAULT_MAX_REF_CHARS;
264
+ const includeDebugMeta = opts.includeDebugMeta === true;
275
265
  const audit = envelope && envelope.audit;
276
266
  const dbg = envelope && envelope.debug;
277
267
  const parts = [];
@@ -287,7 +277,9 @@ function appendReferenceLayoutLines(lines, envelope, opts = {}) {
287
277
 
288
278
  if (dbg && typeof dbg === 'object') {
289
279
  parts.push(appendStringRefBlock(lines, 'debug.executionIds:', dbg.executionIds, maxRefChars));
290
- parts.push(appendDebugMetaLines(lines, dbg, maxRefChars));
280
+ if (includeDebugMeta) {
281
+ parts.push(appendDebugMetaLines(lines, dbg, maxRefChars));
282
+ }
291
283
  }
292
284
 
293
285
  return parts.some(Boolean);
@@ -329,6 +321,8 @@ function appendIntegrationStepLines(lines, envelope) {
329
321
  const integ = envelope && envelope.integration;
330
322
  const steps = integ && Array.isArray(integ.stepResults) ? integ.stepResults : [];
331
323
  if (!steps.length) return;
324
+ lines.push('');
325
+ lines.push(chalk.gray(SEP));
332
326
  lines.push(sectionTitle('Integration steps:'));
333
327
  for (const st of steps) {
334
328
  lines.push(formatIntegrationStepLine(st));
@@ -422,22 +416,27 @@ function formatDatasourceTestRunSummary(envelope, options = {}) {
422
416
  /**
423
417
  * Default TTY block (header + verdict line + short summary + optional completeness).
424
418
  * @param {Object} envelope
425
- * @param {{ focusCapabilityKey?: string }} [options] - When set (e.g. --capability), append single-cap block (plan §2.3).
419
+ * @param {{
420
+ * focusCapabilityKey?: string,
421
+ * includeRefs?: boolean,
422
+ * includeDebugExecutionSummary?: boolean
423
+ * }} [options] - When focusCapabilityKey set (e.g. --capability), append single-cap block (plan §2.3).
424
+ * includeDebugExecutionSummary: show dataplane `debug.executionSummary` in header (TTY debug appendix uses CLI `--debug`).
426
425
  * @returns {string}
427
426
  */
428
427
  function formatDatasourceTestRunTTY(envelope, options = {}) {
429
428
  if (!envelope || typeof envelope !== 'object') return '';
430
- const lines = [...buildTtyMetaLines(envelope)];
429
+ const lines = [
430
+ ...buildTtyMetaLines(envelope, {
431
+ includeDebugExecutionSummary: options.includeDebugExecutionSummary === true
432
+ })
433
+ ];
431
434
  appendNoCapabilitiesReportedLine(lines, envelope, options);
432
435
  lines.push('');
433
436
  lines.push(sectionTitle('Verdict:'));
434
437
  lines.push(chalk.white(pickExecutiveVerdictLine(envelope)));
435
438
  appendCertificateTTY(lines, envelope);
436
- lines.push('');
437
- lines.push(chalk.gray(SEP));
438
- if (appendReferenceLayoutLines(lines, envelope, { maxRefChars: 160 })) {
439
- lines.push('');
440
- }
439
+ appendRefsSectionIfEnabled(lines, envelope, options);
441
440
  appendValidationIssueLines(lines, envelope);
442
441
  appendIntegrationStepLines(lines, envelope);
443
442
  const focus = normalizedFocusCapabilityKey(options.focusCapabilityKey);
@@ -447,6 +446,20 @@ function formatDatasourceTestRunTTY(envelope, options = {}) {
447
446
  return lines.join('\n');
448
447
  }
449
448
 
449
+ function appendRefsSectionIfEnabled(lines, envelope, options) {
450
+ if (options.includeRefs !== true) return;
451
+ const before = lines.length;
452
+ lines.push('');
453
+ lines.push(chalk.gray(SEP));
454
+ const added = appendReferenceLayoutLines(lines, envelope, { maxRefChars: 160, includeDebugMeta: false });
455
+ if (added) {
456
+ lines.push('');
457
+ return;
458
+ }
459
+ // remove the blank + separator when no refs exist
460
+ lines.length = before;
461
+ }
462
+
450
463
  module.exports = {
451
464
  formatDatasourceTestRunSummary,
452
465
  formatDatasourceTestRunTTY,
@@ -42,7 +42,12 @@ function emitCapabilityScopeDiagnostics(envelope, opts = {}) {
42
42
  * @param {string} [options.requestedCapabilityKey]
43
43
  */
44
44
  function printDatasourceTestRunForTTY(envelope, options = {}) {
45
- const displayOpts = { focusCapabilityKey: options.requestedCapabilityKey };
45
+ const mode = resolveDebugDisplayMode(options.debug);
46
+ const displayOpts = {
47
+ focusCapabilityKey: options.requestedCapabilityKey,
48
+ includeRefs: Boolean(mode),
49
+ includeDebugExecutionSummary: Boolean(mode)
50
+ };
46
51
  if (options.json) {
47
52
  logger.log(JSON.stringify(envelope));
48
53
  return;
@@ -52,7 +57,6 @@ function printDatasourceTestRunForTTY(envelope, options = {}) {
52
57
  } else {
53
58
  logger.log(formatDatasourceTestRunTTY(envelope, displayOpts));
54
59
  }
55
- const mode = resolveDebugDisplayMode(options.debug);
56
60
  if (mode) {
57
61
  const appendix = formatDatasourceTestRunDebugBlock(envelope, mode, process.stdout.isTTY);
58
62
  if (appendix) logger.log(appendix);
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @fileoverview TTY meta header lines for DatasourceTestRun (extracted for max-lines).
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const chalk = require('chalk');
10
+ const { headerKeyValue } = require('./cli-test-layout-chalk');
11
+ const { pushCapacityOperationsSummaryLines } = require('./datasource-test-run-capacity-operations');
12
+
13
+ /** @type {number} */
14
+ const DEFAULT_MAX_REF_CHARS = 200;
15
+
16
+ // UI-only: round float seconds tokens like "1.1713749s" -> "1.171s".
17
+ function formatSecondsText(text) {
18
+ const txt = String(text);
19
+ return txt.replace(/(\d+\.\d+)(?=s\b)/g, m => {
20
+ const n = Number(m);
21
+ if (!Number.isFinite(n)) return m;
22
+ return n.toFixed(3);
23
+ });
24
+ }
25
+
26
+ /**
27
+ * @param {string} str
28
+ * @param {number} maxChars
29
+ * @returns {string}
30
+ */
31
+ function truncateRefLine(str, maxChars) {
32
+ const s = String(str);
33
+ if (s.length <= maxChars) return s;
34
+ return `${s.slice(0, Math.max(0, maxChars - 24))}… [+${s.length - maxChars + 24} chars]`;
35
+ }
36
+
37
+ /**
38
+ * @param {string[]} lines
39
+ * @param {Object} envelope
40
+ */
41
+ function pushTtyMetaRunWallLine(lines, envelope) {
42
+ if (envelope.cliWallSeconds === undefined || envelope.cliWallSeconds === null) return;
43
+ const n = Number(envelope.cliWallSeconds);
44
+ if (Number.isFinite(n) && n >= 0) {
45
+ lines.push(`${chalk.gray('Run wall:')} ${chalk.white(`~${n}s`)}`);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * @param {string[]} lines
51
+ * @param {Object} envelope
52
+ */
53
+ function pushTtyMetaCapabilitiesPreviewLine(lines, envelope) {
54
+ const caps = Array.isArray(envelope.capabilities) ? envelope.capabilities : [];
55
+ if (caps.length === 0) return;
56
+ const keys = caps
57
+ .map(c => (c && c.key !== undefined && c.key !== null ? String(c.key) : ''))
58
+ .filter(Boolean);
59
+ if (keys.length === 0) return;
60
+ const preview = keys.slice(0, 10).join(', ');
61
+ const more = keys.length > 10 ? ', …' : '';
62
+ lines.push(`${chalk.gray('Capabilities tested:')} ${chalk.white(`${preview}${more}`)}`);
63
+ }
64
+
65
+ /**
66
+ * @param {string[]} lines
67
+ * @param {Object} envelope
68
+ */
69
+ function pushTtyMetaDebugExecutionSummaryLine(lines, envelope) {
70
+ if (!envelope.debug || typeof envelope.debug !== 'object' || !envelope.debug.executionSummary) return;
71
+ lines.push(
72
+ `${chalk.blue.bold('debug.executionSummary:')} ${chalk.white(
73
+ truncateRefLine(formatSecondsText(envelope.debug.executionSummary), DEFAULT_MAX_REF_CHARS)
74
+ )}`
75
+ );
76
+ }
77
+
78
+ /**
79
+ * @param {string[]} lines
80
+ * @param {Object} envelope
81
+ */
82
+ function pushTtyMetaRunIdLine(lines, envelope) {
83
+ const rid = envelope.runId || envelope.testRunId;
84
+ if (rid) lines.push(`${chalk.gray('Run ID:')} ${chalk.cyan(String(rid))}`);
85
+ }
86
+
87
+ /**
88
+ * @param {string[]} lines
89
+ * @param {Object} envelope
90
+ */
91
+ function pushTtyMetaReportCompletenessLine(lines, envelope) {
92
+ if (!envelope.reportCompleteness || envelope.reportCompleteness === 'full') return;
93
+ lines.push(`${chalk.gray('Report:')} ${chalk.yellow(String(envelope.reportCompleteness))}`);
94
+ }
95
+
96
+ /**
97
+ * @param {Object} envelope
98
+ * @param {(e: Object) => string} formatEnvelopeStatusLine
99
+ * @param {{ includeDebugExecutionSummary?: boolean }} [options]
100
+ * @returns {string[]}
101
+ */
102
+ function buildTtyMetaLines(envelope, formatEnvelopeStatusLine, options = {}) {
103
+ const includeDebugExecutionSummary = options.includeDebugExecutionSummary === true;
104
+ const lines = [];
105
+ lines.push(
106
+ headerKeyValue('Datasource:', `${envelope.datasourceKey} (${envelope.systemKey})`)
107
+ );
108
+ lines.push(headerKeyValue('Run:', String(envelope.runType)));
109
+ pushTtyMetaRunWallLine(lines, envelope);
110
+ pushTtyMetaCapabilitiesPreviewLine(lines, envelope);
111
+ lines.push(formatEnvelopeStatusLine(envelope));
112
+ pushCapacityOperationsSummaryLines(lines, envelope);
113
+ if (includeDebugExecutionSummary) {
114
+ pushTtyMetaDebugExecutionSummaryLine(lines, envelope);
115
+ }
116
+ pushTtyMetaRunIdLine(lines, envelope);
117
+ pushTtyMetaReportCompletenessLine(lines, envelope);
118
+ return lines;
119
+ }
120
+
121
+ module.exports = {
122
+ buildTtyMetaLines
123
+ };
@@ -265,13 +265,38 @@ function formatSingleError(error, options) {
265
265
  return message || `${field}: ${error.message || 'Validation error'}`;
266
266
  }
267
267
 
268
+ /**
269
+ * Removes exact duplicate lines while preserving first-seen order (AJV composite schemas often repeat the same message).
270
+ * @param {string[]} strings
271
+ * @returns {string[]}
272
+ */
273
+ function dedupeFormattedMessagesPreserveOrder(strings) {
274
+ if (!Array.isArray(strings)) {
275
+ return strings;
276
+ }
277
+ const seen = new Set();
278
+ const out = [];
279
+ for (const s of strings) {
280
+ if (typeof s !== 'string') {
281
+ continue;
282
+ }
283
+ if (seen.has(s)) {
284
+ continue;
285
+ }
286
+ seen.add(s);
287
+ out.push(s);
288
+ }
289
+ return out;
290
+ }
291
+
268
292
  /**
269
293
  * Formats validation errors into developer-friendly messages
270
294
  * Converts technical schema errors into actionable advice
271
295
  *
272
296
  * @function formatValidationErrors
273
297
  * @param {Array} errors - Raw validation errors from Ajv
274
- * @param {Object} [options] - Optional; pass `{ deploymentManifest }` or `{ rootData }` for RBAC/pattern detail
298
+ * @param {Object} [options] - Optional; pass `{ deploymentManifest }` or `{ rootData }` for RBAC/pattern detail.
299
+ * Pass `{ dedupe: true }` to remove exact duplicate lines (AJV `allOf`/`if-then` often repeats the same message).
275
300
  * @returns {Array} Formatted error messages
276
301
  *
277
302
  * @example
@@ -283,7 +308,11 @@ function formatValidationErrors(errors, options) {
283
308
  return ['Unknown validation error'];
284
309
  }
285
310
 
286
- return errors.map((e) => formatSingleError(e, options));
311
+ const messages = errors.map(e => formatSingleError(e, options));
312
+ if (options && options.dedupe === true) {
313
+ return dedupeFormattedMessagesPreserveOrder(messages);
314
+ }
315
+ return messages;
287
316
  }
288
317
 
289
318
  /**
@@ -308,6 +337,7 @@ function formatMissingDbPasswordError(appKey, opts = {}) {
308
337
  module.exports = {
309
338
  formatSingleError,
310
339
  formatValidationErrors,
340
+ dedupeFormattedMessagesPreserveOrder,
311
341
  formatMissingDbPasswordError,
312
342
  getPatternDescription,
313
343
  getValueAtInstancePath,
@@ -137,11 +137,43 @@ function rbacOptionalFilename(normalizedExt) {
137
137
  return normalizedExt === '.yaml' || normalizedExt === '.yml' ? 'rbac.yaml' : 'rbac.json';
138
138
  }
139
139
 
140
+ /**
141
+ * Word-wraps plain description text for Markdown (MD013 ~80 columns). Keeps blank
142
+ * lines between paragraphs.
143
+ * @param {string} text - Raw description
144
+ * @param {number} [maxLen=80] - Target max line length
145
+ * @returns {string}
146
+ */
147
+ function wrapPlainTextForMarkdown(text, maxLen = 80) {
148
+ if (!text || typeof text !== 'string') return text;
149
+ const blocks = text.split(/\n\s*\n/);
150
+ const wrapped = blocks.map((block) => {
151
+ const flat = block.replace(/\s+/g, ' ').trim();
152
+ if (!flat) return '';
153
+ const words = flat.split(' ');
154
+ const lines = [];
155
+ let current = '';
156
+ for (const w of words) {
157
+ const candidate = current ? `${current} ${w}` : w;
158
+ if (candidate.length <= maxLen) {
159
+ current = candidate;
160
+ } else {
161
+ if (current) lines.push(current);
162
+ current = w;
163
+ }
164
+ }
165
+ if (current) lines.push(current);
166
+ return lines.join('\n');
167
+ });
168
+ return wrapped.filter(Boolean).join('\n\n');
169
+ }
170
+
140
171
  function buildExternalReadmeContext(params = {}) {
141
172
  const appName = params.appName || params.systemKey || 'external-system';
142
173
  const systemKey = params.systemKey || appName;
143
174
  const displayName = params.displayName || formatDisplayName(systemKey);
144
- const description = params.description || `External system integration for ${systemKey}`;
175
+ const rawDescription = params.description || `External system integration for ${systemKey}`;
176
+ const description = wrapPlainTextForMarkdown(rawDescription);
145
177
  const systemType = params.systemType || 'openapi';
146
178
  const fileExt = params.fileExt !== undefined ? params.fileExt : '.json';
147
179
  const normalizedExt = fileExt && fileExt.startsWith('.') ? fileExt : `.${fileExt || 'json'}`;
@@ -186,13 +218,25 @@ function loadExternalReadmeTemplate() {
186
218
  * @param {Object} params - Context parameters
187
219
  * @returns {string} README content
188
220
  */
221
+ /**
222
+ * Collapses 3+ consecutive newlines to 2 (fixes MD012 from Handlebars spacing).
223
+ * @param {string} md - Markdown body
224
+ * @returns {string}
225
+ */
226
+ function collapseConsecutiveBlankLines(md) {
227
+ if (!md || typeof md !== 'string') return md;
228
+ return md.replace(/\n{3,}/g, '\n\n');
229
+ }
230
+
189
231
  function generateExternalReadmeContent(params = {}) {
190
232
  const template = loadExternalReadmeTemplate();
191
233
  const context = buildExternalReadmeContext(params);
192
- return template(context);
234
+ return collapseConsecutiveBlankLines(template(context));
193
235
  }
194
236
 
195
237
  module.exports = {
196
238
  buildExternalReadmeContext,
197
- generateExternalReadmeContent
239
+ generateExternalReadmeContent,
240
+ wrapPlainTextForMarkdown,
241
+ collapseConsecutiveBlankLines
198
242
  };
@@ -214,6 +214,40 @@ function datasourceTestRunToLegacyRow(run) {
214
214
  };
215
215
  }
216
216
 
217
+ /**
218
+ * Map dataplane **datasourceSummaries** item into legacy probe row shape.
219
+ * @param {Object} summary
220
+ * @returns {Object}
221
+ */
222
+ function datasourceSummaryToLegacyRow(summary) {
223
+ const key = summary.datasourceKey || summary.datasource_key || 'unknown';
224
+ const root = String(summary.status || '').toLowerCase();
225
+ const vStat = String(summary.validationStatus || '').toLowerCase();
226
+ const issues = Array.isArray(summary.issues) ? summary.issues : [];
227
+ const { errors, warnings } = partitionDatasourceTestRunIssues(issues);
228
+ if (vStat === 'warn' && warnings.length === 0) {
229
+ warnings.push('validation warnings');
230
+ }
231
+ const isValid = vStat !== 'fail' && errors.length === 0;
232
+ const cert = String(summary.certificateStatus || '').toLowerCase();
233
+ if (cert === 'not_passed' && isValid) {
234
+ warnings.push('certification not passed');
235
+ }
236
+ return {
237
+ sourceKey: key,
238
+ key,
239
+ success: root !== 'fail',
240
+ skipped: root === 'skipped',
241
+ validationResults: { isValid, errors, warnings },
242
+ endpointTestResults: {
243
+ success: true,
244
+ endpointReachable: true,
245
+ message: null,
246
+ warning: false
247
+ }
248
+ };
249
+ }
250
+
217
251
  /**
218
252
  * Normalize runtime probe body for Tier B display.
219
253
  *
@@ -225,6 +259,7 @@ function datasourceTestRunToLegacyRow(run) {
225
259
  * - **Runtime probe (--probe)** uses validation run and returns either:
226
260
  * - legacy `{ results: ExternalDataSourceTestResponse[] }`, or
227
261
  * - canonical **DatasourceTestRun** envelope (single success shape) which we coerce into a legacy row
262
+ * - optional **datasourceSummaries** on the envelope for multi-datasource system runs
228
263
  *
229
264
  * This helper intentionally accepts multiple shapes so the CLI output stays truthful across dataplane versions.
230
265
  *
@@ -234,6 +269,10 @@ function datasourceTestRunToLegacyRow(run) {
234
269
  function coerceProbeRunToResultRows(probeRaw) {
235
270
  if (!probeRaw || typeof probeRaw !== 'object') return [];
236
271
  if (Array.isArray(probeRaw.results) && probeRaw.results.length > 0) return probeRaw.results;
272
+ const summaries = probeRaw.datasourceSummaries;
273
+ if (Array.isArray(summaries) && summaries.length > 0) {
274
+ return summaries.map(datasourceSummaryToLegacyRow);
275
+ }
237
276
  if (isDatasourceTestRunEnvelope(probeRaw)) return [datasourceTestRunToLegacyRow(probeRaw)];
238
277
  return [];
239
278
  }
@@ -54,7 +54,7 @@ function logDeployProbeDatasourceSection(probeData) {
54
54
  const results = coerceProbeRunToResultRows(probeData);
55
55
  const probeSummary = summarizeProbeResults(results);
56
56
  logSectionTitle('Runtime Readiness:');
57
- logDatasourceTable(probeSummary.rows, probeSummary);
57
+ logDatasourceTable(probeSummary.rows, probeSummary, 'Per datasource:');
58
58
  if (probeSummary.issues.length > 0) {
59
59
  logSeparator();
60
60
  logSectionTitle('Key Issues:');
@@ -253,10 +253,9 @@ function logDeployReadinessSummary(ctx) {
253
253
  logDeployConfigDeploymentRuntime(systemCfg, deploymentOk, probeData, summary, deploymentDetail || null);
254
254
 
255
255
  logSeparator();
256
+ logDatasourceTable(summary.rows, summary, probeData ? 'Configured datasources:' : undefined);
256
257
  if (probeData) {
257
258
  logDeployProbeDatasourceSection(probeData);
258
- } else {
259
- logDatasourceTable(summary.rows, summary);
260
259
  }
261
260
 
262
261
  logDeployIdentityAndCredentialBlocks(systemCfg, !!probeData);
@@ -50,9 +50,10 @@ function verdictLine(v) {
50
50
  /**
51
51
  * @param {Array<{ key: string, tier: string }>} rows
52
52
  * @param {{ ready: number, partial: number, failed: number }} counts
53
+ * @param {string} [title]
53
54
  */
54
- function logDatasourceTable(rows, counts) {
55
- logSectionTitle('Datasources:');
55
+ function logDatasourceTable(rows, counts, title) {
56
+ logSectionTitle(title && String(title).trim() ? String(title).trim() : 'Datasources:');
56
57
  for (const r of rows) {
57
58
  const statusLabel = r.tier === 'ready' ? 'Ready' : r.tier === 'failed' ? 'Failed' : 'Partial';
58
59
  logger.log(