@aifabrix/builder 2.44.3 → 2.44.4

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 (56) 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 +12 -1
  5. package/lib/api/certificates.api.js +21 -3
  6. package/lib/certification/post-unified-cert-sync.js +13 -2
  7. package/lib/certification/sync-after-external-command.js +6 -3
  8. package/lib/certification/sync-system-certification.js +60 -14
  9. package/lib/cli/setup-app.test-commands.js +67 -35
  10. package/lib/cli/setup-utility.js +1 -1
  11. package/lib/commands/datasource-unified-test-cli.js +81 -46
  12. package/lib/commands/datasource-unified-test-cli.options.js +4 -2
  13. package/lib/commands/datasource.js +3 -31
  14. package/lib/commands/repair-datasource-keys.js +1 -1
  15. package/lib/commands/repair-datasource-openapi.js +57 -0
  16. package/lib/commands/repair-datasource.js +5 -0
  17. package/lib/commands/repair-internal.js +2 -4
  18. package/lib/commands/repair.js +1 -2
  19. package/lib/commands/test-e2e-external.js +5 -6
  20. package/lib/commands/upload.js +18 -4
  21. package/lib/commands/wizard-dataplane.js +14 -6
  22. package/lib/datasource/datasource-validate-display.js +162 -0
  23. package/lib/datasource/datasource-validate-summary.js +194 -0
  24. package/lib/datasource/test-e2e.js +65 -37
  25. package/lib/datasource/unified-validation-run-body.js +1 -2
  26. package/lib/datasource/validate.js +14 -6
  27. package/lib/external-system/test.js +12 -8
  28. package/lib/generator/external-controller-manifest.js +12 -2
  29. package/lib/schema/cip-capacity-display.fallback.json +7 -0
  30. package/lib/schema/datasource-test-run.schema.json +79 -1
  31. package/lib/schema/external-datasource.schema.json +94 -2
  32. package/lib/schema/flag-map-validation-run.json +1 -2
  33. package/lib/schema/type/document-storage.json +83 -3
  34. package/lib/utils/configuration-env-resolver.js +38 -0
  35. package/lib/utils/dataplane-resolver.js +3 -2
  36. package/lib/utils/datasource-test-run-capacity-operations.js +149 -0
  37. package/lib/utils/datasource-test-run-debug-display.js +143 -1
  38. package/lib/utils/datasource-test-run-display.js +46 -33
  39. package/lib/utils/datasource-test-run-tty-log.js +6 -2
  40. package/lib/utils/datasource-test-run-tty-meta-lines.js +123 -0
  41. package/lib/utils/error-formatter.js +32 -2
  42. package/lib/utils/external-system-readiness-core.js +39 -0
  43. package/lib/utils/external-system-readiness-deploy-display.js +2 -3
  44. package/lib/utils/external-system-readiness-display-internals.js +3 -2
  45. package/lib/utils/external-system-system-test-tty.js +33 -9
  46. package/lib/utils/external-system-validators.js +62 -5
  47. package/lib/utils/load-cip-capacity-display-config.js +130 -0
  48. package/lib/utils/paths.js +10 -3
  49. package/lib/utils/schema-resolver.js +98 -2
  50. package/lib/utils/validation-run-poll.js +15 -4
  51. package/lib/utils/validation-run-request.js +4 -6
  52. package/lib/validation/dimension-display-helpers.js +60 -0
  53. package/lib/validation/validate-display-log-helpers.js +39 -0
  54. package/lib/validation/validate-display.js +89 -83
  55. package/package.json +1 -1
  56. package/templates/external-system/README.md.hbs +1 -2
@@ -7,12 +7,12 @@
7
7
  "key": "document-storage-schema",
8
8
  "name": "Document Storage Configuration Schema",
9
9
  "description": "JSON schema for validating document storage configurations",
10
- "version": "1.3.0",
10
+ "version": "1.7.0",
11
11
  "type": "schema",
12
12
  "category": "document-storage",
13
13
  "author": "AI Fabrix Team",
14
14
  "createdAt": "2026-01-02T00:00:00Z",
15
- "updatedAt": "2026-04-22T00:00:00Z",
15
+ "updatedAt": "2026-05-01T00:00:00Z",
16
16
  "compatibility": {
17
17
  "minVersion": "1.0.0",
18
18
  "maxVersion": "2.0.0",
@@ -71,6 +71,38 @@
71
71
  "Added optional parameterLookupCoalesceNestedItemScope (boolean, default true) for manifest-controlled binary parameter lookup enrichment"
72
72
  ],
73
73
  "breaking": false
