@duckcodeailabs/dql-core 1.14.1 → 1.14.3-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,6 +36,10 @@ import { loadManifestKnowledgeSkills, manifestKnowledgeSkillInputFiles } from '.
36
36
  export function collectInputFiles(options) {
37
37
  const { projectRoot } = options;
38
38
  const files = new Set();
39
+ // Cache overlapping directory walks here as well. `sync dbt` asks for these
40
+ // tracked files before a cache lookup, so repeated block/domain roots must
41
+ // not turn a warm-cache check into several 4k-file traversals.
42
+ const scanCache = createManifestScanCache();
39
43
  const configPath = join(projectRoot, 'dql.config.json');
40
44
  if (existsSync(configPath))
41
45
  files.add(configPath);
@@ -46,27 +50,27 @@ export function collectInputFiles(options) {
46
50
  }
47
51
  const blockDirs = ['blocks', 'domains', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
48
52
  for (const dir of blockDirs) {
49
- for (const f of scanFilesRecursive(join(projectRoot, dir), ['.dql']))
53
+ for (const f of scanFilesRecursive(join(projectRoot, dir), ['.dql'], scanCache))
50
54
  files.add(f);
51
55
  }
52
56
  const businessViewDirs = ['blocks', 'business-views', 'domains', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
53
57
  for (const dir of businessViewDirs) {
54
- for (const f of scanFilesRecursive(join(projectRoot, dir), ['.dql']))
58
+ for (const f of scanFilesRecursive(join(projectRoot, dir), ['.dql'], scanCache))
55
59
  files.add(f);
56
60
  }
57
61
  const termDirs = ['terms', 'blocks', 'business-views', 'domains', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
58
62
  for (const dir of termDirs) {
59
- for (const f of scanFilesRecursive(join(projectRoot, dir), ['.dql']))
63
+ for (const f of scanFilesRecursive(join(projectRoot, dir), ['.dql'], scanCache))
60
64
  files.add(f);
61
65
  }
62
66
  const notebookDirs = ['notebooks', 'domains', 'blocks', 'dashboards', 'workbooks', ...(options.extraNotebookDirs ?? [])];
63
67
  for (const dir of notebookDirs) {
64
- for (const f of scanFilesRecursive(join(projectRoot, dir), ['.dqlnb']))
68
+ for (const f of scanFilesRecursive(join(projectRoot, dir), ['.dqlnb'], scanCache))
65
69
  files.add(f);
66
70
  }
67
71
  const semanticDir = resolveSemanticPath(projectRoot, config);
68
72
  if (existsSync(semanticDir)) {
69
- for (const f of scanFilesRecursive(semanticDir, ['.yaml', '.yml']))
73
+ for (const f of scanFilesRecursive(semanticDir, ['.yaml', '.yml'], scanCache))
70
74
  files.add(f);
71
75
  }
72
76
  if (options.dbtManifestPath && existsSync(options.dbtManifestPath)) {
@@ -79,7 +83,7 @@ export function collectInputFiles(options) {
79
83
  files.add(semanticManifestPath);
80
84
  }
81
85
  if (config.manifestVersion === 3 && config.modeling?.mode === 'dbt-first') {
82
- for (const f of scanFilesRecursive(join(projectRoot, 'domains'), ['.yaml', '.yml']))
86
+ for (const f of scanFilesRecursive(join(projectRoot, 'domains'), ['.yaml', '.yml'], scanCache))
83
87
  files.add(f);
84
88
  const registry = loadDomainPackageRegistry(projectRoot);
85
89
  for (const f of manifestKnowledgeSkillInputFiles(projectRoot, registry))
@@ -98,6 +102,12 @@ export function collectInputFiles(options) {
98
102
  export function buildManifest(options) {
99
103
  const { projectRoot, dqlVersion = '0.6.0' } = options;
100
104
  const diagnostics = [];
105
+ // A large project commonly stores blocks, terms, and domains under the same
106
+ // directory tree. The manifest scanner needs distinct declaration passes,
107
+ // but it must not re-walk that tree or re-read the same source three times
108
+ // during one build. Source text is always read freshly; unchanged text can
109
+ // reuse a bounded parsed AST across adjacent builds.
110
+ const scanCache = createManifestScanCache(projectRoot);
101
111
  // Load project config
102
112
  const config = loadProjectConfig(projectRoot);
103
113
  const projectName = config.project ?? 'dql-project';
@@ -126,20 +136,20 @@ export function buildManifest(options) {
126
136
  const packageRegistry = dbtFirstV3 ? loadDomainPackageRegistry(projectRoot) : undefined;
127
137
  const domains = packageRegistry
128
138
  ? Object.fromEntries(packageRegistry.values().map((pkg) => [pkg.id, pkg.domain]))
129
- : scanDomains(projectRoot, domainDirs, diagnostics);
139
+ : scanDomains(projectRoot, domainDirs, diagnostics, scanCache);
130
140
  if (!packageRegistry)
131
141
  validateDomainHierarchy(domains, diagnostics);
132
142
  // Scan blocks
133
143
  const blockDirs = ['blocks', 'domains', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
134
- const blockScan = scanBlocks(projectRoot, blockDirs, diagnostics, datalexRegistry);
144
+ const blockScan = scanBlocks(projectRoot, blockDirs, diagnostics, datalexRegistry, scanCache);
135
145
  const blocks = blockScan.blocks;
136
146
  const knowledgeBlocks = blockScan.all;
137
147
  // Scan business composition views
138
148
  const businessViewDirs = ['blocks', 'business-views', 'domains', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
139
- const businessViews = scanBusinessViews(projectRoot, businessViewDirs, diagnostics);
149
+ const businessViews = scanBusinessViews(projectRoot, businessViewDirs, diagnostics, scanCache);
140
150
  // Scan business glossary terms
141
151
  const termDirs = ['terms', 'blocks', 'business-views', 'domains', 'dashboards', 'workbooks', ...(options.extraBlockDirs ?? [])];
142
- const terms = scanTerms(projectRoot, termDirs, diagnostics);
152
+ const terms = scanTerms(projectRoot, termDirs, diagnostics, scanCache);
143
153
  if (packageRegistry) {
144
154
  applyDomainPackageOwnership(projectRoot, packageRegistry, blocks, businessViews, terms, diagnostics);
145
155
  applyDomainPackageOwnershipToValues(projectRoot, packageRegistry, knowledgeBlocks, 'Block', diagnostics);
@@ -455,11 +465,44 @@ function resolveSemanticPath(projectRoot, config) {
455
465
  return join(projectRoot, customPath);
456
466
  return join(projectRoot, 'semantic-layer');
457
467
  }
458
- // ---- Recursive File Scanner ----
459
- function scanFilesRecursive(dir, extensions) {
468
+ // Keep only the most recently compiled project roots. Source text is compared
469
+ // before any AST reuse, so this is a parser cache rather than a stale-source
470
+ // cache; it makes watcher-triggered one-file edits avoid reparsing the other
471
+ // thousands of unchanged DQL declarations.
472
+ const MAX_MANIFEST_PARSE_CACHE_PROJECTS = 4;
473
+ const manifestParseCaches = new Map();
474
+ function createManifestScanCache(projectRoot) {
475
+ let parsedSources;
476
+ if (projectRoot) {
477
+ parsedSources = manifestParseCaches.get(projectRoot);
478
+ if (parsedSources) {
479
+ // Map insertion order doubles as a tiny LRU.
480
+ manifestParseCaches.delete(projectRoot);
481
+ manifestParseCaches.set(projectRoot, parsedSources);
482
+ }
483
+ else {
484
+ parsedSources = new Map();
485
+ manifestParseCaches.set(projectRoot, parsedSources);
486
+ while (manifestParseCaches.size > MAX_MANIFEST_PARSE_CACHE_PROJECTS) {
487
+ const oldest = manifestParseCaches.keys().next().value;
488
+ if (!oldest)
489
+ break;
490
+ manifestParseCaches.delete(oldest);
491
+ }
492
+ }
493
+ }
494
+ return { filesByDirectory: new Map(), sources: new Map(), parsedSources };
495
+ }
496
+ function scanFilesRecursive(dir, extensions, cache) {
497
+ const cacheKey = `${dir}\u0000${[...extensions].sort().join(',')}`;
498
+ const cached = cache?.filesByDirectory.get(cacheKey);
499
+ if (cached)
500
+ return cached;
460
501
  const results = [];
461
- if (!existsSync(dir))
502
+ if (!existsSync(dir)) {
503
+ cache?.filesByDirectory.set(cacheKey, results);
462
504
  return results;
505
+ }
463
506
  const entries = readdirSync(dir, { withFileTypes: true });
464
507
  for (const entry of entries) {
465
508
  const fullPath = join(dir, entry.name);
@@ -467,32 +510,48 @@ function scanFilesRecursive(dir, extensions) {
467
510
  // Skip hidden dirs and node_modules
468
511
  if (entry.name.startsWith('.') || entry.name === 'node_modules')
469
512
  continue;
470
- results.push(...scanFilesRecursive(fullPath, extensions));
513
+ results.push(...scanFilesRecursive(fullPath, extensions, cache));
471
514
  }
472
515
  else if (entry.isFile() && extensions.includes(extname(entry.name))) {
473
516
  results.push(fullPath);
474
517
  }
475
518
  }
519
+ cache?.filesByDirectory.set(cacheKey, results);
476
520
  return results;
477
521
  }
522
+ function readManifestSource(filePath, cache) {
523
+ const cached = cache?.sources.get(filePath);
524
+ if (cached !== undefined)
525
+ return cached;
526
+ const source = readFileSync(filePath, 'utf-8');
527
+ cache?.sources.set(filePath, source);
528
+ return source;
529
+ }
530
+ function parseManifestSource(source, relPath, filePath, cache) {
531
+ const cached = cache?.parsedSources?.get(filePath);
532
+ if (cached?.source === source)
533
+ return cached.ast;
534
+ const ast = new Parser(source, relPath).parse();
535
+ cache?.parsedSources?.set(filePath, { source, ast });
536
+ return ast;
537
+ }
478
538
  // ---- Block Scanning ----
479
- function scanDomains(projectRoot, dirs, diagnostics) {
539
+ function scanDomains(projectRoot, dirs, diagnostics, scanCache) {
480
540
  const domains = {};
481
541
  const seenFiles = new Set();
482
542
  for (const dir of dirs) {
483
543
  const dirPath = join(projectRoot, dir);
484
- const files = scanFilesRecursive(dirPath, ['.dql']);
544
+ const files = scanFilesRecursive(dirPath, ['.dql'], scanCache);
485
545
  for (const filePath of files) {
486
546
  if (seenFiles.has(filePath))
487
547
  continue;
488
548
  seenFiles.add(filePath);
489
549
  const relPath = relative(projectRoot, filePath);
490
550
  try {
491
- const source = readFileSync(filePath, 'utf-8');
551
+ const source = readManifestSource(filePath, scanCache);
492
552
  if (!/(^|\n)\s*domain\s+"/.test(source))
493
553
  continue;
494
- const parser = new Parser(source, relPath);
495
- const ast = parser.parse();
554
+ const ast = parseManifestSource(source, relPath, filePath, scanCache);
496
555
  for (const stmt of ast.statements) {
497
556
  const domain = stmt;
498
557
  if (domain.kind !== 'DomainDecl')
@@ -557,22 +616,21 @@ function validateDomainHierarchy(domains, diagnostics) {
557
616
  }
558
617
  }
559
618
  }
560
- function scanBlocks(projectRoot, dirs, diagnostics, datalexRegistry) {
619
+ function scanBlocks(projectRoot, dirs, diagnostics, datalexRegistry, scanCache) {
561
620
  const blocks = {};
562
621
  const all = [];
563
622
  const seenFiles = new Set();
564
623
  for (const dir of dirs) {
565
624
  const dirPath = join(projectRoot, dir);
566
- const files = scanFilesRecursive(dirPath, ['.dql']);
625
+ const files = scanFilesRecursive(dirPath, ['.dql'], scanCache);
567
626
  for (const filePath of files) {
568
627
  if (seenFiles.has(filePath))
569
628
  continue;
570
629
  seenFiles.add(filePath);
571
630
  const relPath = relative(projectRoot, filePath);
572
631
  try {
573
- const source = readFileSync(filePath, 'utf-8');
574
- const parser = new Parser(source, relPath);
575
- const ast = parser.parse();
632
+ const source = readManifestSource(filePath, scanCache);
633
+ const ast = parseManifestSource(source, relPath, filePath, scanCache);
576
634
  collectContractDiagnostics(ast, relPath, diagnostics, datalexRegistry);
577
635
  for (const stmt of ast.statements) {
578
636
  const block = stmt;
@@ -629,23 +687,22 @@ function duplicateBlockPriority(block) {
629
687
  return score;
630
688
  }
631
689
  // ---- Business View Scanning ----
632
- function scanBusinessViews(projectRoot, dirs, diagnostics) {
690
+ function scanBusinessViews(projectRoot, dirs, diagnostics, scanCache) {
633
691
  const views = {};
634
692
  const seenFiles = new Set();
635
693
  for (const dir of dirs) {
636
694
  const dirPath = join(projectRoot, dir);
637
- const files = scanFilesRecursive(dirPath, ['.dql']);
695
+ const files = scanFilesRecursive(dirPath, ['.dql'], scanCache);
638
696
  for (const filePath of files) {
639
697
  if (seenFiles.has(filePath))
640
698
  continue;
641
699
  seenFiles.add(filePath);
642
700
  const relPath = relative(projectRoot, filePath);
643
701
  try {
644
- const source = readFileSync(filePath, 'utf-8');
702
+ const source = readManifestSource(filePath, scanCache);
645
703
  if (!source.includes('business_view'))
646
704
  continue;
647
- const parser = new Parser(source, relPath);
648
- const ast = parser.parse();
705
+ const ast = parseManifestSource(source, relPath, filePath, scanCache);
649
706
  for (const stmt of ast.statements) {
650
707
  const view = stmt;
651
708
  if (view.kind !== 'BusinessViewDecl')
@@ -676,23 +733,22 @@ function scanBusinessViews(projectRoot, dirs, diagnostics) {
676
733
  return views;
677
734
  }
678
735
  // ---- Business Term Scanning ----
679
- function scanTerms(projectRoot, dirs, diagnostics) {
736
+ function scanTerms(projectRoot, dirs, diagnostics, scanCache) {
680
737
  const terms = {};
681
738
  const seenFiles = new Set();
682
739
  for (const dir of dirs) {
683
740
  const dirPath = join(projectRoot, dir);
684
- const files = scanFilesRecursive(dirPath, ['.dql']);
741
+ const files = scanFilesRecursive(dirPath, ['.dql'], scanCache);
685
742
  for (const filePath of files) {
686
743
  if (seenFiles.has(filePath))
687
744
  continue;
688
745
  seenFiles.add(filePath);
689
746
  const relPath = relative(projectRoot, filePath);
690
747
  try {
691
- const source = readFileSync(filePath, 'utf-8');
748
+ const source = readManifestSource(filePath, scanCache);
692
749
  if (!source.includes('term'))
693
750
  continue;
694
- const parser = new Parser(source, relPath);
695
- const ast = parser.parse();
751
+ const ast = parseManifestSource(source, relPath, filePath, scanCache);
696
752
  for (const stmt of ast.statements) {
697
753
  const term = stmt;
698
754
  if (term.kind !== 'TermDecl')