promptgraph-mcp 2.5.0 → 2.6.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/README.md +31 -10
- package/api.js +4 -2
- package/bundle-counts.js +3 -2
- package/chunker.js +1 -1
- package/commands/bundle.js +150 -0
- package/commands/doctor.js +15 -0
- package/commands/import.js +7 -0
- package/commands/init.js +37 -0
- package/commands/marketplace.js +114 -0
- package/commands/reindex.js +10 -0
- package/commands/search.js +55 -0
- package/commands/setup.js +19 -0
- package/commands/status.js +110 -0
- package/commands/train.js +18 -0
- package/commands/update.js +49 -0
- package/commands/validate.js +63 -0
- package/db.js +125 -111
- package/github-import.js +10 -8
- package/index.js +20 -598
- package/indexer.js +1 -8
- package/marketplace.js +44 -7
- package/package.json +3 -2
- package/search.js +20 -5
- package/src/reranker/reranker.js +76 -11
- package/src/store/index.js +1 -1
package/marketplace.js
CHANGED
|
@@ -561,9 +561,38 @@ export function pruneInvalidRepos() {
|
|
|
561
561
|
|
|
562
562
|
// ── Trust level system ─────────────────────────────────────────────────────────
|
|
563
563
|
const VALID_TRUST_LEVELS = ['verified', 'official', 'community', 'trusted', 'unknown']
|
|
564
|
+
const TRUST_LEVEL_BOOST = { verified: 1.15, official: 1.10, trusted: 1.05, community: 1.0, unknown: 0.95 }
|
|
565
|
+
|
|
566
|
+
// Popularity = log(downloads+1) × (rating+1), then decayed by age (days since last_update, halved every 180 days)
|
|
567
|
+
function calcPopularity(downloads = 0, rating = 0, lastUpdateStr = null) {
|
|
568
|
+
const logDownloads = Math.log10((downloads || 0) + 1)
|
|
569
|
+
const ratingFactor = ((rating || 0) + 1) / 6 // normalised to 0–1
|
|
570
|
+
let pop = logDownloads * ratingFactor * 100
|
|
571
|
+
|
|
572
|
+
if (lastUpdateStr) {
|
|
573
|
+
const daysSince = (Date.now() - new Date(lastUpdateStr + 'Z').getTime()) / 86400000
|
|
574
|
+
if (daysSince > 0) {
|
|
575
|
+
pop *= Math.pow(0.5, daysSince / 180) // half-life 180 days
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return Math.round(pop * 100) / 100
|
|
579
|
+
}
|
|
564
580
|
|
|
565
|
-
|
|
566
|
-
|
|
581
|
+
// Auto-promote threshold: downloads at which a skill graduates to the next trust level
|
|
582
|
+
const AUTO_PROMOTE_THRESHOLDS = [
|
|
583
|
+
{ minDownloads: 10000, level: 'verified' },
|
|
584
|
+
{ minDownloads: 1000, level: 'official' },
|
|
585
|
+
{ minDownloads: 100, level: 'trusted' },
|
|
586
|
+
]
|
|
587
|
+
|
|
588
|
+
function autoPromote(downloads, currentLevel) {
|
|
589
|
+
const rank = VALID_TRUST_LEVELS.indexOf(currentLevel || 'unknown')
|
|
590
|
+
for (const t of AUTO_PROMOTE_THRESHOLDS) {
|
|
591
|
+
if (downloads >= t.minDownloads && VALID_TRUST_LEVELS.indexOf(t.level) > rank) {
|
|
592
|
+
return t.level
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return null
|
|
567
596
|
}
|
|
568
597
|
|
|
569
598
|
export async function setTrustLevel(name, level) {
|
|
@@ -586,28 +615,36 @@ export async function getByTrustLevel(level) {
|
|
|
586
615
|
return { error: `Invalid trust level "${level}". Must be one of: ${VALID_TRUST_LEVELS.join(', ')}` }
|
|
587
616
|
}
|
|
588
617
|
if (level) {
|
|
589
|
-
return db.prepare('SELECT * FROM registry_entries WHERE trust_level = ? ORDER BY
|
|
618
|
+
return db.prepare('SELECT * FROM registry_entries WHERE trust_level = ? ORDER BY downloads DESC, popularity DESC').all(level)
|
|
590
619
|
}
|
|
591
|
-
return db.prepare('SELECT * FROM registry_entries ORDER BY
|
|
620
|
+
return db.prepare('SELECT * FROM registry_entries ORDER BY downloads DESC, popularity DESC').all()
|
|
592
621
|
}
|
|
593
622
|
|
|
594
623
|
export async function incrementDownloads(name) {
|
|
595
624
|
const db = getDb()
|
|
596
625
|
const id = String(name)
|
|
597
|
-
const existing = db.prepare('SELECT downloads, rating FROM registry_entries WHERE id = ?').get(id)
|
|
626
|
+
const existing = db.prepare('SELECT downloads, rating, last_update, trust_level FROM registry_entries WHERE id = ?').get(id)
|
|
598
627
|
if (existing) {
|
|
599
628
|
const newDownloads = (existing.downloads || 0) + 1
|
|
600
|
-
const pop = calcPopularity(newDownloads, existing.rating || 0)
|
|
629
|
+
const pop = calcPopularity(newDownloads, existing.rating || 0, existing.last_update)
|
|
601
630
|
db.prepare('UPDATE registry_entries SET downloads = ?, popularity = ?, last_update = datetime(\'now\') WHERE id = ?')
|
|
602
631
|
.run(newDownloads, pop, id)
|
|
632
|
+
|
|
633
|
+
// Auto-promote if threshold crossed
|
|
634
|
+
const newLevel = autoPromote(newDownloads, existing.trust_level)
|
|
635
|
+
if (newLevel) {
|
|
636
|
+
db.prepare('UPDATE registry_entries SET trust_level = ? WHERE id = ?').run(newLevel, id)
|
|
637
|
+
}
|
|
603
638
|
} else {
|
|
604
639
|
const pop = calcPopularity(1, 0)
|
|
605
|
-
db.prepare('INSERT INTO registry_entries (id, downloads, popularity, last_update) VALUES (?, 1, ?, datetime(\'now\'))')
|
|
640
|
+
db.prepare('INSERT INTO registry_entries (id, downloads, popularity, trust_level, last_update) VALUES (?, 1, ?, \'unknown\', datetime(\'now\'))')
|
|
606
641
|
.run(id, pop)
|
|
607
642
|
}
|
|
608
643
|
return { ok: true }
|
|
609
644
|
}
|
|
610
645
|
|
|
646
|
+
export { VALID_TRUST_LEVELS, TRUST_LEVEL_BOOST, calcPopularity, autoPromote }
|
|
647
|
+
|
|
611
648
|
export async function rateSkill(name, rating) {
|
|
612
649
|
const db = getDb()
|
|
613
650
|
if (typeof rating !== 'number' || rating < 0 || rating > 5) {
|
package/package.json
CHANGED
package/search.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { embed, cosineSimilarity } from './embedder.js';
|
|
2
2
|
import { getDb, blobToVec } from './db.js';
|
|
3
3
|
import { annSearch } from './ann.js';
|
|
4
|
+
import { TRUST_LEVEL_BOOST } from './marketplace.js';
|
|
4
5
|
|
|
5
6
|
function getHybridWeights(query) {
|
|
6
7
|
const hasTechTerms = /[A-Z]|\d/.test(query);
|
|
@@ -10,11 +11,25 @@ function getHybridWeights(query) {
|
|
|
10
11
|
|
|
11
12
|
function applyRatingBoost(db, id, score) {
|
|
12
13
|
const r = db.prepare('SELECT success, fail FROM ratings WHERE skill_id = ?').get(id);
|
|
14
|
+
let boost = 1.0;
|
|
13
15
|
if (r && (r.success + r.fail) > 3) {
|
|
14
|
-
|
|
15
|
-
return score * (0.85 + 0.15 * rating);
|
|
16
|
+
boost *= (0.85 + 0.15 * (r.success / (r.success + r.fail)));
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
// Trust level boost (verified/official/trusted rank higher)
|
|
20
|
+
const re = db.prepare('SELECT trust_level, popularity FROM registry_entries WHERE id = ?').get(id);
|
|
21
|
+
if (re) {
|
|
22
|
+
boost *= (TRUST_LEVEL_BOOST[re.trust_level] || 1.0);
|
|
23
|
+
// Popularity bump: top 20% of skills get +5%, rest unchanged
|
|
24
|
+
if (re.popularity > 0) {
|
|
25
|
+
const top20 = db.prepare('SELECT popularity FROM registry_entries ORDER BY popularity DESC LIMIT 1').get();
|
|
26
|
+
if (top20 && re.popularity >= top20.popularity * 0.2) {
|
|
27
|
+
boost *= 1.05;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return score * boost;
|
|
18
33
|
}
|
|
19
34
|
|
|
20
35
|
function normalizeBM25(raw) {
|
|
@@ -114,13 +129,13 @@ export async function search(query, topK = 5) {
|
|
|
114
129
|
if (rerankerEnabled) {
|
|
115
130
|
const { Reranker } = await import('./src/reranker/reranker.js')
|
|
116
131
|
const reranker = new Reranker()
|
|
117
|
-
const topN = ordered.slice(0,
|
|
132
|
+
const topN = ordered.slice(0, Math.max(50, topK * 6))
|
|
118
133
|
.map(({ id, score }) => {
|
|
119
134
|
const s = skillWithSnippet(db, id, score)
|
|
120
135
|
return s ? { id, text: s.snippet, score } : null
|
|
121
136
|
})
|
|
122
137
|
.filter(Boolean)
|
|
123
|
-
const reranked = await reranker.rerank(query, topN)
|
|
138
|
+
const reranked = await reranker.rerank(query, topN, topK)
|
|
124
139
|
return reranked.map(r => skillWithSnippet(db, r.id, applyRatingBoost(db, r.id, r.score))).filter(Boolean)
|
|
125
140
|
}
|
|
126
141
|
|
package/src/reranker/reranker.js
CHANGED
|
@@ -1,27 +1,92 @@
|
|
|
1
|
+
// Term-overlap reranker with phrase matching and IDF-like weighting.
|
|
2
|
+
// Replace `rerank()` with a cross-encoder (BGE Reranker / MiniLM via ONNX)
|
|
3
|
+
// for deeper semantic reranking. This class is the plug-in point.
|
|
1
4
|
export class Reranker {
|
|
2
5
|
constructor(modelName = 'default', device = 'cpu') {
|
|
3
6
|
this.modelName = modelName
|
|
4
7
|
this.device = device
|
|
5
8
|
}
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
// Plug-in point for BGE Reranker / MiniLM Reranker via ONNX
|
|
9
|
-
async rerank(query, results) {
|
|
10
|
+
rerank(query, results, topK = 5) {
|
|
10
11
|
if (!results || results.length === 0) return []
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
-
|
|
13
|
+
const rawTerms = (query.toLowerCase().match(/\w+/g) || [])
|
|
14
|
+
if (rawTerms.length === 0) return results.slice(0, topK)
|
|
14
15
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const unigrams = rawTerms
|
|
17
|
+
const bigrams = []
|
|
18
|
+
const trigrams = []
|
|
19
|
+
for (let i = 0; i < rawTerms.length - 1; i++) bigrams.push(rawTerms[i] + ' ' + rawTerms[i + 1])
|
|
20
|
+
for (let i = 0; i < rawTerms.length - 2; i++) trigrams.push(rawTerms[i] + ' ' + rawTerms[i + 1] + ' ' + rawTerms[i + 2])
|
|
21
|
+
|
|
22
|
+
const texts = results.map(r => (r.text || '').toLowerCase())
|
|
23
|
+
|
|
24
|
+
// Term doc-frequency
|
|
25
|
+
const termDf = {}
|
|
26
|
+
for (const t of unigrams) {
|
|
27
|
+
termDf[t] = texts.filter(txt => txt.includes(t)).length
|
|
28
|
+
}
|
|
29
|
+
const nResults = results.length
|
|
30
|
+
|
|
31
|
+
// First pass: compute raw tfIdf for each result
|
|
32
|
+
const tfIdfValues = texts.map(text => {
|
|
33
|
+
let sum = 0
|
|
34
|
+
for (const term of unigrams) {
|
|
35
|
+
const df = termDf[term] || 1
|
|
36
|
+
const idf = 1 + Math.log10((nResults + 1) / df)
|
|
37
|
+
let idx = -1
|
|
38
|
+
let count = 0
|
|
39
|
+
while ((idx = text.indexOf(term, idx + 1)) !== -1) count++
|
|
40
|
+
sum += count * idf
|
|
41
|
+
}
|
|
42
|
+
return sum
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const maxTfIdf = Math.max(...tfIdfValues, 1)
|
|
46
|
+
|
|
47
|
+
const scored = results.map((r, idx) => {
|
|
48
|
+
const text = texts[idx]
|
|
49
|
+
const lines = text.split('\n')
|
|
50
|
+
|
|
51
|
+
// 1. N-gram overlap
|
|
52
|
+
const unigramMatch = unigrams.filter(t => text.includes(t)).length / unigrams.length
|
|
53
|
+
const bigramMatch = bigrams.length > 0 ? bigrams.filter(b => text.includes(b)).length / bigrams.length : 0
|
|
54
|
+
const trigramMatch = trigrams.length > 0 ? trigrams.filter(t => text.includes(t)).length / trigrams.length : 0
|
|
55
|
+
const ngramScore = 0.4 * unigramMatch + 0.35 * bigramMatch + 0.25 * trigramMatch
|
|
56
|
+
|
|
57
|
+
// 2. IDF-weighted TF (normalised by max across results)
|
|
58
|
+
const tfIdfScore = tfIdfValues[idx] / maxTfIdf
|
|
59
|
+
|
|
60
|
+
// 3. Exact phrase proximity — consecutive same-order term matches
|
|
61
|
+
let phraseBoost = 0
|
|
62
|
+
if (rawTerms.length >= 2) {
|
|
63
|
+
const textWords = text.split(/\s+/)
|
|
64
|
+
let bestRun = 0
|
|
65
|
+
for (let i = 0; i < textWords.length - rawTerms.length + 1; i++) {
|
|
66
|
+
let matchLen = 0
|
|
67
|
+
for (let j = 0; j < rawTerms.length; j++) {
|
|
68
|
+
if (textWords[i + j] === rawTerms[j]) matchLen++
|
|
69
|
+
else break
|
|
70
|
+
}
|
|
71
|
+
if (matchLen >= 2) bestRun = Math.max(bestRun, matchLen)
|
|
72
|
+
}
|
|
73
|
+
phraseBoost = bestRun / rawTerms.length
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 4. Header position boost
|
|
77
|
+
const headerText = lines.slice(0, 3).join(' ').toLowerCase()
|
|
78
|
+
const headerOverlap = unigrams.filter(t => headerText.includes(t)).length / unigrams.length
|
|
79
|
+
|
|
80
|
+
// Blend
|
|
81
|
+
const overlapScore = 0.25 * ngramScore + 0.25 * tfIdfScore + 0.25 * phraseBoost + 0.15 * headerOverlap + 0.10 * unigramMatch
|
|
18
82
|
|
|
19
83
|
return {
|
|
20
|
-
|
|
21
|
-
|
|
84
|
+
id: r.id,
|
|
85
|
+
text: r.text,
|
|
86
|
+
score: 0.70 * (r.score || 0) + 0.30 * overlapScore,
|
|
22
87
|
}
|
|
23
88
|
})
|
|
24
89
|
|
|
25
|
-
return scored.sort((a, b) => b.score - a.score).slice(0,
|
|
90
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, topK)
|
|
26
91
|
}
|
|
27
92
|
}
|
package/src/store/index.js
CHANGED
|
@@ -7,7 +7,7 @@ export function getStore(type = null) {
|
|
|
7
7
|
if (_store) return _store;
|
|
8
8
|
|
|
9
9
|
if (!type) {
|
|
10
|
-
type = process.env.PG_VECTOR_STORE || '
|
|
10
|
+
type = process.env.PG_VECTOR_STORE || 'hnsw';
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
_store = type === 'hnsw' ? new HNSWVectorStore() : new FlatVectorStore();
|