promptgraph-mcp 2.8.1 → 2.8.3

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.
@@ -1,62 +1,62 @@
1
- import fs from 'fs';
2
-
3
- const SKIP_FILENAMES = new Set([
4
- 'readme', 'changelog', 'license', 'contributing', 'code-of-conduct',
5
- 'security', 'authors', 'credits', 'install', 'installation', 'usage',
6
- 'engagements', 'contributors', 'maintainers', 'acknowledgements',
7
- 'faq', 'glossary', 'index', 'overview', 'summary', 'roadmap', 'todo',
8
- 'notes', 'template', 'example', 'sample', 'demo', 'getting-started',
9
- 'quickstart', 'guide', 'tutorial', 'walkthrough', 'architecture',
10
- 'design', 'spec', 'specification', 'requirements', 'privacy', 'terms',
11
- 'disclaimer', 'notice', 'copying', 'warranty', 'codeofconduct',
12
- 'pull_request_template', 'issue_template', 'funding',
13
- 'claude', 'bugs', 'bug_report', 'bug-report', 'feature_request',
14
- 'feature-request',
15
- ]);
16
-
17
- const SKIP_FILENAME_RE = /^(_|\.)|^v?\d+[\.\-]\d+|^\d{4}[\-_]\d{2}|^readme|^license|^changelog|^contributing|^code.of.conduct|^security|^authors|^credits|^disclaimer|^notice|^copying|^warranty|^promotion|^funding|^claude|^bugs?\b|^feature.?request/i;
18
-
19
- const SKIP_DIRS = new Set([
20
- '.github', 'docs', 'doc', 'documentation', 'examples', 'example',
21
- 'tests', 'test', '__tests__', 'spec', 'fixtures', 'assets', 'images',
22
- 'img', 'screenshots', 'media', 'static', 'public', 'dist', 'build',
23
- 'node_modules', 'vendor', 'third_party',
24
- 'references', 'reference', 'refs', 'cheatsheet', 'cheat-sheet',
25
- 'cheatsheets', 'resources',
26
- 'src', 'cli', 'lib', 'bin',
27
- ]);
28
-
29
- const BADGE_RE = /!\[.*\]\(https?:\/\/(img\.shields\.io|badge\.fury|travis-ci|github\.com\/[^)]+\/badge)/i;
30
- const README_HEADER_RE = /^#\s*readme\b/i;
31
-
32
- export function hardFilter(filePath, raw) {
33
- const parts = filePath.replace(/\\/g, '/').split('/');
34
- const filename = parts[parts.length - 1];
35
- const base = filename.replace(/\.md$/i, '').toLowerCase();
36
-
37
- if (SKIP_FILENAMES.has(base)) {
38
- return { pass: false, reason: `skip filename: ${base}` };
39
- }
40
-
41
- if (SKIP_FILENAME_RE.test(base)) {
42
- return { pass: false, reason: `skip filename pattern: ${base}` };
43
- }
44
-
45
- for (const part of parts.slice(0, -1)) {
46
- if (SKIP_DIRS.has(part.toLowerCase())) {
47
- return { pass: false, reason: `skip dir: ${part}` };
48
- }
49
- }
50
-
51
- if (raw) {
52
- const firstLines = raw.trimStart().slice(0, 300);
53
- if (README_HEADER_RE.test(firstLines)) {
54
- return { pass: false, reason: 'starts with # Readme' };
55
- }
56
- if (BADGE_RE.test(firstLines)) {
57
- return { pass: false, reason: 'badge-only content' };
58
- }
59
- }
60
-
61
- return { pass: true };
62
- }
1
+ import fs from 'fs';
2
+
3
+ const SKIP_FILENAMES = new Set([
4
+ 'readme', 'changelog', 'license', 'contributing', 'code-of-conduct',
5
+ 'security', 'authors', 'credits', 'install', 'installation', 'usage',
6
+ 'engagements', 'contributors', 'maintainers', 'acknowledgements',
7
+ 'faq', 'glossary', 'index', 'overview', 'summary', 'roadmap', 'todo',
8
+ 'notes', 'template', 'example', 'sample', 'demo', 'getting-started',
9
+ 'quickstart', 'guide', 'tutorial', 'walkthrough', 'architecture',
10
+ 'design', 'spec', 'specification', 'requirements', 'privacy', 'terms',
11
+ 'disclaimer', 'notice', 'copying', 'warranty', 'codeofconduct',
12
+ 'pull_request_template', 'issue_template', 'funding',
13
+ 'claude', 'bugs', 'bug_report', 'bug-report', 'feature_request',
14
+ 'feature-request',
15
+ ]);
16
+
17
+ const SKIP_FILENAME_RE = /^(_|\.)|^v?\d+[\.\-]\d+|^\d{4}[\-_]\d{2}|^readme|^license|^changelog|^contributing|^code.of.conduct|^security|^authors|^credits|^disclaimer|^notice|^copying|^warranty|^promotion|^funding|^claude|^bugs?\b|^feature.?request/i;
18
+
19
+ const SKIP_DIRS = new Set([
20
+ '.github', 'docs', 'doc', 'documentation', 'examples', 'example',
21
+ 'tests', 'test', '__tests__', 'spec', 'fixtures', 'assets', 'images',
22
+ 'img', 'screenshots', 'media', 'static', 'public', 'dist', 'build',
23
+ 'node_modules', 'vendor', 'third_party',
24
+ 'references', 'reference', 'refs', 'cheatsheet', 'cheat-sheet',
25
+ 'cheatsheets', 'resources',
26
+ 'src', 'cli', 'lib', 'bin',
27
+ ]);
28
+
29
+ const BADGE_RE = /!\[.*\]\(https?:\/\/(img\.shields\.io|badge\.fury|travis-ci|github\.com\/[^)]+\/badge)/i;
30
+ const README_HEADER_RE = /^#\s*readme\b/i;
31
+
32
+ export function hardFilter(filePath, raw) {
33
+ const parts = filePath.replace(/\\/g, '/').split('/');
34
+ const filename = parts[parts.length - 1];
35
+ const base = filename.replace(/\.md$/i, '').toLowerCase();
36
+
37
+ if (SKIP_FILENAMES.has(base)) {
38
+ return { pass: false, reason: `skip filename: ${base}` };
39
+ }
40
+
41
+ if (SKIP_FILENAME_RE.test(base)) {
42
+ return { pass: false, reason: `skip filename pattern: ${base}` };
43
+ }
44
+
45
+ for (const part of parts.slice(0, -1)) {
46
+ if (SKIP_DIRS.has(part.toLowerCase())) {
47
+ return { pass: false, reason: `skip dir: ${part}` };
48
+ }
49
+ }
50
+
51
+ if (raw) {
52
+ const firstLines = raw.trimStart().slice(0, 300);
53
+ if (README_HEADER_RE.test(firstLines)) {
54
+ return { pass: false, reason: 'starts with # Readme' };
55
+ }
56
+ if (BADGE_RE.test(firstLines)) {
57
+ return { pass: false, reason: 'badge-only content' };
58
+ }
59
+ }
60
+
61
+ return { pass: true };
62
+ }
@@ -1,66 +1,66 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import os from 'os';
4
- import { fileURLToPath } from 'url';
5
- import { globSync } from 'glob';
6
- import { embedBatch } from '../../embedder.js';
7
-
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
- const TRAINING_DIR = path.resolve(__dirname, '../../registry/training');
10
- const MODEL_PATH = path.join(os.homedir(), '.claude', '.promptgraph', 'model.json');
11
-
12
- function readAllMd(dir) {
13
- const files = globSync(`${dir}/**/*.md`);
14
- return files.map(f => fs.readFileSync(f, 'utf8'));
15
- }
16
-
17
- function meanVector(vectors) {
18
- if (!vectors.length) return null;
19
- const dim = vectors[0].length;
20
- const centroid = new Float32Array(dim);
21
- for (const v of vectors) {
22
- for (let i = 0; i < dim; i++) centroid[i] += v[i];
23
- }
24
- for (let i = 0; i < dim; i++) centroid[i] /= vectors.length;
25
- return Array.from(centroid);
26
- }
27
-
28
- export async function train() {
29
- const goodDir = path.join(TRAINING_DIR, 'good');
30
- const badDir = path.join(TRAINING_DIR, 'bad');
31
-
32
- const goodTexts = readAllMd(goodDir);
33
- const badTexts = readAllMd(badDir);
34
-
35
- if (goodTexts.length < 1 || badTexts.length < 1) {
36
- throw new Error(`Need at least 1 good and 1 bad training example (good: ${goodTexts.length}, bad: ${badTexts.length})`);
37
- }
38
-
39
- const allTexts = [...goodTexts, ...badTexts];
40
- const allVecs = await embedBatch(allTexts);
41
-
42
- const goodVecs = allVecs.slice(0, goodTexts.length);
43
- const badVecs = allVecs.slice(goodTexts.length);
44
-
45
- const model = {
46
- version: 1,
47
- good: meanVector(goodVecs),
48
- bad: meanVector(badVecs),
49
- counts: { good: goodTexts.length, bad: badTexts.length },
50
- };
51
-
52
- fs.mkdirSync(path.dirname(MODEL_PATH), { recursive: true });
53
- fs.writeFileSync(MODEL_PATH, JSON.stringify(model));
54
- return model;
55
- }
56
-
57
- export function loadModel() {
58
- try {
59
- const raw = fs.readFileSync(MODEL_PATH, 'utf8');
60
- return JSON.parse(raw);
61
- } catch {
62
- return null;
63
- }
64
- }
65
-
66
- export { MODEL_PATH };
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { fileURLToPath } from 'url';
5
+ import { globSync } from 'glob';
6
+ import { embedBatch } from '../../embedder.js';
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const TRAINING_DIR = path.resolve(__dirname, '../../registry/training');
10
+ const MODEL_PATH = path.join(os.homedir(), '.claude', '.promptgraph', 'model.json');
11
+
12
+ function readAllMd(dir) {
13
+ const files = globSync(`${dir}/**/*.md`);
14
+ return files.map(f => fs.readFileSync(f, 'utf8'));
15
+ }
16
+
17
+ function meanVector(vectors) {
18
+ if (!vectors.length) return null;
19
+ const dim = vectors[0].length;
20
+ const centroid = new Float32Array(dim);
21
+ for (const v of vectors) {
22
+ for (let i = 0; i < dim; i++) centroid[i] += v[i];
23
+ }
24
+ for (let i = 0; i < dim; i++) centroid[i] /= vectors.length;
25
+ return Array.from(centroid);
26
+ }
27
+
28
+ export async function train() {
29
+ const goodDir = path.join(TRAINING_DIR, 'good');
30
+ const badDir = path.join(TRAINING_DIR, 'bad');
31
+
32
+ const goodTexts = readAllMd(goodDir);
33
+ const badTexts = readAllMd(badDir);
34
+
35
+ if (goodTexts.length < 1 || badTexts.length < 1) {
36
+ throw new Error(`Need at least 1 good and 1 bad training example (good: ${goodTexts.length}, bad: ${badTexts.length})`);
37
+ }
38
+
39
+ const allTexts = [...goodTexts, ...badTexts];
40
+ const allVecs = await embedBatch(allTexts);
41
+
42
+ const goodVecs = allVecs.slice(0, goodTexts.length);
43
+ const badVecs = allVecs.slice(goodTexts.length);
44
+
45
+ const model = {
46
+ version: 1,
47
+ good: meanVector(goodVecs),
48
+ bad: meanVector(badVecs),
49
+ counts: { good: goodTexts.length, bad: badTexts.length },
50
+ };
51
+
52
+ fs.mkdirSync(path.dirname(MODEL_PATH), { recursive: true });
53
+ fs.writeFileSync(MODEL_PATH, JSON.stringify(model));
54
+ return model;
55
+ }
56
+
57
+ export function loadModel() {
58
+ try {
59
+ const raw = fs.readFileSync(MODEL_PATH, 'utf8');
60
+ return JSON.parse(raw);
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ export { MODEL_PATH };
@@ -1,92 +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.
4
- export class Reranker {
5
- constructor(modelName = 'default', device = 'cpu') {
6
- this.modelName = modelName
7
- this.device = device
8
- }
9
-
10
- rerank(query, results, topK = 5) {
11
- if (!results || results.length === 0) return []
12
-
13
- const rawTerms = (query.toLowerCase().match(/\w+/g) || [])
14
- if (rawTerms.length === 0) return results.slice(0, topK)
15
-
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
82
-
83
- return {
84
- id: r.id,
85
- text: r.text,
86
- score: 0.70 * (r.score || 0) + 0.30 * overlapScore,
87
- }
88
- })
89
-
90
- return scored.sort((a, b) => b.score - a.score).slice(0, topK)
91
- }
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.
4
+ export class Reranker {
5
+ constructor(modelName = 'default', device = 'cpu') {
6
+ this.modelName = modelName
7
+ this.device = device
8
+ }
9
+
10
+ rerank(query, results, topK = 5) {
11
+ if (!results || results.length === 0) return []
12
+
13
+ const rawTerms = (query.toLowerCase().match(/\w+/g) || [])
14
+ if (rawTerms.length === 0) return results.slice(0, topK)
15
+
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
82
+
83
+ return {
84
+ id: r.id,
85
+ text: r.text,
86
+ score: 0.70 * (r.score || 0) + 0.30 * overlapScore,
87
+ }
88
+ })
89
+
90
+ return scored.sort((a, b) => b.score - a.score).slice(0, topK)
91
+ }
92
+ }
@@ -1,61 +1,61 @@
1
- import { VectorStore } from './vector-store.js';
2
-
3
- export class FlatVectorStore extends VectorStore {
4
- constructor() {
5
- super();
6
- this._items = [];
7
- }
8
-
9
- async add(id, vector) {
10
- this._items.push({ skill_id: id, vec: new Float32Array(vector) });
11
- }
12
-
13
- async addBatch(entries) {
14
- for (const { id, vector } of entries) {
15
- this._items.push({ skill_id: id, vec: new Float32Array(vector) });
16
- }
17
- }
18
-
19
- async remove(id) {
20
- this._items = this._items.filter(item => item.skill_id !== id);
21
- }
22
-
23
- async search(vector, topK = 20) {
24
- const qArr = new Float32Array(vector);
25
- const bestBySkill = new Map();
26
- for (const entry of this._items) {
27
- const score = cosineSim(qArr, entry.vec);
28
- const prev = bestBySkill.get(entry.skill_id);
29
- if (!prev || score > prev) bestBySkill.set(entry.skill_id, score);
30
- }
31
- return [...bestBySkill.entries()]
32
- .sort((a, b) => b[1] - a[1])
33
- .slice(0, topK)
34
- .map(([skill_id, score]) => ({ skill_id, score }));
35
- }
36
-
37
- async build(entries) {
38
- this._items = [];
39
- for (const { skill_id, vector } of entries) {
40
- this._items.push({ skill_id, vec: new Float32Array(vector) });
41
- }
42
- }
43
-
44
- async clear() {
45
- this._items = [];
46
- }
47
-
48
- get size() {
49
- return this._items.length;
50
- }
51
- }
52
-
53
- function cosineSim(a, b) {
54
- let dot = 0, na = 0, nb = 0;
55
- for (let i = 0; i < a.length; i++) {
56
- dot += a[i] * b[i];
57
- na += a[i] * a[i];
58
- nb += b[i] * b[i];
59
- }
60
- return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-8);
61
- }
1
+ import { VectorStore } from './vector-store.js';
2
+
3
+ export class FlatVectorStore extends VectorStore {
4
+ constructor() {
5
+ super();
6
+ this._items = [];
7
+ }
8
+
9
+ async add(id, vector) {
10
+ this._items.push({ skill_id: id, vec: new Float32Array(vector) });
11
+ }
12
+
13
+ async addBatch(entries) {
14
+ for (const { id, vector } of entries) {
15
+ this._items.push({ skill_id: id, vec: new Float32Array(vector) });
16
+ }
17
+ }
18
+
19
+ async remove(id) {
20
+ this._items = this._items.filter(item => item.skill_id !== id);
21
+ }
22
+
23
+ async search(vector, topK = 20) {
24
+ const qArr = new Float32Array(vector);
25
+ const bestBySkill = new Map();
26
+ for (const entry of this._items) {
27
+ const score = cosineSim(qArr, entry.vec);
28
+ const prev = bestBySkill.get(entry.skill_id);
29
+ if (!prev || score > prev) bestBySkill.set(entry.skill_id, score);
30
+ }
31
+ return [...bestBySkill.entries()]
32
+ .sort((a, b) => b[1] - a[1])
33
+ .slice(0, topK)
34
+ .map(([skill_id, score]) => ({ skill_id, score }));
35
+ }
36
+
37
+ async build(entries) {
38
+ this._items = [];
39
+ for (const { skill_id, vector } of entries) {
40
+ this._items.push({ skill_id, vec: new Float32Array(vector) });
41
+ }
42
+ }
43
+
44
+ async clear() {
45
+ this._items = [];
46
+ }
47
+
48
+ get size() {
49
+ return this._items.length;
50
+ }
51
+ }
52
+
53
+ function cosineSim(a, b) {
54
+ let dot = 0, na = 0, nb = 0;
55
+ for (let i = 0; i < a.length; i++) {
56
+ dot += a[i] * b[i];
57
+ na += a[i] * a[i];
58
+ nb += b[i] * b[i];
59
+ }
60
+ return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-8);
61
+ }