@karmaniverous/jeeves-meta 0.3.0 → 0.3.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.
package/dist/cli.js CHANGED
@@ -386,6 +386,10 @@ function buildMetaFilter(config) {
386
386
  key: 'domains',
387
387
  match: { value: config.metaProperty.domains[0] },
388
388
  },
389
+ {
390
+ key: 'file_path',
391
+ match: { text: 'meta.json' },
392
+ },
389
393
  ],
390
394
  };
391
395
  }
@@ -405,21 +409,95 @@ async function discoverMetas(config, watcher) {
405
409
  filter,
406
410
  fields: ['file_path'],
407
411
  });
408
- // Deduplicate by file_path (multi-chunk files)
412
+ // Deduplicate by .meta/ directory path (handles multi-chunk files)
409
413
  const seen = new Set();
410
414
  const metaPaths = [];
411
415
  for (const sf of scanFiles) {
412
416
  const fp = normalizePath$1(sf.file_path);
413
- if (seen.has(fp))
414
- continue;
415
- seen.add(fp);
416
417
  // Derive .meta/ directory from file_path (strip /meta.json)
417
418
  const metaPath = fp.replace(/\/meta\.json$/, '');
419
+ if (seen.has(metaPath))
420
+ continue;
421
+ seen.add(metaPath);
418
422
  metaPaths.push(metaPath);
419
423
  }
420
424
  return metaPaths;
421
425
  }
422
426
 