74
+ },
75
+ {
76
+ "version": "1.4.0",
77
+ "date": "2026-04-26T00:00:00Z",
78
+ "changes": [
79
+ "Added optional maxBinaryFetchesPerSync (integer >= 1) to cap binary fetch+store operations per sync run"
80
+ ],
81
+ "breaking": false
82
+ },
83
+ {
84
+ "version": "1.5.0",
85
+ "date": "2026-04-27T00:00:00Z",
86
+ "changes": [
87
+ "ingestAfterSync: post-sync chunk+embed no longer waits on trust (eligibility/schema gates unchanged)"
88
+ ],
89
+ "breaking": false
90
+ },
91
+ {
92
+ "version": "1.6.0",
93
+ "date": "2026-04-28T00:00:00Z",
94
+ "changes": [
95
+ "Added documentStorage.changeDetection.ignoreMetadataKeys / ignoreMetadataPaths for stable contentHash when volatile metadata changes"
96
+ ],
97
+ "breaking": false
98
+ },
99
+ {
100
+ "version": "1.7.0",
101
+ "date": "2026-05-01T00:00:00Z",
102
+ "changes": [
103
+ "Added optional maxBinaryFetchConcurrency (integer >= 1, default 1) to bound parallel binary fetches in two-phase sync phase 3"
104
+ ],
105
+ "breaking": false
74
106
  }
75
107
  ]
76
108
  },
@@ -95,7 +127,18 @@
95
127
  "ingestAfterSync": {
96
128
  "type": "boolean",
97
129
  "default": false,
98
- "description": "When true, chunk and embed each document after store during sync so vector search returns hits immediately. When false, ingestion runs later (e.g. Celery task or on approval). Set true for E2E tests that validate vector step."
130
+ "description": "When true, chunk and embed each document after store during sync without requiring trust promotion first; eligibility (validation/schema/dimensions) still enforced. When false, ingestion runs later."
131
+ },
132
+ "maxBinaryFetchesPerSync": {
133
+ "type": "integer",
134
+ "minimum": 1,
135
+ "description": "When set, caps binary fetch + store operations per sync run (metadata list may still return more rows). Optional for fast integration/E2E; omit in production for full sync."
136
+ },
137
+ "maxBinaryFetchConcurrency": {
138
+ "type": "integer",
139
+ "minimum": 1,
140
+ "default": 1,
141
+ "description": "Maximum concurrent binary CIP/HTTP fetches during two-phase sync phase 3. Default 1 (sequential). Increase cautiously; external APIs may throttle."
99
142
  },
100
143
  "binaryOperationRef": {
101
144
  "type": "string",
@@ -133,6 +176,22 @@
133
176
  "default": true,
134
177
  "description": "When true, binary parameterMapping and HTTP path templates use a lookup view that merges metadata and coalesces storage-scope ids from a nested item parentReference when the row's parentReference omits them. Set false for strict manifest-only paths."
135
178
  },
179
+ "jsonNestedDownloadUrlKeys": {
180
+ "type": "array",
181
+ "items": {
182
+ "type": "string",
183
+ "minLength": 1
184
+ },
185
+ "description": "Optional JSON object key names (exact match) searched depth-first when a binary GET returns JSON without bytes; requires preAuthenticatedDownloadUrlHostSuffixAllowlist. Merged into binaryOperation when using binaryOperationRef."
186
+ },
187
+ "preAuthenticatedDownloadUrlHostSuffixAllowlist": {
188
+ "type": "array",
189
+ "items": {
190
+ "type": "string",
191
+ "minLength": 1
192
+ },
193
+ "description": "Substrings the discovered follow-up URL must contain (case-insensitive) before an unauthenticated GET. Required when jsonNestedDownloadUrlKeys is set."
194
+ },
136
195
  "processing": {
137
196
  "type": "object",
138
197
  "properties": {
@@ -219,6 +278,27 @@
219
278
  }
220
279
  },
221
280
  "additionalProperties": false
281
+ },
282
+ "changeDetection": {
283
+ "type": "object",
284
+ "description": "Optional configuration to ignore volatile metadata keys/paths during contentHash / change detection only.",
285
+ "properties": {
286
+ "ignoreMetadataKeys": {
287
+ "type": "array",
288
+ "description": "Top-level metadata keys to ignore when computing contentHash / change detection.",
289
+ "items": { "type": "string", "minLength": 1 },
290
+ "uniqueItems": true,
291
+ "default": []
292
+ },
293
+ "ignoreMetadataPaths": {
294
+ "type": "array",
295
+ "description": "Optional dot-path patterns to ignore (e.g. 'analytics.*', 'versions').",
296
+ "items": { "type": "string", "minLength": 1 },
297
+ "uniqueItems": true,
298
+ "default": []
299
+ }
300
+ },
301
+ "additionalProperties": false
222
302
  }
223
303
  },
224
304
  "additionalProperties": false
