@aifabrix/builder 2.44.0 → 2.44.2

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 (71) 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/core/secrets.js +47 -13
  35. package/lib/datasource/log-viewer.js +135 -20
  36. package/lib/datasource/test-e2e.js +35 -17
  37. package/lib/datasource/unified-validation-run-body.js +3 -0
  38. package/lib/datasource/unified-validation-run.js +2 -1
  39. package/lib/external-system/deploy.js +53 -18
  40. package/lib/infrastructure/compose.js +12 -3
  41. package/lib/infrastructure/helpers-docker-check.js +67 -0
  42. package/lib/infrastructure/helpers.js +47 -58
  43. package/lib/infrastructure/index.js +3 -1
  44. package/lib/infrastructure/services.js +4 -56
  45. package/lib/schema/external-system.schema.json +25 -3
  46. package/lib/schema/type/document-storage.json +15 -2
  47. package/lib/utils/api.js +28 -3
  48. package/lib/utils/app-service-env-from-builder.js +47 -6
  49. package/lib/utils/config-paths.js +4 -0
  50. package/lib/utils/configuration-env-resolver.js +11 -8
  51. package/lib/utils/credential-secrets-env.js +5 -5
  52. package/lib/utils/datasource-test-run-certificate-tty.js +82 -0
  53. package/lib/utils/datasource-test-run-display.js +19 -2
  54. package/lib/utils/datasource-test-run-exit.js +25 -0
  55. package/lib/utils/external-system-display.js +8 -0
  56. package/lib/utils/external-system-system-test-tty-overview.js +120 -0
  57. package/lib/utils/external-system-system-test-tty.js +417 -0
  58. package/lib/utils/paths.js +14 -0
  59. package/lib/utils/url-declarative-resolve-load-doc.js +50 -20
  60. package/lib/utils/urls-local-registry.js +36 -12
  61. package/lib/utils/validation-run-poll.js +28 -5
  62. package/lib/utils/validation-run-post-retry.js +20 -8
  63. package/lib/utils/validation-run-request.js +18 -0
  64. package/lib/validation/validate-external-cert-sync.js +23 -0
  65. package/lib/validation/validate.js +4 -1
  66. package/package.json +4 -3
  67. package/scripts/install-local.js +4 -1
  68. package/scripts/pnpm-global-remove.js +48 -0
  69. package/templates/applications/dataplane/env.template +4 -0
  70. package/templates/infra/compose.yaml.hbs +15 -14
  71. package/templates/infra/servers.json.hbs +3 -1
@@ -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);
@@ -8,10 +8,50 @@ const path = require('path');
8
8
  const yaml = require('js-yaml');
9
9
  const fsRealSync = require('../internal/fs-real-sync');
10
10
 
