@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 +273 -178
- package/dist/index.d.ts +76 -2
- package/dist/index.js +236 -85
- package/package.json +1 -1
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
|
|
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
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
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
|
-
|
|
1565
|
-
|
|
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
|
-
|
|
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
|
|
1704
|
-
|
|
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
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
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
|
|
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,
|
|
1918
|
+
const { filterInScope, paginatedScan, readLatestArchive, computeStructureHash, } = await Promise.resolve().then(function () { return index; });
|
|
1817
1919
|
const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
|
|
1818
|
-
const
|
|
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
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
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
|
|
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
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
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
|
-
|
|
1550
|
-
|
|
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
|
-
|
|
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 };
|