@@ -113,6 +113,43 @@ function resolveConfigurationValues(configArray, envMap, secrets, systemKey) {
113
113
  }
114
114
  }
115
115
 
116
+ /**
117
+ * Collects `configuration[]` entry names with `location: "variable"` whose values look
118
+ * resolved for publish (literal strings: no `{{`, not `kv://`, non-empty). Used after
119
+ * `resolveConfigurationValues` mutates the upload payload.
120
+ *
121
+ * @param {{ application?: { configuration?: unknown[] }, dataSources?: unknown[] }} payload - Pipeline upload payload
122
+ * @returns {string[]} Unique names in encounter order (application first, then dataSources)
123
+ */
124
+ function collectResolvedVariableConfigurationNames(payload) {
125
+ const out = [];
126
+ const seen = new Set();
127
+
128
+ function consider(arr) {
129
+ if (!Array.isArray(arr)) return;
130
+ for (const item of arr) {
131
+ if (!item || typeof item !== 'object') continue;
132
+ if (String(item.location || '').toLowerCase() !== 'variable') continue;
133
+ const name = item.name && String(item.name).trim();
134
+ if (!name || seen.has(name)) continue;
135
+ const val = item.value;
136
+ if (typeof val !== 'string') continue;
137
+ const trimmed = val.trim();
138
+ if (!trimmed) continue;
139
+ if (trimmed.startsWith('kv://')) continue;
140
+ if (trimmed.includes('{{')) continue;
141
+ seen.add(name);
142
+ out.push(name);
143
+ }
144
+ }
145
+
146
+ consider(payload?.application?.configuration);
147
+ for (const ds of payload?.dataSources || []) {
148
+ consider(ds?.configuration);
149
+ }
150
+ return out;
151
+ }
152
+
116
153
  /**
117
154
  * Returns the set of variable names (keys) defined in env.template content.
118
155
  *
@@ -175,6 +212,7 @@ async function retemplateConfigurationForDownload(systemKey, configArray) {
175
212
  module.exports = {
176
213
  buildResolvedEnvMapForIntegration,
177
214
  resolveConfigurationValues,
215
+ collectResolvedVariableConfigurationNames,
178
216
  getEnvTemplateVariableNames,
179
217
  retemplateConfigurationFromEnvTemplate,
180
218
  retemplateConfigurationForDownload,
@@ -17,11 +17,12 @@ const { discoverDataplaneUrl } = require('../commands/wizard-dataplane');
17
17
  * @param {string} controllerUrl - Controller URL
18
18
  * @param {string} environment - Environment key
19
19
  * @param {Object} authConfig - Authentication configuration
20
+ * @param {{ silent?: boolean }} [opts] - Passed to discoverDataplaneUrl
20
21
  * @returns {Promise<string>} Resolved dataplane URL
21
22
  * @throws {Error} If dataplane URL cannot be resolved
22
23
  */