11
+ /**
12
+ * @param {string} cfgPath
13
+ * @returns {object|null}
14
+ */
15
+ function tryReadApplicationYamlAt(cfgPath) {
16
+ try {
17
+ if (!fsRealSync.existsSync(cfgPath)) {
18
+ return null;
19
+ }
20
+ const raw = fsRealSync.readFileSync(cfgPath, 'utf8');
21
+ const doc = yaml.load(raw);
22
+ return doc && typeof doc === 'object' ? doc : null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Prefer projectRoot/builder (fixtures, tests); then {@link pathsUtil.getBuilderPath} (global npm).
30
+ * @param {string} appKey
31
+ * @param {object} pathsUtil
32
+ * @returns {string[]}
33
+ */
34
+ function collectApplicationYamlPathsForUrlResolve(appKey, pathsUtil) {
35
+ const list = [];
36
+ const root = pathsUtil.getProjectRoot();
37
+ if (root) {
38
+ list.push(path.resolve(path.join(root, 'builder', appKey, 'application.yaml')));
39
+ }
40
+ try {
41
+ const viaBuilder = path.resolve(path.join(pathsUtil.getBuilderPath(appKey), 'application.yaml'));
42
+ if (!list.length || path.resolve(list[0]) !== viaBuilder) {
43
+ list.push(viaBuilder);
44
+ }
45
+ } catch {
46
+ /* ignore getBuilderPath errors */
47
+ }
48
+ return list;
49
+ }
50
+
11
51
  /**
12
52
  * @param {string} appKey
13
53
  * @param {Object} ctx
14
- * @param {object} pathsUtil - paths module (getProjectRoot)
54
+ * @param {object} pathsUtil - paths module (getProjectRoot, getBuilderPath)
15
55
  * @returns {object|null}
16
56
  */
17
57
  function loadApplicationYamlDocForUrlResolve(appKey, ctx, pathsUtil) {
@@ -19,28 +59,18 @@ function loadApplicationYamlDocForUrlResolve(appKey, ctx, pathsUtil) {
19
59
  const current = ctx.currentAppKey || '';
20
60
  if (appKey === current && ctx.variablesPath) {
21
61
  const vp = path.resolve(String(ctx.variablesPath));
22
- try {
23
- const raw = fsRealSync.readFileSync(vp, 'utf8');
24
- const doc = yaml.load(raw);
25
- if (doc && typeof doc === 'object') {
26
- return doc;
27
- }
28
- } catch {
29
- // Fall through to builder-relative resolution
62
+ const fromVars = tryReadApplicationYamlAt(vp);
63
+ if (fromVars) {
64
+ return fromVars;
30
65
  }
31
66
  }
32
- const root = pathsUtil.getProjectRoot();
33
- if (!root) {
34
- return null;
35
- }
36
- const cfgPath = path.resolve(path.join(root, 'builder', appKey, 'application.yaml'));
37
- try {
38
- const raw = fsRealSync.readFileSync(cfgPath, 'utf8');
39
- const doc = yaml.load(raw);
40
- return doc && typeof doc === 'object' ? doc : null;
41
- } catch {
42
- return null;
67
+ for (const cfgPath of collectApplicationYamlPathsForUrlResolve(appKey, pathsUtil)) {
68
+ const doc = tryReadApplicationYamlAt(cfgPath);
69
+ if (doc) {
70
+ return doc;
71
+ }
43
72
  }
73
+ return null;
44
74
  } catch {
45
75
  return null;
46
76
  }
@@ -156,19 +156,12 @@ function mergeDocIntoRegistry(merged, doc, folderName) {
156
156
  }
157
157
 
158
158
  /**
159
- * Merge scan results into registry (does not remove stale keys).
160
- * @param {string|null} projectRoot - getProjectRoot() or null
161
- * @returns {Object} Updated registry
159
+ * @param {Object} merged
160
+ * @param {string} builderDir
162
161
  */
163
- function refreshUrlsLocalRegistryFromBuilder(projectRoot) {
164
- const root = projectRoot || pathsUtil.getProjectRoot();
165
- const merged = { ...readUrlsLocalRegistrySync() };
166
- if (!root) {
167
- return writeMergedRegistry(merged);
168
- }
169
- const builderDir = path.join(root, 'builder');
170
- if (!fsRealSync.existsSync(builderDir) || !fsRealSync.statSync(builderDir).isDirectory()) {
171
- return writeMergedRegistry(merged);
162
+ function mergeBuilderDirIntoRegistry(merged, builderDir) {
163
+ if (!builderDir || !fsRealSync.existsSync(builderDir) || !fsRealSync.statSync(builderDir).isDirectory()) {
164
+ return;
172
165
  }
173
166
  for (const ent of fsRealSync.readdirSync(builderDir, { withFileTypes: true })) {
174
167
  if (!ent.isDirectory()) {
@@ -180,6 +173,37 @@ function refreshUrlsLocalRegistryFromBuilder(projectRoot) {
180
173
  }
181
174
  mergeDocIntoRegistry(merged, doc, ent.name);
182
175
  }
176
+ }
177
+
178
+ /**
179
+ * Merge scan results into registry (does not remove stale keys).
180
+ * @param {string|null} projectRoot - getProjectRoot() or null (same semantics as projectRoot || getProjectRoot())
181
+ * @returns {Object} Updated registry
182
+ */
183
+ function refreshUrlsLocalRegistryFromBuilder(projectRoot) {
184
+ const root = projectRoot || pathsUtil.getProjectRoot();
185
+ const merged = { ...readUrlsLocalRegistrySync() };
186
+ if (!root) {
187
+ return writeMergedRegistry(merged);
188
+ }
189
+ // Published npm tarball omits builder/ under the package root (.npmignore). Global installs must
190
+ // still refresh from the real builder tree (AIFABRIX_BUILDER_DIR or integration base + builder).
191
+ const legacyBuilderDir = path.join(root, 'builder');
192
+ const effectiveBuilderDir = pathsUtil.getBuilderRoot();
193
+ const builderDirs = [legacyBuilderDir];
194
+ try {
195
+ if (
196
+ effectiveBuilderDir &&
197
+ path.resolve(effectiveBuilderDir) !== path.resolve(legacyBuilderDir)
198
+ ) {
199
+ builderDirs.push(effectiveBuilderDir);
200
+ }
201
+ } catch {
202
+ /* ignore path resolution errors */
203
+ }
204
+ for (const builderDir of builderDirs) {
205
+ mergeBuilderDirIntoRegistry(merged, builderDir);
206
+ }
183
207
  return writeMergedRegistry(merged);
184
208
  }
185
209
 
@@ -4,6 +4,8 @@
4
4
  * @version 2.0.0
5
5
  */
6
6
 
7
+ const chalk = require('chalk');
8
+ const logger = require('./logger');
7
9
  const { getValidationRunWithTransportRetry } = require('./validation-run-post-retry');
8
10
 
9
11
  const INITIAL_INTERVAL_MS = 2000;
@@ -23,6 +25,19 @@ function sleep(ms) {
23
25
  return new Promise(resolve => setTimeout(resolve, ms));
24
26
  }
25
27
 
28
+ function maybeLogPollProgress(envelope, verbosePoll, lastProgressLogAtRef) {
29
+ if (!verbosePoll || !envelope || typeof envelope !== 'object') return;
30
+ const now = Date.now();
31
+ if (now - lastProgressLogAtRef[0] < 5000) return;
32
+ lastProgressLogAtRef[0] = now;
33
+ const st = envelope.status !== undefined && envelope.status !== null ? String(envelope.status) : '?';
34
+ const c =
35
+ envelope.reportCompleteness !== undefined && envelope.reportCompleteness !== null
36
+ ? String(envelope.reportCompleteness)
37
+ : '?';
38
+ logger.log(chalk.gray(` Polling validation run… completeness=${c} status=${st}`));
39
+ }
40
+
26
41
  /**
27
42
  * Whether polling should stop on this envelope.
28
43
  * @param {Object} envelope - DatasourceTestRun-like
@@ -42,6 +57,8 @@ function isTerminalReportCompleteness(envelope) {
42
57
  * @param {string} opts.testRunId
43
58
  * @param {number} opts.budgetMs - Remaining wall-clock budget for polls only (POST excluded)
44
59
  * @param {typeof getValidationRunWithTransportRetry} [opts.fetchRun] - Inject for tests (default: GET with transport retry)
60
+ * @param {boolean} [opts.verbosePoll] - Log throttled progress (plan §3.13)
61
+ * @param {number} [opts.pollRequestTimeoutMs] - Per-GET HTTP timeout (match validation aggregate budget)
45
62
  * @returns {Promise<{ envelope: Object|null, timedOut: boolean, lastApiResult: Object|null }>}
46
63
  */
47
64
  async function pollValidationRunUntilComplete(opts) {
@@ -50,18 +67,22 @@ async function pollValidationRunUntilComplete(opts) {
50
67
  authConfig,
51
68
  testRunId,
52
69
  budgetMs,
53
- fetchRun = getValidationRunWithTransportRetry
70
+ fetchRun = getValidationRunWithTransportRetry,
71
+ verbosePoll = false,
72
+ pollRequestTimeoutMs
54
73
  } = opts;
74
+ const pollTransportOpts =
75
+ Number.isFinite(pollRequestTimeoutMs) && pollRequestTimeoutMs > 0
76
+ ? { timeoutMs: pollRequestTimeoutMs }
77
+ : {};
55
78
  const deadline = Date.now() + Math.max(0, budgetMs);
56
79
  let attempt = 0;
57
80
  let lastApiResult = null;
58
81
  let envelope = null;
82
+ const lastProgressLogAtRef = [0];
59
83
 
60
84
  while (Date.now() < deadline) {
61
- const remaining = deadline - Date.now();
62
- if (remaining <= 0) break;
63
-
64
- lastApiResult = await fetchRun(dataplaneUrl, authConfig, testRunId);
85
+ lastApiResult = await fetchRun(dataplaneUrl, authConfig, testRunId, pollTransportOpts);
65
86
  if (!lastApiResult.success) {
66
87
  return { envelope: null, timedOut: false, lastApiResult };
67
88
  }
@@ -70,6 +91,8 @@ async function pollValidationRunUntilComplete(opts) {
70
91
  return { envelope, timedOut: false, lastApiResult };
71
92
  }
72
93
 
94
+ maybeLogPollProgress(envelope, verbosePoll, lastProgressLogAtRef);
95
+
73
96
  const delay = Math.min(nextPollDelayMs(attempt), Math.max(0, deadline - Date.now()));
74
97
  attempt += 1;
75
98
  if (delay > 0) {