@karmaniverous/jeeves-meta 0.3.0 → 0.3.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.
package/dist/cli.js CHANGED
@@ -420,6 +420,80 @@ async function discoverMetas(config, watcher) {
420
420
  return metaPaths;
421
421
  }
422
422
 
423
+ /**
424
+ * File-system lock for preventing concurrent synthesis on the same meta.
425
+ *
426
+ * Lock file: .meta/.lock containing PID + timestamp.
427
+ * Stale timeout: 30 minutes.
428
+ *
429
+ * @module lock
430
+ */
431
+ const LOCK_FILE = '.lock';
432
+ const STALE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
433
+ /**
434
+ * Attempt to acquire a lock on a .meta directory.
435
+ *
436
+ * @param metaPath - Absolute path to the .meta directory.
437
+ * @returns True if lock was acquired, false if already locked (non-stale).
438
+ */
439
+ function acquireLock(metaPath) {
440
+ const lockPath = join(metaPath, LOCK_FILE);
441
+ if (existsSync(lockPath)) {
442
+ try {
443
+ const raw = readFileSync(lockPath, 'utf8');
444
+ const data = JSON.parse(raw);
445
+ const lockAge = Date.now() - new Date(data.startedAt).getTime();
446
+ if (lockAge < STALE_TIMEOUT_MS) {
447
+ return false; // Lock is active
448
+ }
449
+ // Stale lock — fall through to overwrite
450
+ }
451
+ catch {
452
+ // Corrupt lock file — overwrite
453
+ }
454
+ }
455
+ const lock = {
456
+ pid: process.pid,
457
+ startedAt: new Date().toISOString(),
458
+ };
459
+ writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
460
+ return true;
461
+ }
462
+ /**
463
+ * Release a lock on a .meta directory.
464
+ *
465
+ * @param metaPath - Absolute path to the .meta directory.
466
+ */
467
+ function releaseLock(metaPath) {
468
+ const lockPath = join(metaPath, LOCK_FILE);
469
+ try {
470
+ unlinkSync(lockPath);
471
+ }
472
+ catch {
473
+ // Already removed or never existed
474
+ }
475
+ }
476
+ /**
477
+ * Check if a .meta directory is currently locked (non-stale).
478
+ *
479
+ * @param metaPath - Absolute path to the .meta directory.
480
+ * @returns True if locked and not stale.
481
+ */
482
+ function isLocked(metaPath) {
483
+ const lockPath = join(metaPath, LOCK_FILE);
484
+ if (!existsSync(lockPath))
485
+ return false;
486
+ try {
487
+ const raw = readFileSync(lockPath, 'utf8');
488
+ const data = JSON.parse(raw);
489
+ const lockAge = Date.now() - new Date(data.startedAt).getTime();
490
+ return lockAge < STALE_TIMEOUT_MS;
491
+ }
492
+ catch {
493
+ return false; // Corrupt lock = not locked
494
+ }
495
+ }
496
+
423
497
  /**
424
498
  * Build the ownership tree from discovered .meta/ paths.
425
499
  *
@@ -497,6 +571,138 @@ function findNode(tree, targetPath) {
497
571
  return Array.from(tree.nodes.values()).find((n) => n.metaPath === targetPath || n.ownerPath === targetPath);
498
572
  }
499
573
 
574
+ /**
575
+ * Unified meta listing: scan, dedup, enrich.
576
+ *
577
+ * Single source of truth for all consumers that need a list of metas
578
+ * with enriched metadata. Replaces duplicated scan+dedup logic in
579
+ * plugin tools, CLI, and prompt injection.
580
+ *
581
+ * @module discovery/listMetas
582
+ */
583
+ /**
584
+ * Discover, deduplicate, and enrich all metas.
585
+ *
586
+ * This is the single consolidated function that replaces all duplicated
587
+ * scan+dedup+enrich logic across the codebase. All enrichment comes from
588
+ * reading meta.json on disk (the canonical source).
589
+ *
590
+ * @param config - Validated synthesis config.
591
+ * @param watcher - Watcher HTTP client for discovery.
592
+ * @returns Enriched meta list with summary statistics and ownership tree.
593
+ */
594
+ async function listMetas(config, watcher) {
595
+ // Step 1: Discover deduplicated meta paths via watcher scan
596
+ const metaPaths = await discoverMetas(config, watcher);
597
+ // Step 2: Build ownership tree
598
+ const tree = buildOwnershipTree(metaPaths);
599
+ // Step 3: Read and enrich each meta from disk
600
+ const entries = [];
601
+ let staleCount = 0;
602
+ let errorCount = 0;
603
+ let lockedCount = 0;
604
+ let neverSynthesizedCount = 0;
605
+ let totalArchTokens = 0;
606
+ let totalBuilderTokens = 0;
607
+ let totalCriticTokens = 0;
608
+ let lastSynthPath = null;
609
+ let lastSynthAt = null;
610
+ let stalestPath = null;
611
+ let stalestEffective = -1;
612
+ for (const node of tree.nodes.values()) {
613
+ let meta;
614
+ try {
615
+ meta = JSON.parse(readFileSync(join(node.metaPath, 'meta.json'), 'utf8'));
616
+ }
617
+ catch {
618
+ // Skip unreadable metas
619
+ continue;
620
+ }
621
+ const depth = meta._depth ?? node.treeDepth;
622
+ const emphasis = meta._emphasis ?? 1;
623
+ const hasError = Boolean(meta._error);
624
+ const locked = isLocked(normalizePath$1(node.metaPath));
625
+ const neverSynth = !meta._generatedAt;
626
+ // Compute staleness
627
+ let stalenessSeconds;
628
+ if (neverSynth) {
629
+ stalenessSeconds = Infinity;
630
+ }
631
+ else {
632
+ const genAt = new Date(meta._generatedAt).getTime();
633
+ stalenessSeconds = Math.max(0, Math.floor((Date.now() - genAt) / 1000));
634
+ }
635
+ // Tokens
636
+ const archTokens = meta._architectTokens ?? 0;
637
+ const buildTokens = meta._builderTokens ?? 0;
638
+ const critTokens = meta._criticTokens ?? 0;
639
+ // Accumulate summary stats
640
+ if (stalenessSeconds > 0)
641
+ staleCount++;
642
+ if (hasError)
643
+ errorCount++;
644
+ if (locked)
645
+ lockedCount++;
646
+ if (neverSynth)
647
+ neverSynthesizedCount++;
648
+ totalArchTokens += archTokens;
649
+ totalBuilderTokens += buildTokens;
650
+ totalCriticTokens += critTokens;
651
+ // Track last synthesized
652
+ if (meta._generatedAt) {
653
+ if (!lastSynthAt || meta._generatedAt > lastSynthAt) {
654
+ lastSynthAt = meta._generatedAt;
655
+ lastSynthPath = node.metaPath;
656
+ }
657
+ }
658
+ // Track stalest (effective staleness for scheduling)
659
+ const depthFactor = Math.pow(1 + config.depthWeight, depth);
660
+ const effectiveStaleness = (stalenessSeconds === Infinity
661
+ ? Number.MAX_SAFE_INTEGER
662
+ : stalenessSeconds) *
663
+ depthFactor *
664
+ emphasis;
665
+ if (effectiveStaleness > stalestEffective) {
666
+ stalestEffective = effectiveStaleness;
667
+ stalestPath = node.metaPath;
668
+ }
669
+ entries.push({
670
+ path: node.metaPath,
671
+ depth,
672
+ emphasis,
673
+ stalenessSeconds,
674
+ lastSynthesized: meta._generatedAt ?? null,
675
+ hasError,
676
+ locked,
677
+ architectTokens: archTokens > 0 ? archTokens : null,
678
+ builderTokens: buildTokens > 0 ? buildTokens : null,
679
+ criticTokens: critTokens > 0 ? critTokens : null,
680
+ children: node.children.length,
681
+ node,
682
+ meta,
683
+ });
684
+ }
685
+ return {
686
+ summary: {
687
+ total: entries.length,
688
+ stale: staleCount,
689
+ errors: errorCount,
690
+ locked: lockedCount,
691
+ neverSynthesized: neverSynthesizedCount,
692
+ tokens: {
693
+ architect: totalArchTokens,
694
+ builder: totalBuilderTokens,
695
+ critic: totalCriticTokens,
696
+ },
697
+ stalestPath,
698
+ lastSynthesizedPath: lastSynthPath,
699
+ lastSynthesizedAt: lastSynthAt,
700
+ },
701
+ entries,
702
+ tree,
703
+ };
704
+ }
705
+
500
706
  /**
501
707
  * Compute the file scope owned by a meta node.
502
708
  *
@@ -690,80 +896,6 @@ class GatewayExecutor {
690
896
  }
691
897
  }
692
898
 
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
899
  /**
768
900
  * Build the SynthContext for a synthesis cycle.
769
901
  *
@@ -1557,16 +1689,31 @@ class HttpWatcherClient {
1557
1689
  throw new Error('Retry exhausted');
1558
1690
  }
1559
1691
  async scan(params) {
1560
- const body = {};
1561
- if (params.pathPrefix !== undefined) {
1562
- body.pathPrefix = params.pathPrefix;
1692
+ // Build Qdrant filter: merge explicit filter with pathPrefix/modifiedAfter
1693
+ const mustClauses = [];
1694
+ // Carry over any existing 'must' clauses from the provided filter
1695
+ if (params.filter) {
1696
+ const existing = params.filter.must;
1697
+ if (Array.isArray(existing)) {
1698
+ mustClauses.push(...existing);
1699
+ }
1563
1700
  }
1564
- if (params.filter !== undefined) {
1565
- body.filter = params.filter;
1701
+ // Translate pathPrefix into a Qdrant text match on file_path
1702
+ if (params.pathPrefix !== undefined) {
1703
+ mustClauses.push({
1704
+ key: 'file_path',
1705
+ match: { text: params.pathPrefix },
1706
+ });
1566
1707
  }
1708
+ // Translate modifiedAfter into a Qdrant range filter on modified_at
1567
1709
  if (params.modifiedAfter !== undefined) {
1568
- body.modifiedAfter = params.modifiedAfter;
1710
+ mustClauses.push({
1711
+ key: 'modified_at',
1712
+ range: { gt: params.modifiedAfter },
1713
+ });
1569
1714
  }
1715
+ const filter = { must: mustClauses };
1716
+ const body = { filter };
1570
1717
  if (params.fields !== undefined) {
1571
1718
  body.fields = params.fields;
1572
1719
  }
@@ -1630,6 +1777,7 @@ var index = /*#__PURE__*/Object.freeze({
1630
1777
  isLocked: isLocked,
1631
1778
  isStale: isStale,
1632
1779
  listArchiveFiles: listArchiveFiles,
1780
+ listMetas: listMetas,
1633
1781
  loadSynthConfig: loadSynthConfig,
1634
1782
  mergeAndWrite: mergeAndWrite,
1635
1783
  metaJsonSchema: metaJsonSchema,
@@ -1700,85 +1848,36 @@ function output(data) {
1700
1848
  }
1701
1849
  async function runStatus(config) {
1702
1850
  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
- });
1851
+ const result = await listMetas(config, watcher);
1852
+ output(result.summary);
1744
1853
  }