23
- async function resolveDataplaneUrl(controllerUrl, environment, authConfig) {
24
- return await discoverDataplaneUrl(controllerUrl, environment, authConfig);
24
+ async function resolveDataplaneUrl(controllerUrl, environment, authConfig, opts = {}) {
25
+ return await discoverDataplaneUrl(controllerUrl, environment, authConfig, opts);
25
26
  }
26
27
 
27
28
  module.exports = {
@@ -0,0 +1,149 @@
1
+ /**
2
+ * @fileoverview Capacity scenario outcome lines for DatasourceTestRun TTY (shared by header + debug appendix).
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const chalk = require('chalk');
10
+ const { successGlyph, failureGlyph } = require('./cli-test-layout-chalk');
11
+ const {
12
+ getCipCapacityDisplayConfig,
13
+ standardOperationRank,
14
+ parseCapacityDetailKey
15
+ } = require('./load-cip-capacity-display-config');
16
+
17
+ /**
18
+ * @param {string} op
19
+ * @returns {string}
20
+ */
21
+ function formatCapacityOperationLabel(op) {
22
+ const { aliases } = getCipCapacityDisplayConfig();
23
+ return (aliases && aliases[op]) || op;
24
+ }
25
+
26
+ /**
27
+ * @param {Object} envelope
28
+ * @returns {object|null}
29
+ */
30
+ function findCapacityStep(envelope) {
31
+ const dbg = envelope && envelope.debug;
32
+ const e2e = dbg && dbg.e2eAsyncDebug;
33
+ const stepDebug = e2e && Array.isArray(e2e.stepDebug) ? e2e.stepDebug : [];
34
+ return stepDebug.find(s => s && String(s.name) === 'capacity') || null;
35
+ }
36
+
37
+ /**
38
+ * @param {object[]} rows
39
+ * @param {string} datasourceKey
40
+ * @returns {object[]}
41
+ */
42
+ function filterCapacityDatasourceRows(rows, datasourceKey) {
43
+ if (!datasourceKey) return rows;
44
+ const matched = rows.filter(r => r && String(r.key) === datasourceKey);
45
+ return matched.length ? matched : rows;
46
+ }
47
+
48
+ /**
49
+ * @param {object[]} dsRows
50
+ * @returns {Map<string, { ok: boolean, error: string, minIndex: number }>}
51
+ */
52
+ function mergeCapacityDetailsByOp(dsRows) {
53
+ /** @type {Map<string, { ok: boolean, error: string, minIndex: number }>} */
54
+ const byOp = new Map();
55
+ for (const ds of dsRows) {
56
+ const details = ds && Array.isArray(ds.capabilityDetails) ? ds.capabilityDetails : [];
57
+ for (const row of details) {
58
+ if (!row || row.key === undefined || row.key === null) continue;
59
+ const parsed = parseCapacityDetailKey(String(row.key));
60
+ if (!parsed) continue;
61
+ const { op, index } = parsed;
62
+ const ok = row.success !== false && !row.error;
63
+ const err = row.error ? String(row.error) : '';
64
+ const prev = byOp.get(op);
65
+ if (!prev) {
66
+ byOp.set(op, { ok, error: ok ? '' : err, minIndex: index });
67
+ } else {
68
+ byOp.set(op, {
69
+ ok: prev.ok && ok,
70
+ error: prev.error || err,
71
+ minIndex: Math.min(prev.minIndex, index)
72
+ });
73
+ }
74
+ }
75
+ }
76
+ return byOp;
77
+ }
78
+
79
+ /**
80
+ * @param {string[]} standardOrder
81
+ * @param {Map<string, { minIndex: number }>} byOp
82
+ * @returns {(a: string, b: string) => number}
83
+ */
84
+ function capacityOpComparator(standardOrder, byOp) {
85
+ return (a, b) => {
86
+ const ia = byOp.get(a).minIndex;
87
+ const ib = byOp.get(b).minIndex;
88
+ if (ia !== ib) return ia - ib;
89
+ const ra = standardOperationRank(standardOrder, a);
90
+ const rb = standardOperationRank(standardOrder, b);
91
+ if (ra !== rb) return ra - rb;
92
+ return a.localeCompare(b);
93
+ };
94
+ }
95
+
96
+ /**
97
+ * @param {string[]} lines
98
+ * @param {Map<string, { ok: boolean, error: string }>} byOp
99
+ * @param {string[]} opsSorted
100
+ */
101
+ function appendCapacityOperationsLines(lines, byOp, opsSorted) {
102
+ lines.push('');
103
+ lines.push(chalk.blue.bold('Capacity operations:'));
104
+ for (const op of opsSorted) {
105
+ const row = byOp.get(op);
106
+ const label = formatCapacityOperationLabel(op);
107
+ const sym = row.ok ? successGlyph() : failureGlyph();
108
+ const tail = row.ok ? '' : chalk.red(` — ${row.error || 'failed'}`);
109
+ lines.push(` ${sym} ${chalk.white(label)}${tail}`);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * List capacity scenario outcomes from the capacity step (order: scenario index `#`, then schema op order).
115
+ * @param {string[]} lines
116
+ * @param {Object} envelope
117
+ */
118
+ function pushCapacityOperationsSummaryLines(lines, envelope) {
119
+ const capStep = findCapacityStep(envelope);
120
+ if (!capStep || !capStep.evidence || !Array.isArray(capStep.evidence.datasources)) return;
121
+
122
+ const dk =
123
+ envelope && envelope.datasourceKey !== undefined && envelope.datasourceKey !== null
124
+ ? String(envelope.datasourceKey)
125
+ : '';
126
+ const dsRows = filterCapacityDatasourceRows(capStep.evidence.datasources, dk);
127
+ const byOp = mergeCapacityDetailsByOp(dsRows);
128
+ if (!byOp.size) return;
129
+
130
+ const { standardOrder } = getCipCapacityDisplayConfig();
131
+ const opsSorted = Array.from(byOp.keys()).sort(capacityOpComparator(standardOrder, byOp));
132
+ appendCapacityOperationsLines(lines, byOp, opsSorted);
133
+ }
134
+
135
+ /**
136
+ * @param {string} capacityKey
137
+ * @returns {string|null}
138
+ */
139
+ function parseCapacityScenarioOp(capacityKey) {
140
+ const p = parseCapacityDetailKey(String(capacityKey));
141
+ return p ? p.op : null;
142
+ }
143
+
144
+ module.exports = {
145
+ pushCapacityOperationsSummaryLines,
146
+ formatCapacityOperationLabel,
147
+ parseCapacityScenarioOp,
148
+ parseCapacityDetailKey
149
+ };
@@ -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);