427
+ /**
428
+ * File-system lock for preventing concurrent synthesis on the same meta.
429
+ *
430
+ * Lock file: .meta/.lock containing PID + timestamp.
431
+ * Stale timeout: 30 minutes.
432
+ *
433
+ * @module lock
434
+ */
435
+ const LOCK_FILE = '.lock';
436
+ const STALE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
437
+ /**
438
+ * Attempt to acquire a lock on a .meta directory.
439
+ *
440
+ * @param metaPath - Absolute path to the .meta directory.
441
+ * @returns True if lock was acquired, false if already locked (non-stale).
442
+ */
443
+ function acquireLock(metaPath) {
444
+ const lockPath = join(metaPath, LOCK_FILE);
445
+ if (existsSync(lockPath)) {
446
+ try {
447
+ const raw = readFileSync(lockPath, 'utf8');
448
+ const data = JSON.parse(raw);
449
+ const lockAge = Date.now() - new Date(data.startedAt).getTime();
450
+ if (lockAge < STALE_TIMEOUT_MS) {
451
+ return false; // Lock is active
452
+ }
453
+ // Stale lock — fall through to overwrite
454
+ }
455
+ catch {
456
+ // Corrupt lock file — overwrite
457
+ }
458
+ }
459
+ const lock = {
460
+ pid: process.pid,
461
+ startedAt: new Date().toISOString(),
462
+ };
463
+ writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
464
+ return true;
465
+ }
466
+ /**
467
+ * Release a lock on a .meta directory.
468
+ *
469
+ * @param metaPath - Absolute path to the .meta directory.
470
+ */
471
+ function releaseLock(metaPath) {
472
+ const lockPath = join(metaPath, LOCK_FILE);
473
+ try {
474
+ unlinkSync(lockPath);
475
+ }
476
+ catch {
477
+ // Already removed or never existed
478
+ }
479
+ }
480
+ /**
481
+ * Check if a .meta directory is currently locked (non-stale).
482
+ *
483
+ * @param metaPath - Absolute path to the .meta directory.
484
+ * @returns True if locked and not stale.
485
+ */
486
+ function isLocked(metaPath) {
487
+ const lockPath = join(metaPath, LOCK_FILE);
488
+ if (!existsSync(lockPath))
489
+ return false;
490
+ try {
491
+ const raw = readFileSync(lockPath, 'utf8');
492
+ const data = JSON.parse(raw);
493
+ const lockAge = Date.now() - new Date(data.startedAt).getTime();
494
+ return lockAge < STALE_TIMEOUT_MS;
495
+ }
496
+ catch {
497
+ return false; // Corrupt lock = not locked
498
+ }
499
+ }
500
+
423
501
  /**
424
502
  * Build the ownership tree from discovered .meta/ paths.
425
503
  *
@@ -497,6 +575,138 @@ function findNode(tree, targetPath) {
497
575
  return Array.from(tree.nodes.values()).find((n) => n.metaPath === targetPath || n.ownerPath === targetPath);
498
576
  }
499
577
 
578
+ /**
579
+ * Unified meta listing: scan, dedup, enrich.
580
+ *
581
+ * Single source of truth for all consumers that need a list of metas
582
+ * with enriched metadata. Replaces duplicated scan+dedup logic in
583
+ * plugin tools, CLI, and prompt injection.
584
+ *
585
+ * @module discovery/listMetas
586
+ */
587
+ /**
588
+ * Discover, deduplicate, and enrich all metas.
589
+ *
590
+ * This is the single consolidated function that replaces all duplicated
591
+ * scan+dedup+enrich logic across the codebase. All enrichment comes from
592
+ * reading meta.json on disk (the canonical source).
593
+ *
594
+ * @param config - Validated synthesis config.
595
+ * @param watcher - Watcher HTTP client for discovery.
596
+ * @returns Enriched meta list with summary statistics and ownership tree.
597
+ */
598
+ async function listMetas(config, watcher) {
599
+ // Step 1: Discover deduplicated meta paths via watcher scan
600
+ const metaPaths = await discoverMetas(config, watcher);
601
+ // Step 2: Build ownership tree
602
+ const tree = buildOwnershipTree(metaPaths);
603
+ // Step 3: Read and enrich each meta from disk
604
+ const entries = [];
605
+ let staleCount = 0;
606
+ let errorCount = 0;
607
+ let lockedCount = 0;
608
+ let neverSynthesizedCount = 0;
609
+ let totalArchTokens = 0;
610
+ let totalBuilderTokens = 0;
611
+ let totalCriticTokens = 0;
612
+ let lastSynthPath = null;
613
+ let lastSynthAt = null;
614
+ let stalestPath = null;
615
+ let stalestEffective = -1;
616
+ for (const node of tree.nodes.values()) {
617
+ let meta;
618
+ try {
619
+ meta = JSON.parse(readFileSync(join(node.metaPath, 'meta.json'), 'utf8'));
620
+ }
621
+ catch {
622
+ // Skip unreadable metas
623
+ continue;
624
+ }
625
+ const depth = meta._depth ?? node.treeDepth;
626
+ const emphasis = meta._emphasis ?? 1;
627
+ const hasError = Boolean(meta._error);
628
+ const locked = isLocked(normalizePath$1(node.metaPath));
629
+ const neverSynth = !meta._generatedAt;
630
+ // Compute staleness
631
+ let stalenessSeconds;
632
+ if (neverSynth) {
633
+ stalenessSeconds = Infinity;
634
+ }
635
+ else {
636
+ const genAt = new Date(meta._generatedAt).getTime();
637
+ stalenessSeconds = Math.max(0, Math.floor((Date.now() - genAt) / 1000));
638
+ }
639
+ // Tokens
640
+ const archTokens = meta._architectTokens ?? 0;
641
+ const buildTokens = meta._builderTokens ?? 0;
642
+ const critTokens = meta._criticTokens ?? 0;
643
+ // Accumulate summary stats
644
+ if (stalenessSeconds > 0)
645
+ staleCount++;
646
+ if (hasError)
647
+ errorCount++;
648
+ if (locked)
649
+ lockedCount++;
650
+ if (neverSynth)
651
+ neverSynthesizedCount++;
652
+ totalArchTokens += archTokens;
653
+ totalBuilderTokens += buildTokens;
654
+ totalCriticTokens += critTokens;
655
+ // Track last synthesized
656
+ if (meta._generatedAt) {
657
+ if (!lastSynthAt || meta._generatedAt > lastSynthAt) {
658
+ lastSynthAt = meta._generatedAt;
659
+ lastSynthPath = node.metaPath;
660
+ }
661
+ }
662
+ // Track stalest (effective staleness for scheduling)
663
+ const depthFactor = Math.pow(1 + config.depthWeight, depth);
664
+ const effectiveStaleness = (stalenessSeconds === Infinity
665
+ ? Number.MAX_SAFE_INTEGER
666
+ : stalenessSeconds) *
667
+ depthFactor *
668
+ emphasis;
669
+ if (effectiveStaleness > stalestEffective) {
670
+ stalestEffective = effectiveStaleness;
671
+ stalestPath = node.metaPath;
672
+ }
673
+ entries.push({
674
+ path: node.metaPath,
675
+ depth,
676
+ emphasis,
677
+ stalenessSeconds,
678
+ lastSynthesized: meta._generatedAt ?? null,
679
+ hasError,
680
+ locked,
681
+ architectTokens: archTokens > 0 ? archTokens : null,
682
+ builderTokens: buildTokens > 0 ? buildTokens : null,
683
+ criticTokens: critTokens > 0 ? critTokens : null,
684
+ children: node.children.length,
685
+ node,
686
+ meta,
687
+ });
688
+ }
689
+ return {
690
+ summary: {
691
+ total: entries.length,
692
+ stale: staleCount,
693
+ errors: errorCount,
694
+ locked: lockedCount,
695
+ neverSynthesized: neverSynthesizedCount,
696
+ tokens: {
697
+ architect: totalArchTokens,
698
+ builder: totalBuilderTokens,
699
+ critic: totalCriticTokens,
700
+ },
701
+ stalestPath,
702
+ lastSynthesizedPath: lastSynthPath,
703
+ lastSynthesizedAt: lastSynthAt,
704
+ },
705
+ entries,
706
+ tree,
707
+ };
708
+ }
709
+
500
710
  /**
501
711
  * Compute the file scope owned by a meta node.
502
712
  *
@@ -690,80 +900,6 @@ class GatewayExecutor {
690
900
  }
691
901
  }
692
902
 
693
- /**
694
- * File-system lock for preventing concurrent synthesis on the same meta.
695
- *
696
- * Lock file: .meta/.lock containing PID + timestamp.
697
- * Stale timeout: 30 minutes.
698
- *
699
- * @module lock
700
- */
701
- const LOCK_FILE = '.lock';
702
- const STALE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
703
- /**
704
- * Attempt to acquire a lock on a .meta directory.
705
- *
706
- * @param metaPath - Absolute path to the .meta directory.
707
- * @returns True if lock was acquired, false if already locked (non-stale).
708
- */
709
- function acquireLock(metaPath) {
710
- const lockPath = join(metaPath, LOCK_FILE);
711
- if (existsSync(lockPath)) {
712
- try {
713
- const raw = readFileSync(lockPath, 'utf8');
714
- const data = JSON.parse(raw);
715
- const lockAge = Date.now() - new Date(data.startedAt).getTime();
716
- if (lockAge < STALE_TIMEOUT_MS) {
717
- return false; // Lock is active
718
- }
719
- // Stale lock — fall through to overwrite
720
- }
721
- catch {
722
- // Corrupt lock file — overwrite
723
- }
724
- }
725
- const lock = {
726
- pid: process.pid,
727
- startedAt: new Date().toISOString(),
728
- };
729
- writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
730
- return true;
731
- }
732
- /**
733
- * Release a lock on a .meta directory.
734
- *
735
- * @param metaPath - Absolute path to the .meta directory.
736
- */
737
- function releaseLock(metaPath) {
738
- const lockPath = join(metaPath, LOCK_FILE);
739
- try {
740
- unlinkSync(lockPath);
741
- }
742
- catch {
743
- // Already removed or never existed
744
- }
745
- }
746
- /**
747
- * Check if a .meta directory is currently locked (non-stale).
748
- *
749
- * @param metaPath - Absolute path to the .meta directory.
750
- * @returns True if locked and not stale.
751
- */
752
- function isLocked(metaPath) {
753
- const lockPath = join(metaPath, LOCK_FILE);
754
- if (!existsSync(lockPath))
755
- return false;
756
- try {
757
- const raw = readFileSync(lockPath, 'utf8');
758
- const data = JSON.parse(raw);
759
- const lockAge = Date.now() - new Date(data.startedAt).getTime();
760
- return lockAge < STALE_TIMEOUT_MS;
761
- }
762
- catch {
763
- return false; // Corrupt lock = not locked
764
- }
765
- }
766
-
767
903
  /**
768
904
  * Build the SynthContext for a synthesis cycle.
769
905
  *
@@ -1557,16 +1693,31 @@ class HttpWatcherClient {
1557
1693
  throw new Error('Retry exhausted');
1558
1694
  }
1559
1695
  async scan(params) {
1560
- const body = {};
1561
- if (params.pathPrefix !== undefined) {
1562
- body.pathPrefix = params.pathPrefix;
1696
+ // Build Qdrant filter: merge explicit filter with pathPrefix/modifiedAfter
1697
+ const mustClauses = [];
1698
+ // Carry over any existing 'must' clauses from the provided filter
1699
+ if (params.filter) {
1700
+ const existing = params.filter.must;
1701
+ if (Array.isArray(existing)) {
1702
+ mustClauses.push(...existing);
1703
+ }
1563
1704
  }
1564
- if (params.filter !== undefined) {
1565
- body.filter = params.filter;
1705
+ // Translate pathPrefix into a Qdrant text match on file_path
1706
+ if (params.pathPrefix !== undefined) {
1707
+ mustClauses.push({
1708
+ key: 'file_path',
1709
+ match: { text: params.pathPrefix },
1710
+ });
1566
1711
  }
1712
+ // Translate modifiedAfter into a Qdrant range filter on modified_at
1567
1713
  if (params.modifiedAfter !== undefined) {
1568
- body.modifiedAfter = params.modifiedAfter;
1714
+ mustClauses.push({
1715
+ key: 'modified_at',
1716
+ range: { gt: params.modifiedAfter },
1717
+ });
1569
1718
  }
1719
+ const filter = { must: mustClauses };
1720
+ const body = { filter };
1570
1721
  if (params.fields !== undefined) {
1571
1722
  body.fields = params.fields;
1572
1723
  }
@@ -1630,6 +1781,7 @@ var index = /*#__PURE__*/Object.freeze({
1630
1781
  isLocked: isLocked,
1631
1782
  isStale: isStale,
1632
1783
  listArchiveFiles: listArchiveFiles,
1784
+ listMetas: listMetas,
1633
1785
  loadSynthConfig: loadSynthConfig,
1634
1786
  mergeAndWrite: mergeAndWrite,
1635
1787
  metaJsonSchema: metaJsonSchema,
@@ -1700,85 +1852,36 @@ function output(data) {
1700
1852
  }
1701
1853
  async function runStatus(config) {
1702
1854
  const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
1703
- const metaPaths = await discoverMetas(config, watcher);
1704
- const tree = buildOwnershipTree(metaPaths);
1705
- let stale = 0;
1706
- let errors = 0;
1707
- let locked = 0;
1708
- let neverSynth = 0;
1709
- let archTokens = 0;
1710
- let buildTokens = 0;
1711
- let critTokens = 0;
1712
- for (const node of tree.nodes.values()) {
1713
- let meta;
1714
- try {
1715
- meta = readMeta(node.metaPath);
1716
- }
1717
- catch {
1718
- continue;
1719
- }
1720
- const s = actualStaleness(meta);
1721
- if (s > 0)
1722
- stale++;
1723
- if (meta._error)
1724
- errors++;
1725
- if (isLocked(normalizePath$1(node.metaPath)))
1726
- locked++;
1727
- if (!meta._generatedAt)
1728
- neverSynth++;
1729
- if (meta._architectTokens)
1730
- archTokens += meta._architectTokens;
1731
- if (meta._builderTokens)
1732
- buildTokens += meta._builderTokens;
1733
- if (meta._criticTokens)
1734
- critTokens += meta._criticTokens;
1735
- }
1736
- output({
1737
- total: tree.nodes.size,
1738
- stale,
1739
- errors,
1740
- locked,
1741
- neverSynthesized: neverSynth,
1742
- tokens: { architect: archTokens, builder: buildTokens, critic: critTokens },
1743
- });
1855
+ const result = await listMetas(config, watcher);
1856
+ output(result.summary);
1744
1857
  }
1745
1858
  async function runList(config) {
1746
1859
  const prefix = getArg('--prefix');
1747
1860
  const filter = getArg('--filter');
1748
1861
  const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
1749
- const metaPaths = await discoverMetas(config, watcher);
1750
- const tree = buildOwnershipTree(metaPaths);
1751
- const rows = [];
1752
- for (const node of tree.nodes.values()) {
1753
- if (prefix && !node.metaPath.includes(prefix))
1754
- continue;
1755
- let meta;
1756
- try {
1757
- meta = readMeta(node.metaPath);
1758
- }
1759
- catch {
1760
- continue;
1761
- }
1762
- const s = actualStaleness(meta);
1763
- const hasError = Boolean(meta._error);
1764
- const isLockedNow = isLocked(normalizePath$1(node.metaPath));
1765
- if (filter === 'hasError' && !hasError)
1766
- continue;
1767
- if (filter === 'stale' && s <= 0)
1768
- continue;
1769
- if (filter === 'locked' && !isLockedNow)
1770
- continue;
1771
- if (filter === 'never' && meta._generatedAt)
1772
- continue;
1773
- rows.push({
1774
- path: node.metaPath,
1775
- depth: meta._depth ?? node.treeDepth,
1776
- staleness: s === Infinity ? 'never' : String(Math.round(s)) + 's',
1777
- hasError,
1778
- locked: isLockedNow,
1779
- children: node.children.length,
1780
- });
1781
- }
1862
+ const result = await listMetas(config, watcher);
1863
+ let entries = result.entries;
1864
+ if (prefix) {
1865
+ entries = entries.filter((e) => e.path.includes(prefix));
1866
+ }
1867
+ if (filter === 'hasError')
1868
+ entries = entries.filter((e) => e.hasError);
1869
+ if (filter === 'stale')
1870
+ entries = entries.filter((e) => e.stalenessSeconds > 0);
1871
+ if (filter === 'locked')
1872
+ entries = entries.filter((e) => e.locked);
1873
+ if (filter === 'never')
1874
+ entries = entries.filter((e) => e.stalenessSeconds === Infinity);
1875
+ const rows = entries.map((e) => ({
1876
+ path: e.path,
1877
+ depth: e.depth,
1878
+ staleness: e.stalenessSeconds === Infinity
1879
+ ? 'never'
1880
+ : String(Math.round(e.stalenessSeconds)) + 's',
1881
+ hasError: e.hasError,
1882
+ locked: e.locked,
1883
+ children: e.children,
1884
+ }));
1782
1885
  output({ total: rows.length, items: rows });
1783
1886
  }
1784
1887
  async function runDetail(config) {
@@ -1789,10 +1892,9 @@ async function runDetail(config) {
1789
1892
  }
1790
1893
  const archiveArg = getArg('--archive');
1791
1894
  const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
1792
- const metaPaths = await discoverMetas(config, watcher);
1793
- const tree = buildOwnershipTree(metaPaths);
1895
+ const metaResult = await listMetas(config, watcher);
1794
1896
  const normalized = normalizePath$1(targetPath);
1795
- const node = findNode(tree, normalized);
1897
+ const node = findNode(metaResult.tree, normalized);
1796
1898
  if (!node) {
1797
1899
  console.error('Meta not found: ' + targetPath);
1798
1900
  process.exit(1);
@@ -1813,33 +1915,26 @@ async function runDetail(config) {
1813
1915
  }
1814
1916
  async function runPreview(config) {
1815
1917
  const targetPath = getArg('--path');
1816
- const { filterInScope, paginatedScan, readLatestArchive, computeStructureHash, selectCandidate, } = await Promise.resolve().then(function () { return index; });
1918
+ const { filterInScope, paginatedScan, readLatestArchive, computeStructureHash, } = await Promise.resolve().then(function () { return index; });
1817
1919
  const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
1818
- const metaPaths = await discoverMetas(config, watcher);
1819
- const tree = buildOwnershipTree(metaPaths);
1920
+ const metaResult = await listMetas(config, watcher);
1820
1921
  let targetNode;
1821
1922
  if (targetPath) {
1822
1923
  const normalized = normalizePath$1(targetPath);
1823
- targetNode = findNode(tree, normalized);
1924
+ targetNode = findNode(metaResult.tree, normalized);
1824
1925
  if (!targetNode) {
1825
1926
  console.error('Meta not found: ' + targetPath);
1826
1927
  process.exit(1);
1827
1928
  }
1828
1929
  }
1829
1930
  else {
1830
- const candidates = [];
1831
- for (const node of tree.nodes.values()) {
1832
- let meta;
1833
- try {
1834
- meta = readMeta(node.metaPath);
1835
- }
1836
- catch {
1837
- continue;
1838
- }
1839
- const s = actualStaleness(meta);
1840
- if (s > 0)
1841
- candidates.push({ node, meta, actualStaleness: s });
1842
- }
1931
+ const candidates = metaResult.entries
1932
+ .filter((e) => e.stalenessSeconds > 0)
1933
+ .map((e) => ({
1934
+ node: e.node,
1935
+ meta: e.meta,
1936
+ actualStaleness: e.stalenessSeconds,
1937
+ }));
1843
1938
  const weighted = computeEffectiveStaleness(candidates, config.depthWeight);
1844
1939
  const winner = selectCandidate(weighted);
1845
1940
  if (!winner) {
package/dist/index.d.ts CHANGED
@@ -389,6 +389,80 @@ interface OwnershipTree {
389
389
  roots: MetaNode[];
390
390
  }
391
391
 
392
+ /**
393
+ * Unified meta listing: scan, dedup, enrich.
394
+ *
395
+ * Single source of truth for all consumers that need a list of metas
396
+ * with enriched metadata. Replaces duplicated scan+dedup logic in
397
+ * plugin tools, CLI, and prompt injection.
398
+ *
399
+ * @module discovery/listMetas
400
+ */
401
+
402
+ /** Enriched meta entry returned by listMetas(). */
403
+ interface MetaEntry {
404
+ /** Normalized .meta/ directory path. */
405
+ path: string;
406
+ /** Tree depth (0 = leaf, higher = more abstract). */
407
+ depth: number;
408
+ /** Scheduling emphasis multiplier. */
409
+ emphasis: number;
410
+ /** Seconds since last synthesis, or Infinity if never synthesized. */
411
+ stalenessSeconds: number;
412
+ /** ISO timestamp of last synthesis, or null. */
413
+ lastSynthesized: string | null;
414
+ /** Whether the last synthesis had an error. */
415
+ hasError: boolean;
416
+ /** Whether this meta is currently locked. */
417
+ locked: boolean;
418
+ /** Cumulative architect tokens, or null if never run. */
419
+ architectTokens: number | null;
420
+ /** Cumulative builder tokens, or null if never run. */
421
+ builderTokens: number | null;
422
+ /** Cumulative critic tokens, or null if never run. */
423
+ criticTokens: number | null;
424
+ /** Number of direct children in the ownership tree. */
425
+ children: number;
426
+ /** The underlying MetaNode from the ownership tree. */
427
+ node: MetaNode;
428
+ /** The parsed meta.json content. */
429
+ meta: MetaJson;
430
+ }
431
+ /** Summary statistics computed from the meta list. */
432
+ interface MetaListSummary {
433
+ total: number;
434
+ stale: number;
435
+ errors: number;
436
+ locked: number;
437
+ neverSynthesized: number;
438
+ tokens: {
439
+ architect: number;
440
+ builder: number;
441
+ critic: number;
442
+ };
443
+ stalestPath: string | null;
444
+ lastSynthesizedPath: string | null;
445
+ lastSynthesizedAt: string | null;
446
+ }
447
+ /** Full result from listMetas(). */
448
+ interface MetaListResult {
449
+ summary: MetaListSummary;
450
+ entries: MetaEntry[];
451
+ tree: OwnershipTree;
452
+ }
453
+ /**
454
+ * Discover, deduplicate, and enrich all metas.
455
+ *
456
+ * This is the single consolidated function that replaces all duplicated
457
+ * scan+dedup+enrich logic across the codebase. All enrichment comes from
458
+ * reading meta.json on disk (the canonical source).
459
+ *
460
+ * @param config - Validated synthesis config.
461
+ * @param watcher - Watcher HTTP client for discovery.
462
+ * @returns Enriched meta list with summary statistics and ownership tree.
463
+ */
464
+ declare function listMetas(config: SynthConfig, watcher: WatcherClient): Promise<MetaListResult>;
465
+
392
466
  /**
393
467
  * Build the ownership tree from discovered .meta/ paths.
394
468
  *
@@ -895,5 +969,5 @@ declare class HttpWatcherClient implements WatcherClient {
895
969
  unregisterRules(source: string): Promise<void>;
896
970
  }
897
971
 
898
- export { GatewayExecutor, HttpWatcherClient, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildMetaFilter, buildOwnershipTree, computeEffectiveStaleness, computeEma, computeStructureHash, createSnapshot, discoverMetas, filterInScope, findNode, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, loadSynthConfig, mergeAndWrite, metaJsonSchema, normalizePath, orchestrate, paginatedScan, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, releaseLock, resolveConfigPath, selectCandidate, synthConfigSchema, synthErrorSchema, toSynthError };
899
- export type { BuilderOutput, GatewayExecutorOptions, HttpWatcherClientOptions, InferenceRuleSpec, MergeOptions, MetaJson, MetaNode, OrchestrateResult, OwnershipTree, ScanFile, ScanParams, ScanResponse, StalenessCandidate, SynthConfig, SynthContext, SynthError, SynthExecutor, SynthSpawnOptions, SynthSpawnResult, WatcherClient };
972
+ export { GatewayExecutor, HttpWatcherClient, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildMetaFilter, buildOwnershipTree, computeEffectiveStaleness, computeEma, computeStructureHash, createSnapshot, discoverMetas, filterInScope, findNode, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, listMetas, loadSynthConfig, mergeAndWrite, metaJsonSchema, normalizePath, orchestrate, paginatedScan, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, releaseLock, resolveConfigPath, selectCandidate, synthConfigSchema, synthErrorSchema, toSynthError };
973
+ export type { BuilderOutput, GatewayExecutorOptions, HttpWatcherClientOptions, InferenceRuleSpec, MergeOptions, MetaEntry, MetaJson, MetaListResult, MetaListSummary, MetaNode, OrchestrateResult, OwnershipTree, ScanFile, ScanParams, ScanResponse, StalenessCandidate, SynthConfig, SynthContext, SynthError, SynthExecutor, SynthSpawnOptions, SynthSpawnResult, WatcherClient };
package/dist/index.js CHANGED
@@ -371,6 +371,10 @@ function buildMetaFilter(config) {
371
371
  key: 'domains',
372
372
  match: { value: config.metaProperty.domains[0] },
373
373
  },
374
+ {
375
+ key: 'file_path',
376
+ match: { text: 'meta.json' },
377
+ },
374
378
  ],
375
379
  };
376
380
  }
@@ -390,21 +394,95 @@ async function discoverMetas(config, watcher) {
390
394
  filter,
391
395
  fields: ['file_path'],
392
396
  });
393
- // Deduplicate by file_path (multi-chunk files)
397
+ // Deduplicate by .meta/ directory path (handles multi-chunk files)
394
398
  const seen = new Set();
395
399
  const metaPaths = [];
396
400
  for (const sf of scanFiles) {
397
401
  const fp = normalizePath$1(sf.file_path);
398
- if (seen.has(fp))
399
- continue;
400
- seen.add(fp);
401
402
  // Derive .meta/ directory from file_path (strip /meta.json)
402
403
  const metaPath = fp.replace(/\/meta\.json$/, '');
404
+ if (seen.has(metaPath))
405
+ continue;
406
+ seen.add(metaPath);
403
407
  metaPaths.push(metaPath);
404
408
  }
405
409
  return metaPaths;
406
410
  }
407
411
 
412
+ /**
413
+ * File-system lock for preventing concurrent synthesis on the same meta.
414
+ *
415
+ * Lock file: .meta/.lock containing PID + timestamp.
416
+ * Stale timeout: 30 minutes.
417
+ *
418
+ * @module lock
419
+ */
420
+ const LOCK_FILE = '.lock';
421
+ const STALE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
422
+ /**
423
+ * Attempt to acquire a lock on a .meta directory.
424
+ *
425
+ * @param metaPath - Absolute path to the .meta directory.
426
+ * @returns True if lock was acquired, false if already locked (non-stale).
427
+ */
428
+ function acquireLock(metaPath) {
429
+ const lockPath = join(metaPath, LOCK_FILE);
430
+ if (existsSync(lockPath)) {
431
+ try {
432
+ const raw = readFileSync(lockPath, 'utf8');
433
+ const data = JSON.parse(raw);
434
+ const lockAge = Date.now() - new Date(data.startedAt).getTime();
435
+ if (lockAge < STALE_TIMEOUT_MS) {
436
+ return false; // Lock is active
437
+ }
438
+ // Stale lock — fall through to overwrite
439
+ }
440
+ catch {
441
+ // Corrupt lock file — overwrite
442
+ }
443
+ }
444
+ const lock = {
445
+ pid: process.pid,
446
+ startedAt: new Date().toISOString(),
447
+ };
448
+ writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
449
+ return true;
450
+ }
451
+ /**
452
+ * Release a lock on a .meta directory.
453
+ *
454
+ * @param metaPath - Absolute path to the .meta directory.
455
+ */
456
+ function releaseLock(metaPath) {
457
+ const lockPath = join(metaPath, LOCK_FILE);
458
+ try {
459
+ unlinkSync(lockPath);
460
+ }
461
+ catch {
462
+ // Already removed or never existed
463
+ }
464
+ }
465
+ /**
466
+ * Check if a .meta directory is currently locked (non-stale).
467
+ *
468
+ * @param metaPath - Absolute path to the .meta directory.
469
+ * @returns True if locked and not stale.
470
+ */
471
+ function isLocked(metaPath) {
472
+ const lockPath = join(metaPath, LOCK_FILE);
473
+ if (!existsSync(lockPath))
474
+ return false;
475
+ try {
476
+ const raw = readFileSync(lockPath, 'utf8');
477
+ const data = JSON.parse(raw);
478
+ const lockAge = Date.now() - new Date(data.startedAt).getTime();
479
+ return lockAge < STALE_TIMEOUT_MS;
480
+ }
481
+ catch {
482
+ return false; // Corrupt lock = not locked
483
+ }
484
+ }
485
+
408
486
  /**
409
487
  * Build the ownership tree from discovered .meta/ paths.
410
488
  *
@@ -482,6 +560,138 @@ function findNode(tree, targetPath) {
482
560
  return Array.from(tree.nodes.values()).find((n) => n.metaPath === targetPath || n.ownerPath === targetPath);
483
561
  }
484
562
 
563
+ /**
564
+ * Unified meta listing: scan, dedup, enrich.
565
+ *
566
+ * Single source of truth for all consumers that need a list of metas
567
+ * with enriched metadata. Replaces duplicated scan+dedup logic in
568
+ * plugin tools, CLI, and prompt injection.
569
+ *
570
+ * @module discovery/listMetas
571
+ */
572
+ /**
573
+ * Discover, deduplicate, and enrich all metas.
574
+ *
575
+ * This is the single consolidated function that replaces all duplicated
576
+ * scan+dedup+enrich logic across the codebase. All enrichment comes from
577
+ * reading meta.json on disk (the canonical source).
578
+ *
579
+ * @param config - Validated synthesis config.
580
+ * @param watcher - Watcher HTTP client for discovery.
581
+ * @returns Enriched meta list with summary statistics and ownership tree.
582
+ */
583
+ async function listMetas(config, watcher) {
584
+ // Step 1: Discover deduplicated meta paths via watcher scan
585
+ const metaPaths = await discoverMetas(config, watcher);
586
+ // Step 2: Build ownership tree
587
+ const tree = buildOwnershipTree(metaPaths);
588
+ // Step 3: Read and enrich each meta from disk
589
+ const entries = [];
590
+ let staleCount = 0;
591
+ let errorCount = 0;
592
+ let lockedCount = 0;
593
+ let neverSynthesizedCount = 0;
594
+ let totalArchTokens = 0;
595
+ let totalBuilderTokens = 0;
596
+ let totalCriticTokens = 0;
597
+ let lastSynthPath = null;
598
+ let lastSynthAt = null;
599
+ let stalestPath = null;
600
+ let stalestEffective = -1;
601
+ for (const node of tree.nodes.values()) {
602
+ let meta;
603
+ try {
604
+ meta = JSON.parse(readFileSync(join(node.metaPath, 'meta.json'), 'utf8'));
605
+ }
606
+ catch {
607
+ // Skip unreadable metas
608
+ continue;
609
+ }
610
+ const depth = meta._depth ?? node.treeDepth;
611
+ const emphasis = meta._emphasis ?? 1;
612
+ const hasError = Boolean(meta._error);
613
+ const locked = isLocked(normalizePath$1(node.metaPath));
614
+ const neverSynth = !meta._generatedAt;
615
+ // Compute staleness
616
+ let stalenessSeconds;
617
+ if (neverSynth) {
618
+ stalenessSeconds = Infinity;
619
+ }
620
+ else {
621
+ const genAt = new Date(meta._generatedAt).getTime();
622
+ stalenessSeconds = Math.max(0, Math.floor((Date.now() - genAt) / 1000));
623
+ }
624
+ // Tokens
625
+ const archTokens = meta._architectTokens ?? 0;
626
+ const buildTokens = meta._builderTokens ?? 0;
627
+ const critTokens = meta._criticTokens ?? 0;
628
+ // Accumulate summary stats
629
+ if (stalenessSeconds > 0)
630
+ staleCount++;
631
+ if (hasError)
632
+ errorCount++;
633
+ if (locked)
634
+ lockedCount++;
635
+ if (neverSynth)
636
+ neverSynthesizedCount++;
637
+ totalArchTokens += archTokens;
638
+ totalBuilderTokens += buildTokens;
639
+ totalCriticTokens += critTokens;
640
+ // Track last synthesized
641
+ if (meta._generatedAt) {
642
+ if (!lastSynthAt || meta._generatedAt > lastSynthAt) {
643
+ lastSynthAt = meta._generatedAt;
644
+ lastSynthPath = node.metaPath;
645
+ }
646
+ }
647
+ // Track stalest (effective staleness for scheduling)
648
+ const depthFactor = Math.pow(1 + config.depthWeight, depth);
649
+ const effectiveStaleness = (stalenessSeconds === Infinity
650
+ ? Number.MAX_SAFE_INTEGER
651
+ : stalenessSeconds) *
652
+ depthFactor *
653
+ emphasis;
654
+ if (effectiveStaleness > stalestEffective) {
655
+ stalestEffective = effectiveStaleness;
656
+ stalestPath = node.metaPath;
657
+ }
658
+ entries.push({
659
+ path: node.metaPath,
660
+ depth,
661
+ emphasis,
662
+ stalenessSeconds,
663
+ lastSynthesized: meta._generatedAt ?? null,
664
+ hasError,
665
+ locked,
666
+ architectTokens: archTokens > 0 ? archTokens : null,
667
+ builderTokens: buildTokens > 0 ? buildTokens : null,
668
+ criticTokens: critTokens > 0 ? critTokens : null,
669
+ children: node.children.length,
670
+ node,
671
+ meta,
672
+ });
673
+ }
674
+ return {
675
+ summary: {
676
+ total: entries.length,
677
+ stale: staleCount,
678
+ errors: errorCount,
679
+ locked: lockedCount,
680
+ neverSynthesized: neverSynthesizedCount,
681
+ tokens: {
682
+ architect: totalArchTokens,
683
+ builder: totalBuilderTokens,
684
+ critic: totalCriticTokens,
685
+ },
686
+ stalestPath,
687
+ lastSynthesizedPath: lastSynthPath,
688
+ lastSynthesizedAt: lastSynthAt,
689
+ },
690
+ entries,
691
+ tree,
692
+ };
693
+ }
694
+
485
695
  /**
486
696
  * Compute the file scope owned by a meta node.
487
697
  *
@@ -675,80 +885,6 @@ class GatewayExecutor {
675
885
  }
676
886
  }
677
887
 
678
- /**
679
- * File-system lock for preventing concurrent synthesis on the same meta.
680
- *
681
- * Lock file: .meta/.lock containing PID + timestamp.
682
- * Stale timeout: 30 minutes.
683
- *
684
- * @module lock
685
- */
686
- const LOCK_FILE = '.lock';
687
- const STALE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
688
- /**
689
- * Attempt to acquire a lock on a .meta directory.
690
- *
691
- * @param metaPath - Absolute path to the .meta directory.
692
- * @returns True if lock was acquired, false if already locked (non-stale).
693
- */
694
- function acquireLock(metaPath) {
695
- const lockPath = join(metaPath, LOCK_FILE);
696
- if (existsSync(lockPath)) {
697
- try {
698
- const raw = readFileSync(lockPath, 'utf8');
699
- const data = JSON.parse(raw);
700
- const lockAge = Date.now() - new Date(data.startedAt).getTime();
701
- if (lockAge < STALE_TIMEOUT_MS) {
702
- return false; // Lock is active
703
- }
704
- // Stale lock — fall through to overwrite
705
- }
706
- catch {
707
- // Corrupt lock file — overwrite
708
- }
709
- }
710
- const lock = {
711
- pid: process.pid,
712
- startedAt: new Date().toISOString(),
713
- };
714
- writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
715
- return true;
716
- }
717
- /**
718
- * Release a lock on a .meta directory.
719
- *
720
- * @param metaPath - Absolute path to the .meta directory.
721
- */
722
- function releaseLock(metaPath) {
723
- const lockPath = join(metaPath, LOCK_FILE);
724
- try {
725
- unlinkSync(lockPath);
726
- }
727
- catch {
728
- // Already removed or never existed
729
- }
730
- }
731
- /**
732
- * Check if a .meta directory is currently locked (non-stale).
733
- *
734
- * @param metaPath - Absolute path to the .meta directory.
735
- * @returns True if locked and not stale.
736
- */
737
- function isLocked(metaPath) {
738
- const lockPath = join(metaPath, LOCK_FILE);
739
- if (!existsSync(lockPath))
740
- return false;
741
- try {
742
- const raw = readFileSync(lockPath, 'utf8');
743
- const data = JSON.parse(raw);
744
- const lockAge = Date.now() - new Date(data.startedAt).getTime();
745
- return lockAge < STALE_TIMEOUT_MS;
746
- }
747
- catch {
748
- return false; // Corrupt lock = not locked
749
- }
750
- }
751
-
752
888
  /**
753
889
  * Build the SynthContext for a synthesis cycle.
754
890
  *
@@ -1542,16 +1678,31 @@ class HttpWatcherClient {
1542
1678
  throw new Error('Retry exhausted');
1543
1679
  }
1544
1680
  async scan(params) {
1545
- const body = {};
1546
- if (params.pathPrefix !== undefined) {
1547
- body.pathPrefix = params.pathPrefix;
1681
+ // Build Qdrant filter: merge explicit filter with pathPrefix/modifiedAfter
1682
+ const mustClauses = [];
1683
+ // Carry over any existing 'must' clauses from the provided filter
1684
+ if (params.filter) {
1685
+ const existing = params.filter.must;
1686
+ if (Array.isArray(existing)) {
1687
+ mustClauses.push(...existing);
1688
+ }
1548
1689
  }
1549
- if (params.filter !== undefined) {
1550
- body.filter = params.filter;
1690
+ // Translate pathPrefix into a Qdrant text match on file_path
1691
+ if (params.pathPrefix !== undefined) {
1692
+ mustClauses.push({
1693
+ key: 'file_path',
1694
+ match: { text: params.pathPrefix },
1695
+ });
1551
1696
  }
1697
+ // Translate modifiedAfter into a Qdrant range filter on modified_at
1552
1698
  if (params.modifiedAfter !== undefined) {
1553
- body.modifiedAfter = params.modifiedAfter;
1699
+ mustClauses.push({
1700
+ key: 'modified_at',
1701
+ range: { gt: params.modifiedAfter },
1702
+ });
1554
1703
  }
1704
+ const filter = { must: mustClauses };
1705
+ const body = { filter };
1555
1706
  if (params.fields !== undefined) {
1556
1707
  body.fields = params.fields;
1557
1708
  }
@@ -1584,4 +1735,4 @@ class HttpWatcherClient {
1584
1735
  }
1585
1736
  }
1586
1737
 
1587
- export { GatewayExecutor, HttpWatcherClient, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildMetaFilter, buildOwnershipTree, computeEffectiveStaleness, computeEma, computeStructureHash, createSnapshot, discoverMetas, filterInScope, findNode, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, loadSynthConfig, mergeAndWrite, metaJsonSchema, normalizePath$1 as normalizePath, orchestrate, paginatedScan, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, releaseLock, resolveConfigPath, selectCandidate, synthConfigSchema, synthErrorSchema, toSynthError };
1738
+ export { GatewayExecutor, HttpWatcherClient, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildMetaFilter, buildOwnershipTree, computeEffectiveStaleness, computeEma, computeStructureHash, createSnapshot, discoverMetas, filterInScope, findNode, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, listMetas, loadSynthConfig, mergeAndWrite, metaJsonSchema, normalizePath$1 as normalizePath, orchestrate, paginatedScan, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, releaseLock, resolveConfigPath, selectCandidate, synthConfigSchema, synthErrorSchema, toSynthError };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karmaniverous/jeeves-meta",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "author": "Jason Williscroft",
5
5
  "description": "Knowledge synthesis engine for the Jeeves platform",
6
6
  "license": "BSD-3-Clause",