1745
1854
  async function runList(config) {
1746
1855
  const prefix = getArg('--prefix');
1747
1856
  const filter = getArg('--filter');
1748
1857
  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
- }
1858
+ const result = await listMetas(config, watcher);
1859
+ let entries = result.entries;
1860
+ if (prefix) {
1861
+ entries = entries.filter((e) => e.path.includes(prefix));
1862
+ }
1863
+ if (filter === 'hasError')
1864
+ entries = entries.filter((e) => e.hasError);
1865
+ if (filter === 'stale')
1866
+ entries = entries.filter((e) => e.stalenessSeconds > 0);
1867
+ if (filter === 'locked')
1868
+ entries = entries.filter((e) => e.locked);
1869
+ if (filter === 'never')
1870
+ entries = entries.filter((e) => e.stalenessSeconds === Infinity);
1871
+ const rows = entries.map((e) => ({
1872
+ path: e.path,
1873
+ depth: e.depth,
1874
+ staleness: e.stalenessSeconds === Infinity
1875
+ ? 'never'
1876
+ : String(Math.round(e.stalenessSeconds)) + 's',
1877
+ hasError: e.hasError,
1878
+ locked: e.locked,
1879
+ children: e.children,
1880
+ }));
1782
1881
  output({ total: rows.length, items: rows });
