Files
simonandCursor 1e1111c90f Qortino AI QDN assembly, share-to-Quitter, categorized saves, File shim fixes
Safety snapshot of mobile work before merging upstream chat-v2:
- Qortino AI: QDN pack discovery/download (Q-Share ids), external app
  storage, install validation, teach/report Q-Mail, PDF thumbs+zoom
- Android share target "Quitter" with native ShareReceiver plugin
- Categorized device saves (Images/Videos/Audio/Documents/Apps/GO state)
- createNamedFile helper: cordova-plugin-file clobbers global File

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 12:47:44 +00:00

320 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* High-impact Qortino answer audit (no LLM).
* Scores golden FAQ + app-catalog + QDN-nudge behaviour for paraphrases
* users actually type.
*
* Usage: node tools/qortino-knowledge/audit-answers.mjs
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
// Load bundled goldenAnswers (post-import)
const goldenPath = path.resolve(
__dirname,
'../../src/qortinoAgent/goldenAnswers.json'
);
const { entries: ENTRIES } = JSON.parse(fs.readFileSync(goldenPath, 'utf8'));
const STOP = new Set(
'a an the is are was were be to of in on for and or how do does did can could would should i me my you your we our please tell about with from what which when where who why'.split(
' '
)
);
function normalize(text) {
return text
.toLowerCase()
.replace(/[']/g, "'")
.replace(/[^a-z0-9\s']/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function tokens(text) {
return normalize(text)
.split(' ')
.filter((t) => t.length > 1 && !STOP.has(t));
}
function jaccard(a, b) {
if (!a.length || !b.length) return 0;
const A = new Set(a);
const B = new Set(b);
let inter = 0;
for (const t of A) if (B.has(t)) inter += 1;
const union = A.size + B.size - inter;
return union === 0 ? 0 : inter / union;
}
function phraseBonus(query, candidate) {
const q = normalize(query);
const c = normalize(candidate);
if (!q || !c) return 0;
if (q === c) return 1;
if (q.includes(c) || c.includes(q)) return 0.55;
if (q.length >= 12 && c.includes(q.slice(0, Math.min(24, q.length))))
return 0.25;
return 0;
}
function categoryRank(category, queryHasQortalCue) {
if (queryHasQortalCue && category === 'qortal') return 3;
if (category === 'qortal') return 2;
if (category === 'survival' || category === 'health') return 1;
if (category === 'programming') return -1;
return 0;
}
function rankGolden(query, topN = 3) {
const qNorm = normalize(query);
const qTokens = tokens(query);
const qortalCue =
/\b(qortal|qort|qdn|q-?app|q-?chat|q-?tube|hub|minter)\b/.test(qNorm) ||
/\b(trade\s+portal|seed\s*phrase|seedphrase|minting|nomination)\b/.test(
qNorm
);
const scored = [];
for (const entry of ENTRIES) {
const candidates = [entry.q, ...(entry.aliases || [])];
let local = 0;
for (const cand of candidates) {
const byPhrase = phraseBonus(qNorm, cand);
const byTok = jaccard(qTokens, tokens(cand));
const score = Math.max(byPhrase, byTok * 0.85 + byPhrase * 0.15);
const boosted = score + (entry.priority === 1 ? 0.03 : 0);
if (boosted > local) local = boosted;
}
scored.push({ entry, score: local });
}
scored.sort((a, b) => {
if (Math.abs(b.score - a.score) > 0.02) return b.score - a.score;
return (
categoryRank(b.entry.category, qortalCue) -
categoryRank(a.entry.category, qortalCue)
);
});
return scored.slice(0, topN);
}
/** High-impact paraphrase probes: expected category / id substring / must-not */
const PROBES = [
// Network / onboarding (highest traffic)
{ q: 'explain qortal simply', wantCat: 'qortal', wantId: /^qortal-001/, mustNot: /\bgame\b/i },
{ q: 'why should I use qortal', wantCat: 'qortal' },
{ q: 'is qortal decentralized', wantCat: 'qortal' },
{ q: 'how do I get started with qortal', wantCat: 'qortal', wantId: /^qortal-059/ },
{ q: 'what do I need to use qortal', wantCat: 'qortal' },
{ q: 'how do I create a wallet', wantCat: 'qortal' },
{ q: 'how do I backup my seed phrase', wantCat: 'qortal' },
{ q: 'what is a node', wantCat: 'qortal' },
{ q: 'do I need to run a node', wantCat: 'qortal' },
{ q: 'how do I sync my node', wantCat: 'qortal' },
{ q: 'what is level on qortal', wantCat: 'qortal' },
{ q: 'how do levels work', wantCat: 'qortal' },
{ q: 'how long to reach level 5', wantCat: 'qortal' },
{ q: 'what is proof of contribution', wantCat: 'qortal', wantId: /^qortal-005/ },
{ q: 'can I mint without a nomination', wantCat: 'qortal' },
{ q: 'how much qort do I need to start', wantCat: 'qortal' },
{ q: 'how do I get my first qort', wantCat: 'qortal' },
{ q: 'is qortal free', wantCat: 'qortal' },
{ q: 'who created qortal', wantCat: 'qortal' },
{ q: 'who founded qortal', wantCat: 'qortal' },
{ q: 'what is the difference between hub and go', wantCat: 'qortal' },
{ q: 'what is qortal go', wantCat: 'qortal' },
{ q: 'how do I publish a website', wantCat: 'qortal' },
{ q: 'how do I make a q-app', wantCat: 'qortal' },
{ q: 'what is an avatar on qortal', wantCat: 'qortal' },
{ q: 'how do groups work', wantCat: 'qortal' },
{ q: 'what is private messaging on qortal', wantCat: 'qortal' },
// Dangerous confusions
{ q: 'who is simon james', wantCat: /philosophy|qortal/, mustNot: /dishwasher|water filter/i },
{ q: 'what is playing qortal', wantCat: 'philosophy', mustNot: /^Qortal is a fully decentralized blockchain/ },
{ q: 'is qortal a crypto scam', wantCat: 'qortal' },
{ q: 'can I buy qort on binance', wantCat: 'qortal' },
// Survival / health (life-critical)
{ q: 'how do I make water safe to drink', wantCat: 'survival' },
{ q: 'can I drink rainwater', wantCat: 'survival' },
{ q: 'how do I filter river water', wantCat: 'survival' },
{ q: 'what if I have no fire to boil water', wantCat: 'survival' },
{ q: 'how do I start a fire without matches', wantCat: 'survival' },
{ q: 'how do I build a shelter outside', wantCat: 'survival' },
{ q: 'someone is not breathing what do I do', wantCat: /survival|health/ },
{ q: 'how do I do cpr', wantCat: /survival|health/ },
{ q: 'how do I treat a broken bone', wantCat: /survival|health/ },
{ q: 'snake bite what to do', wantCat: /survival|health/ },
{ q: 'how do I treat heat stroke', wantCat: /survival|health/ },
{ q: 'how do I treat frostbite', wantCat: /survival|health/ },
{ q: 'what foods store the longest', wantCat: /survival|gardening|life-skills/ },
{ q: 'how do I purify air in a smoke emergency', wantCat: /survival|health|earth-changes/ },
// Health
{ q: 'how much water should I drink a day', wantCat: 'health' },
{ q: 'how do I lower blood pressure naturally', wantCat: 'health' },
{ q: 'what helps with sleep', wantCat: 'health' },
{ q: 'how do I stop a panic attack', wantCat: /health|philosophy/ },
{ q: 'is intermittent fasting safe', wantCat: 'health' },
// Near-miss / soft-talk that should NOT steal FAQs
{ q: 'what is love', wantCat: /philosophy|life-skills/, forbidId: /^qortal-/ },
{ q: 'how do I grow mint', forbidId: /^qortal-00[5-8]/ }, // not minting
// Deep pass — common paraphrases / traps
{ q: 'how do I register a name on qortal', wantCat: 'qortal' },
{ q: 'how do I send qort to someone', wantCat: 'qortal' },
{ q: 'how do I join a group on qortal', wantCat: 'qortal' },
{ q: 'what is qdn', wantCat: 'qortal' },
{ q: 'what is a reward share', wantCat: 'qortal' },
{ q: 'how do I get nominated for minting', wantCat: 'qortal' },
{ q: 'can I use qortal without a computer', wantCat: 'qortal' },
{ q: 'is my seed phrase public', wantCat: 'qortal' },
{ q: 'how do I recover my wallet', wantCat: 'qortal' },
{ q: 'what is trade portal', wantCat: 'qortal' },
{ q: 'how do I stay cool in a heatwave', wantCat: /survival|health|earth-changes/ },
{ q: 'how do I purify water with bleach', wantCat: 'survival' },
{ q: 'what should I pack in a go bag', wantCat: /survival|life-skills/ },
{ q: 'how do I stop a nosebleed', wantCat: /survival|health/ },
{ q: 'how do I treat a burn', wantCat: /survival|health/ },
{ q: 'signs of a heart attack', wantCat: /survival|health/ },
{ q: 'how do I grow tomatoes', wantCat: 'gardening', forbidId: /^qortal-/ },
{ q: 'how do I save seeds', wantCat: 'gardening' },
{ q: 'what is composting', wantCat: 'gardening' },
{ q: 'how do I meditate', wantCat: /philosophy|health|life-skills/ },
];
const FAST = 0.58;
const MAX = 0.64;
const missFast = [];
const missMax = [];
const wrongCat = [];
const badContent = [];
const nearMiss = []; // has a decent #2 that might be better
for (const p of PROBES) {
const top = rankGolden(p.q, 3);
const best = top[0];
const hitFast = best && best.score >= FAST;
const hitMax = best && best.score >= MAX;
if (!hitFast) {
missFast.push({
q: p.q,
best: best
? { id: best.entry.id, cat: best.entry.category, score: +best.score.toFixed(3), q: best.entry.q }
: null,
top2: top[1]
? { id: top[1].entry.id, score: +top[1].score.toFixed(3) }
: null,
});
} else if (!hitMax) {
missMax.push({
q: p.q,
id: best.entry.id,
score: +best.score.toFixed(3),
note: 'works in Fast, fails Max threshold 0.64',
});
}
if (hitFast && p.wantCat) {
const catOk =
typeof p.wantCat === 'string'
? best.entry.category === p.wantCat
: p.wantCat.test(best.entry.category);
if (!catOk) {
wrongCat.push({
q: p.q,
got: `${best.entry.category}/${best.entry.id}`,
score: +best.score.toFixed(3),
wantCat: String(p.wantCat),
a: best.entry.a.slice(0, 100),
});
}
}
if (hitFast && p.wantId && !p.wantId.test(best.entry.id)) {
wrongCat.push({
q: p.q,
got: best.entry.id,
score: +best.score.toFixed(3),
wantId: String(p.wantId),
a: best.entry.a.slice(0, 100),
});
}
if (hitFast && p.forbidId && p.forbidId.test(best.entry.id)) {
badContent.push({
q: p.q,
problem: `hit forbidden id ${best.entry.id}`,
a: best.entry.a.slice(0, 120),
});
}
if (hitFast && p.mustNot && p.mustNot.test(best.entry.a)) {
badContent.push({
q: p.q,
problem: `answer matches mustNot ${p.mustNot}`,
id: best.entry.id,
a: best.entry.a.slice(0, 120),
});
}
// Close runners: #2 within 0.08 of #1 and different category
if (
top[0] &&
top[1] &&
top[0].score - top[1].score < 0.08 &&
top[0].entry.category !== top[1].entry.category &&
top[0].score >= 0.5
) {
nearMiss.push({
q: p.q,
a: { id: top[0].entry.id, cat: top[0].entry.category, score: +top[0].score.toFixed(3) },
b: { id: top[1].entry.id, cat: top[1].entry.category, score: +top[1].score.toFixed(3) },
});
}
}
// Cross-check: duplicate aliases that point to different answers
const aliasMap = new Map();
const dupAliases = [];
for (const e of ENTRIES) {
for (const a of [e.q, ...(e.aliases || [])]) {
const key = normalize(a);
if (!key) continue;
if (aliasMap.has(key) && aliasMap.get(key).id !== e.id) {
dupAliases.push({
alias: key,
a: aliasMap.get(key).id,
b: e.id,
});
} else {
aliasMap.set(key, e);
}
}
}
const report = {
summary: {
probes: PROBES.length,
missFast: missFast.length,
missMaxOnly: missMax.length,
wrongCatOrId: wrongCat.length,
badContent: badContent.length,
ambiguousNearMiss: nearMiss.length,
duplicateAliases: dupAliases.length,
},
missFast,
missMax,
wrongCat,
badContent,
nearMiss: nearMiss.slice(0, 25),
duplicateAliases: dupAliases.slice(0, 30),
};
console.log(JSON.stringify(report, null, 2));