1783
1882
  }
1784
1883
  async function runDetail(config) {
@@ -1789,10 +1888,9 @@ async function runDetail(config) {
1789
1888
  }
1790
1889
  const archiveArg = getArg('--archive');
1791
1890
  const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
1792
- const metaPaths = await discoverMetas(config, watcher);
1793
- const tree = buildOwnershipTree(metaPaths);
1891
+ const metaResult = await listMetas(config, watcher);
1794
1892
  const normalized = normalizePath$1(targetPath);
1795
- const node = findNode(tree, normalized);
1893
+ const node = findNode(metaResult.tree, normalized);
1796
1894
  if (!node) {
1797
1895
  console.error('Meta not found: ' + targetPath);
1798
1896
  process.exit(1);
@@ -1813,33 +1911,26 @@ async function runDetail(config) {
1813
1911
  }
1814
1912
  async function runPreview(config) {
1815
1913
  const targetPath = getArg('--path');
1816
- const { filterInScope, paginatedScan, readLatestArchive, computeStructureHash, selectCandidate, } = await Promise.resolve().then(function () { return index; });
1914
+ const { filterInScope, paginatedScan, readLatestArchive, computeStructureHash, } = await Promise.resolve().then(function () { return index; });
1817
1915
  const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
1818
- const metaPaths = await discoverMetas(config, watcher);
1819
- const tree = buildOwnershipTree(metaPaths);
1916
+ const metaResult = await listMetas(config, watcher);
1820
1917
  let targetNode;
1821
1918
  if (targetPath) {
1822
1919
  const normalized = normalizePath$1(targetPath);
1823
- targetNode = findNode(tree, normalized);
1920
+ targetNode = findNode(metaResult.tree, normalized);
1824
1921
  if (!targetNode) {
1825
1922
  console.error('Meta not found: ' + targetPath);
1826
1923
  process.exit(1);
1827
1924
  }
1828
1925
  }
1829
1926
  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
- }
1927
+ const candidates = metaResult.entries
1928
+ .filter((e) => e.stalenessSeconds > 0)
1929
+ .map((e) => ({
1930
+ node: e.node,
1931
+ meta: e.meta,
1932
+ actualStaleness: e.stalenessSeconds,
1933
+ }));
1843
1934
  const weighted = computeEffectiveStaleness(candidates, config.depthWeight);
1844
1935
  const winner = selectCandidate(weighted);
1845
1936
  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
@@ -405,6 +405,80 @@ async function discoverMetas(config, watcher) {
405
405
  return metaPaths;
406
406
  }
407
407
 
408
+ /**
409
+ * File-system lock for preventing concurrent synthesis on the same meta.
410
+ *
411
+ * Lock file: .meta/.lock containing PID + timestamp.
412
+ * Stale timeout: 30 minutes.
413
+ *
414
+ * @module lock
415
+ */
416
+ const LOCK_FILE = '.lock';
417
+ const STALE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
418
+ /**
419
+ * Attempt to acquire a lock on a .meta directory.
420
+ *
421
+ * @param metaPath - Absolute path to the .meta directory.
422
+ * @returns True if lock was acquired, false if already locked (non-stale).
423
+ */
424
+ function acquireLock(metaPath) {
425
+ const lockPath = join(metaPath, LOCK_FILE);
426
+ if (existsSync(lockPath)) {
427
+ try {
428
+ const raw = readFileSync(lockPath, 'utf8');
429
+ const data = JSON.parse(raw);
430
+ const lockAge = Date.now() - new Date(data.startedAt).getTime();
431
+ if (lockAge < STALE_TIMEOUT_MS) {
432
+ return false; // Lock is active
433
+ }
434
+ // Stale lock — fall through to overwrite
435
+ }
436
+ catch {
437
+ // Corrupt lock file — overwrite
438
+ }
439
+ }
440
+ const lock = {
441
+ pid: process.pid,
442
+ startedAt: new Date().toISOString(),
443
+ };
444
+ writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
445
+ return true;
446
+ }
447
+ /**
448
+ * Release a lock on a .meta directory.
449
+ *
450
+ * @param metaPath - Absolute path to the .meta directory.
451
+ */
452
+ function releaseLock(metaPath) {
453
+ const lockPath = join(metaPath, LOCK_FILE);
454
+ try {
455
+ unlinkSync(lockPath);
456
+ }
457
+ catch {
458
+ // Already removed or never existed
459
+ }
460
+ }
461
+ /**
462
+ * Check if a .meta directory is currently locked (non-stale).
463
+ *
464
+ * @param metaPath - Absolute path to the .meta directory.
465
+ * @returns True if locked and not stale.
466
+ */
467
+ function isLocked(metaPath) {
468
+ const lockPath = join(metaPath, LOCK_FILE);
469
+ if (!existsSync(lockPath))
470
+ return false;
471
+ try {
472
+ const raw = readFileSync(lockPath, 'utf8');
473
+ const data = JSON.parse(raw);
474
+ const lockAge = Date.now() - new Date(data.startedAt).getTime();
475
+ return lockAge < STALE_TIMEOUT_MS;
476
+ }
477
+ catch {
478
+ return false; // Corrupt lock = not locked
479
+ }
480
+ }
481
+
408
482
  /**
409
483
  * Build the ownership tree from discovered .meta/ paths.
410
484
  *
@@ -482,6 +556,138 @@ function findNode(tree, targetPath) {
482
556
  return Array.from(tree.nodes.values()).find((n) => n.metaPath === targetPath || n.ownerPath === targetPath);
483
557
  }
484
558
 
559
+ /**
560
+ * Unified meta listing: scan, dedup, enrich.
561
+ *
562
+ * Single source of truth for all consumers that need a list of metas
563
+ * with enriched metadata. Replaces duplicated scan+dedup logic in
564
+ * plugin tools, CLI, and prompt injection.
565
+ *
566
+ * @module discovery/listMetas
567
+ */
568
+ /**
569
+ * Discover, deduplicate, and enrich all metas.
570
+ *
571
+ * This is the single consolidated function that replaces all duplicated
572
+ * scan+dedup+enrich logic across the codebase. All enrichment comes from
573
+ * reading meta.json on disk (the canonical source).
574
+ *
575
+ * @param config - Validated synthesis config.
576
+ * @param watcher - Watcher HTTP client for discovery.
577
+ * @returns Enriched meta list with summary statistics and ownership tree.
578
+ */
579
+ async function listMetas(config, watcher) {
580
+ // Step 1: Discover deduplicated meta paths via watcher scan
581
+ const metaPaths = await discoverMetas(config, watcher);
582
+ // Step 2: Build ownership tree
583
+ const tree = buildOwnershipTree(metaPaths);
584
+ // Step 3: Read and enrich each meta from disk
585
+ const entries = [];
586
+ let staleCount = 0;
587
+ let errorCount = 0;
588
+ let lockedCount = 0;
589
+ let neverSynthesizedCount = 0;
590
+ let totalArchTokens = 0;
591
+ let totalBuilderTokens = 0;
592
+ let totalCriticTokens = 0;
593
+ let lastSynthPath = null;
594
+ let lastSynthAt = null;
595
+ let stalestPath = null;
596
+ let stalestEffective = -1;
597
+ for (const node of tree.nodes.values()) {
598
+ let meta;
599
+ try {
600
+ meta = JSON.parse(readFileSync(join(node.metaPath, 'meta.json'), 'utf8'));
601
+ }
602
+ catch {
603
+ // Skip unreadable metas
604
+ continue;
605
+ }
606
+ const depth = meta._depth ?? node.treeDepth;
607
+ const emphasis = meta._emphasis ?? 1;
608
+ const hasError = Boolean(meta._error);
609
+ const locked = isLocked(normalizePath$1(node.metaPath));
610
+ const neverSynth = !meta._generatedAt;
611
+ // Compute staleness
612
+ let stalenessSeconds;
613
+ if (neverSynth) {
614
+ stalenessSeconds = Infinity;
615
+ }
616
+ else {
617
+ const genAt = new Date(meta._generatedAt).getTime();
618
+ stalenessSeconds = Math.max(0, Math.floor((Date.now() - genAt) / 1000));
619
+ }
620
+ // Tokens
621
+ const archTokens = meta._architectTokens ?? 0;
622
+ const buildTokens = meta._builderTokens ?? 0;
623
+ const critTokens = meta._criticTokens ?? 0;
624
+ // Accumulate summary stats
625
+ if (stalenessSeconds > 0)
626
+ staleCount++;
627
+ if (hasError)
628
+ errorCount++;
629
+ if (locked)
630
+ lockedCount++;
631
+ if (neverSynth)
632
+ neverSynthesizedCount++;
633
+ totalArchTokens += archTokens;
634
+ totalBuilderTokens += buildTokens;
635
+ totalCriticTokens += critTokens;
636
+ // Track last synthesized
637
+ if (meta._generatedAt) {
638
+ if (!lastSynthAt || meta._generatedAt > lastSynthAt) {
639
+ lastSynthAt = meta._generatedAt;
640
+ lastSynthPath = node.metaPath;
641
+ }
642
+ }
643
+ // Track stalest (effective staleness for scheduling)
644
+ const depthFactor = Math.pow(1 + config.depthWeight, depth);
645
+ const effectiveStaleness = (stalenessSeconds === Infinity
646
+ ? Number.MAX_SAFE_INTEGER
647
+ : stalenessSeconds) *
648
+ depthFactor *
649
+ emphasis;
650
+ if (effectiveStaleness > stalestEffective) {
651
+ stalestEffective = effectiveStaleness;
652
+ stalestPath = node.metaPath;
653
+ }
654
+ entries.push({
655
+ path: node.metaPath,
656
+ depth,
657
+ emphasis,
658
+ stalenessSeconds,
659
+ lastSynthesized: meta._generatedAt ?? null,
660
+ hasError,
661
+ locked,
662
+ architectTokens: archTokens > 0 ? archTokens : null,
663
+ builderTokens: buildTokens > 0 ? buildTokens : null,
664
+ criticTokens: critTokens > 0 ? critTokens : null,
665
+ children: node.children.length,
666
+ node,
667
+ meta,
668
+ });
669
+ }
670
+ return {
671
+ summary: {
672
+ total: entries.length,
673
+ stale: staleCount,
674
+ errors: errorCount,
675
+ locked: lockedCount,
676
+ neverSynthesized: neverSynthesizedCount,
677
+ tokens: {
678
+ architect: totalArchTokens,
679
+ builder: totalBuilderTokens,
680
+ critic: totalCriticTokens,
681
+ },
682
+ stalestPath,
683
+ lastSynthesizedPath: lastSynthPath,
684
+ lastSynthesizedAt: lastSynthAt,
685
+ },
686
+ entries,
687
+ tree,
688
+ };
689
+ }
690
+
485
691
  /**
486
692
  * Compute the file scope owned by a meta node.
487
693
  *
@@ -675,80 +881,6 @@ class GatewayExecutor {
675
881
  }
676
882
  }
677
883
 
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
884
  /**
753
885
  * Build the SynthContext for a synthesis cycle.
754
886
  *
@@ -1542,16 +1674,31 @@ class HttpWatcherClient {
1542
1674
  throw new Error('Retry exhausted');
1543
1675
  }
1544
1676
  async scan(params) {
1545
- const body = {};
1546
- if (params.pathPrefix !== undefined) {
1547
- body.pathPrefix = params.pathPrefix;
1677
+ // Build Qdrant filter: merge explicit filter with pathPrefix/modifiedAfter
1678
+ const mustClauses = [];
1679
+ // Carry over any existing 'must' clauses from the provided filter
1680
+ if (params.filter) {
1681
+ const existing = params.filter.must;
1682
+ if (Array.isArray(existing)) {
1683
+ mustClauses.push(...existing);
1684
+ }
1548
1685
  }
1549
- if (params.filter !== undefined) {
1550
- body.filter = params.filter;
1686
+ // Translate pathPrefix into a Qdrant text match on file_path
1687
+ if (params.pathPrefix !== undefined) {
1688
+ mustClauses.push({
1689
+ key: 'file_path',
1690
+ match: { text: params.pathPrefix },
1691
+ });
1551
1692
  }
1693
+ // Translate modifiedAfter into a Qdrant range filter on modified_at
1552
1694
  if (params.modifiedAfter !== undefined) {
1553
- body.modifiedAfter = params.modifiedAfter;
1695
+ mustClauses.push({
1696
+ key: 'modified_at',
1697
+ range: { gt: params.modifiedAfter },
1698
+ });
1554
1699
  }
1700
+ const filter = { must: mustClauses };
1701
+ const body = { filter };
1555
1702
  if (params.fields !== undefined) {
1556
1703
  body.fields = params.fields;
1557
1704
  }
@@ -1584,4 +1731,4 @@ class HttpWatcherClient {
1584
1731
  }
1585
1732
  }
1586
1733
 
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 };
1734
+ 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.1",
4
4
  "author": "Jason Williscroft",
5
5
  "description": "Knowledge synthesis engine for the Jeeves platform",
6
6
  "license": "BSD-3-Clause",