@nosserb/prmt 0.0.0-stage → 2.0.0
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/LICENSE +21 -0
- package/README.md +191 -2
- package/bin/prmt.mjs +174 -0
- package/dist/assets/AuditModule-BOO_iRy8.js +1 -0
- package/dist/assets/Audits-yUBm0EpW.js +1 -0
- package/dist/assets/BrainPage-FIWO_9Nc.js +3 -0
- package/dist/assets/BrainPage-XQCEGRc5.css +1 -0
- package/dist/assets/Carte-CPuiR1Ha.css +1 -0
- package/dist/assets/Carte-xycu1mQn.js +1 -0
- package/dist/assets/Dashboard-DKvCQcX8.js +2 -0
- package/dist/assets/Dashboard-XdhC6_hM.css +1 -0
- package/dist/assets/Field-C7c2mCbb.js +2 -0
- package/dist/assets/Journal-BLIIlh80.css +1 -0
- package/dist/assets/Journal-BaelFjwV.js +1 -0
- package/dist/assets/Landing-B2jxV3zs.css +1 -0
- package/dist/assets/Landing-Dr612Zdu.js +26 -0
- package/dist/assets/Register-DA_Aq5f8.js +1 -0
- package/dist/assets/Reports-8wfDNSxH.js +1 -0
- package/dist/assets/Reports-B8GXN95N.css +1 -0
- package/dist/assets/SavoirPage-BTxbH3LY.css +1 -0
- package/dist/assets/SavoirPage-Dp3eUCHp.js +1 -0
- package/dist/assets/SettingsPage-BElIP46c.css +1 -0
- package/dist/assets/SettingsPage-BiQbnw3o.js +1 -0
- package/dist/assets/Shell-BAyHVljp.css +1 -0
- package/dist/assets/Shell-CQRlx9-V.js +2 -0
- package/dist/assets/Table-DUmGel5e.js +1 -0
- package/dist/assets/Tabs-CfhbELZk.js +1 -0
- package/dist/assets/Terminal-Bv1IJfi9.js +3 -0
- package/dist/assets/Terminal-CEfj38NV.css +1 -0
- package/dist/assets/arrow-right-Cx3pAEhl.js +1 -0
- package/dist/assets/auditViews-BMza2Sdz.js +1 -0
- package/dist/assets/auditViews-BePWj26h.css +1 -0
- package/dist/assets/bricolage-grotesque-latin-ext-opsz-normal-IcJDqblK.woff2 +0 -0
- package/dist/assets/bricolage-grotesque-latin-opsz-normal-Cre6nC2_.woff2 +0 -0
- package/dist/assets/bricolage-grotesque-vietnamese-opsz-normal-D9N6E8K1.woff2 +0 -0
- package/dist/assets/charts-BcijSJ4J.js +1 -0
- package/dist/assets/circle-stop-C33BAcKW.js +1 -0
- package/dist/assets/command-ChA49Xk7.js +1 -0
- package/dist/assets/command-Ck4pdQCv.css +1 -0
- package/dist/assets/download-CeuKOenD.js +1 -0
- package/dist/assets/eye-CXXHBBpr.js +1 -0
- package/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/assets/habillage-B5mMBadn.js +1 -0
- package/dist/assets/index-BH1etemR.css +1 -0
- package/dist/assets/index-COSiM-R9.js +14 -0
- package/dist/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
- package/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/assets/layers-2x-Bpkbi35X.png +0 -0
- package/dist/assets/layers-BWBAp2CZ.png +0 -0
- package/dist/assets/markdown-DF87LQh1.css +1 -0
- package/dist/assets/markdown-o5sMhPHN.js +4 -0
- package/dist/assets/marker-icon-hN30_KVU.png +0 -0
- package/dist/assets/play-Daw4K_73.js +1 -0
- package/dist/assets/plus-D_QHm8ud.js +1 -0
- package/dist/assets/trash-DLpHM-s8.js +1 -0
- package/dist/assets/web-6j1-aY67.js +1 -0
- package/dist/favicon.svg +1 -0
- package/dist/index.html +18 -0
- package/docs/ANALYSE.md +87 -0
- package/docs/SECURITE.md +188 -0
- package/lib/scripts/lier.js +15 -0
- package/lib/scripts/searxng.js +91 -0
- package/lib/server/app.js +444 -0
- package/lib/server/audits/modules/local.js +429 -0
- package/lib/server/audits/modules/network.js +331 -0
- package/lib/server/audits/modules/web.js +302 -0
- package/lib/server/audits/net.js +141 -0
- package/lib/server/audits/registry.js +202 -0
- package/lib/server/audits/types.js +58 -0
- package/lib/server/brain/brain.js +624 -0
- package/lib/server/brain/conversations.js +82 -0
- package/lib/server/brain/ollama.js +197 -0
- package/lib/server/brain/prompt.js +188 -0
- package/lib/server/brain/provider.js +1 -0
- package/lib/server/cli/bridge.js +292 -0
- package/lib/server/cli/pont.js +62 -0
- package/lib/server/config.js +68 -0
- package/lib/server/engine/analyzer.js +205 -0
- package/lib/server/engine/catalog.js +229 -0
- package/lib/server/engine/decoder.js +91 -0
- package/lib/server/engine/engine.js +194 -0
- package/lib/server/engine/paths.js +129 -0
- package/lib/server/engine/policy.js +397 -0
- package/lib/server/engine/relax.js +10 -0
- package/lib/server/engine/runner.js +193 -0
- package/lib/server/errors.js +23 -0
- package/lib/server/http/guard.js +75 -0
- package/lib/server/http/router.js +154 -0
- package/lib/server/http/static.js +76 -0
- package/lib/server/index.js +40 -0
- package/lib/server/journal.js +139 -0
- package/lib/server/lien.js +33 -0
- package/lib/server/savoir/base.js +284 -0
- package/lib/server/savoir/lecture.js +406 -0
- package/lib/server/savoir/memoire.js +115 -0
- package/lib/server/search/categories.js +179 -0
- package/lib/server/search/fiche.js +326 -0
- package/lib/server/search/lieux.js +134 -0
- package/lib/server/search/overpass.js +216 -0
- package/lib/server/search/searxng.js +223 -0
- package/lib/server/search/tiles.js +103 -0
- package/lib/server/status.js +157 -0
- package/lib/server/stores.js +141 -0
- package/lib/server/telephone/acces.js +457 -0
- package/lib/server/telephone/certificat.js +79 -0
- package/lib/server/telephone/qr.js +308 -0
- package/lib/shared/analysis.js +40 -0
- package/lib/shared/schemas.js +105 -0
- package/lib/shared/types.js +10 -0
- package/package.json +81 -4
- package/searxng/settings.yml +35 -0
- package/tui/Modelfile +23 -0
- package/tui/README.md +103 -0
- package/tui/prmt_tui.py +65 -0
- package/tui/promethee/__init__.py +1 -0
- package/tui/promethee/__main__.py +4 -0
- package/tui/promethee/accueil.py +66 -0
- package/tui/promethee/agents.py +220 -0
- package/tui/promethee/app/__init__.py +4 -0
- package/tui/promethee/app/application.py +299 -0
- package/tui/promethee/app/clavier.py +99 -0
- package/tui/promethee/app/commandes_slash.py +164 -0
- package/tui/promethee/app/contexte.py +188 -0
- package/tui/promethee/app/definitions.py +518 -0
- package/tui/promethee/app/ecran.py +177 -0
- package/tui/promethee/app/envoi.py +141 -0
- package/tui/promethee/app/historique.py +373 -0
- package/tui/promethee/app/modele.py +108 -0
- package/tui/promethee/app/notes.py +63 -0
- package/tui/promethee/app/outils.py +589 -0
- package/tui/promethee/app/plateforme.py +409 -0
- package/tui/promethee/app/saisie.py +100 -0
- package/tui/promethee/app/taches.py +92 -0
- package/tui/promethee/app/tour.py +539 -0
- package/tui/promethee/app/validation.py +210 -0
- package/tui/promethee/appels.py +35 -0
- package/tui/promethee/commandes.py +154 -0
- package/tui/promethee/completion.py +140 -0
- package/tui/promethee/config.py +12 -0
- package/tui/promethee/consignes.py +278 -0
- package/tui/promethee/conversation.py +94 -0
- package/tui/promethee/etat.py +253 -0
- package/tui/promethee/fichiers.py +354 -0
- package/tui/promethee/journal.py +116 -0
- package/tui/promethee/logo.py +132 -0
- package/tui/promethee/memoire.py +50 -0
- package/tui/promethee/ollama.py +52 -0
- package/tui/promethee/plan.py +91 -0
- package/tui/promethee/politique.py +429 -0
- package/tui/promethee/preferences.py +65 -0
- package/tui/promethee/presse_papiers.py +44 -0
- package/tui/promethee/recherche.py +581 -0
- package/tui/promethee/securite.py +204 -0
- package/tui/promethee/shell.py +190 -0
- package/tui/promethee/textes.py +190 -0
- package/tui/promethee/theme.py +92 -0
- package/tui/promethee/widgets.py +493 -0
- package/tui/requirements.txt +1 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Le savoir local : les pages lues après une recherche, découpées en passages, dans une base SQLite (data/savoir.db).
|
|
3
|
+
*
|
|
4
|
+
* Le modèle ne change pas : il relit ces passages avant de répondre. La recherche est plein texte (FTS5, accents ignorés,
|
|
5
|
+
* classement BM25), avec un radical grossier (« vulnérabilités » trouve « vulnerability ») et une mesure de couverture : un
|
|
6
|
+
* passage n'est relu hors recherche web que s'il contient l'essentiel des mots de la question.
|
|
7
|
+
*
|
|
8
|
+
* Taille bornée : au-delà de 15 millions de caractères, les pages les moins récemment utilisées partent.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
14
|
+
import { chunkText } from './lecture.js';
|
|
15
|
+
export const MAX_CHARS = 15_000_000;
|
|
16
|
+
/** Une page lue il y a moins de 7 jours n'est pas relue sur le web. */
|
|
17
|
+
export const FRESH_DAYS = 7;
|
|
18
|
+
const STOP = new Set(('les des une est pas pour que qui dans sur avec par plus sont aux ces ses son sa mon ma mes ton ta tes leur leurs nous vous ils elles elle lui ' +
|
|
19
|
+
'cette cet tout tous toute toutes comme mais donc car aussi bien fait faire peut peux veux quoi quel quelle quels quelles comment pourquoi ' +
|
|
20
|
+
'quand combien est-ce cela ceci etre avoir dois doit faut entre sans sous chez vers apres avant depuis encore deja tres trop alors ' +
|
|
21
|
+
'explique expliquer explication donne donner dire dis montre montrer parle parler resume resumer sais savoir connais connait ' +
|
|
22
|
+
'the and for with that this from are was were has have had not but you your what which who how why when where can does did its into ' +
|
|
23
|
+
'about will would should could there their them they then than also more most some any all our out use using').split(' '));
|
|
24
|
+
const plain = (s) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
|
|
25
|
+
/** Les mots qui comptent dans une question : sans accents, sans mots vides, 3 lettres au moins ou un chiffre. */
|
|
26
|
+
export function queryTerms(text) {
|
|
27
|
+
const terms = [];
|
|
28
|
+
for (const w of plain(text).split(/[^\p{L}\p{N}]+/u)) {
|
|
29
|
+
if (!w || STOP.has(w))
|
|
30
|
+
continue;
|
|
31
|
+
if (w.length < 3 && !/\d/.test(w))
|
|
32
|
+
continue;
|
|
33
|
+
if (!terms.includes(w))
|
|
34
|
+
terms.push(w);
|
|
35
|
+
}
|
|
36
|
+
return terms.slice(0, 16);
|
|
37
|
+
}
|
|
38
|
+
/** Radical grossier : les mots longs perdent leur fin (pluriel, accord, -ité / -ity), pour une recherche par préfixe. */
|
|
39
|
+
export const stem = (term) => (term.length > 5 && !/\d/.test(term) ? term.slice(0, Math.max(5, term.length - 2)) : term);
|
|
40
|
+
/**
|
|
41
|
+
* Équivalents anglais des mots les plus courants des questions : beaucoup de pages techniques sont en anglais, et
|
|
42
|
+
* « dernière version » ne trouverait jamais « latest release ». Une recherche par le sens (modèle d'embedding) ferait
|
|
43
|
+
* mieux, sans liste. Les mots dont le radical suffit (sécurité, vulnérabilité) n'y sont pas.
|
|
44
|
+
*/
|
|
45
|
+
const ALSO = {
|
|
46
|
+
dernier: ['latest', 'newest'],
|
|
47
|
+
derniere: ['latest', 'newest'],
|
|
48
|
+
derniers: ['latest'],
|
|
49
|
+
dernieres: ['latest'],
|
|
50
|
+
nouveaute: ['new', 'changelog'],
|
|
51
|
+
nouveautes: ['new', 'changelog'],
|
|
52
|
+
nouveau: ['new'],
|
|
53
|
+
nouvelle: ['new'],
|
|
54
|
+
nouvelles: ['new'],
|
|
55
|
+
neuf: ['new'],
|
|
56
|
+
version: ['release'],
|
|
57
|
+
versions: ['releases'],
|
|
58
|
+
sortie: ['release', 'released'],
|
|
59
|
+
sorti: ['released'],
|
|
60
|
+
faille: ['vulnerability', 'flaw'],
|
|
61
|
+
failles: ['vulnerabilities', 'flaws'],
|
|
62
|
+
correctif: ['patch', 'fix'],
|
|
63
|
+
correctifs: ['patches', 'fixes'],
|
|
64
|
+
telecharger: ['download'],
|
|
65
|
+
telechargement: ['download'],
|
|
66
|
+
reseau: ['network'],
|
|
67
|
+
logiciel: ['software'],
|
|
68
|
+
navigateur: ['browser'],
|
|
69
|
+
annonce: ['announcement', 'announced'],
|
|
70
|
+
};
|
|
71
|
+
/** Un mot de la question et ses équivalents : l'un d'eux suffit. */
|
|
72
|
+
export const variants = (term) => [term, ...(ALSO[term] ?? [])];
|
|
73
|
+
/** Requête FTS5 : chaque mot entre guillemets (aucun opérateur ne passe), en préfixe pour les mots longs, reliés par OR. */
|
|
74
|
+
export function ftsQuery(terms) {
|
|
75
|
+
return terms
|
|
76
|
+
.flatMap(variants)
|
|
77
|
+
.map((t) => (stem(t) !== t ? `"${stem(t)}"*` : `"${t}"`))
|
|
78
|
+
.join(' OR ');
|
|
79
|
+
}
|
|
80
|
+
/** Part des mots de la question présents dans un passage (par radical). */
|
|
81
|
+
export function coverage(terms, text) {
|
|
82
|
+
if (terms.length === 0)
|
|
83
|
+
return { found: 0, ratio: 0 };
|
|
84
|
+
const words = new Set(plain(text).split(/[^\p{L}\p{N}]+/u));
|
|
85
|
+
const all = [...words];
|
|
86
|
+
let found = 0;
|
|
87
|
+
const has = (t) => {
|
|
88
|
+
const s = stem(t);
|
|
89
|
+
return words.has(t) || (s !== t && all.some((w) => w.startsWith(s)));
|
|
90
|
+
};
|
|
91
|
+
for (const t of terms)
|
|
92
|
+
if (variants(t).some(has))
|
|
93
|
+
found++;
|
|
94
|
+
return { found, ratio: found / terms.length };
|
|
95
|
+
}
|
|
96
|
+
const toPage = (r) => ({ id: r.id, url: r.url, host: r.host, title: r.title, learnedAt: r.learned_at, usedAt: r.used_at, uses: r.uses, chars: r.chars, query: r.query });
|
|
97
|
+
export class Savoir {
|
|
98
|
+
db;
|
|
99
|
+
file;
|
|
100
|
+
now;
|
|
101
|
+
maxChars;
|
|
102
|
+
/** file null : base en mémoire (tests). */
|
|
103
|
+
constructor(file, options = {}) {
|
|
104
|
+
this.file = file;
|
|
105
|
+
this.now = options.now ?? Date.now;
|
|
106
|
+
this.maxChars = options.maxChars ?? MAX_CHARS;
|
|
107
|
+
if (file)
|
|
108
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
109
|
+
this.db = new DatabaseSync(file ?? ':memory:');
|
|
110
|
+
this.db.exec(`
|
|
111
|
+
PRAGMA journal_mode = WAL;
|
|
112
|
+
PRAGMA foreign_keys = ON;
|
|
113
|
+
CREATE TABLE IF NOT EXISTS pages (
|
|
114
|
+
id TEXT PRIMARY KEY,
|
|
115
|
+
url TEXT NOT NULL UNIQUE,
|
|
116
|
+
host TEXT NOT NULL,
|
|
117
|
+
title TEXT NOT NULL,
|
|
118
|
+
lang TEXT,
|
|
119
|
+
learned_at TEXT NOT NULL,
|
|
120
|
+
used_at TEXT,
|
|
121
|
+
uses INTEGER NOT NULL DEFAULT 0,
|
|
122
|
+
chars INTEGER NOT NULL,
|
|
123
|
+
query TEXT,
|
|
124
|
+
text TEXT NOT NULL
|
|
125
|
+
);
|
|
126
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS passages USING fts5(text, title, page_id UNINDEXED, ord UNINDEXED, tokenize = 'unicode61 remove_diacritics 2');
|
|
127
|
+
`);
|
|
128
|
+
}
|
|
129
|
+
close() {
|
|
130
|
+
this.db.close();
|
|
131
|
+
}
|
|
132
|
+
iso() {
|
|
133
|
+
return new Date(this.now()).toISOString();
|
|
134
|
+
}
|
|
135
|
+
/** La page déjà retenue pour cette adresse, si elle a moins de 7 jours. */
|
|
136
|
+
fresh(url) {
|
|
137
|
+
const row = this.db.prepare('SELECT id, url, host, title, learned_at, used_at, uses, chars, query FROM pages WHERE url = ?').get(url);
|
|
138
|
+
if (!row)
|
|
139
|
+
return null;
|
|
140
|
+
return this.now() - Date.parse(row.learned_at) < FRESH_DAYS * 86_400_000 ? toPage(row) : null;
|
|
141
|
+
}
|
|
142
|
+
/** Retient une page : son texte entier et ses passages. Une page déjà connue est remplacée. */
|
|
143
|
+
learn(input) {
|
|
144
|
+
const url = input.url;
|
|
145
|
+
const host = new URL(url).hostname.replace(/^www\./, '');
|
|
146
|
+
const id = createHash('sha256').update(url).digest('hex').slice(0, 16);
|
|
147
|
+
const title = (input.title || host).slice(0, 300);
|
|
148
|
+
const chunks = chunkText(input.text);
|
|
149
|
+
this.db.exec('BEGIN');
|
|
150
|
+
try {
|
|
151
|
+
this.db.prepare('DELETE FROM passages WHERE page_id = ?').run(id);
|
|
152
|
+
this.db.prepare('DELETE FROM pages WHERE id = ?').run(id);
|
|
153
|
+
this.db
|
|
154
|
+
.prepare('INSERT INTO pages (id, url, host, title, lang, learned_at, used_at, uses, chars, query, text) VALUES (?, ?, ?, ?, ?, ?, NULL, 0, ?, ?, ?)')
|
|
155
|
+
.run(id, url, host, title, input.lang, this.iso(), input.text.length, input.query, input.text);
|
|
156
|
+
const insert = this.db.prepare('INSERT INTO passages (text, title, page_id, ord) VALUES (?, ?, ?, ?)');
|
|
157
|
+
chunks.forEach((text, ord) => insert.run(text, title, id, ord));
|
|
158
|
+
this.db.exec('COMMIT');
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
this.db.exec('ROLLBACK');
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
this.evict();
|
|
165
|
+
return this.page(id);
|
|
166
|
+
}
|
|
167
|
+
page(id) {
|
|
168
|
+
const row = this.db.prepare('SELECT id, url, host, title, learned_at, used_at, uses, chars, query FROM pages WHERE id = ?').get(id);
|
|
169
|
+
return row ? toPage(row) : null;
|
|
170
|
+
}
|
|
171
|
+
/** Une page et son texte retenu. */
|
|
172
|
+
get(id) {
|
|
173
|
+
const row = this.db.prepare('SELECT id, url, host, title, learned_at, used_at, uses, chars, query, text FROM pages WHERE id = ?').get(id);
|
|
174
|
+
return row ? { page: toPage(row), text: row.text } : null;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Les passages les plus pertinents pour une question. only : seulement ces pages ; except : sauf celles-là.
|
|
178
|
+
* minRatio et minFound filtrent par couverture (0 : tout ce que FTS5 trouve).
|
|
179
|
+
*/
|
|
180
|
+
search(question, options = {}) {
|
|
181
|
+
const terms = queryTerms(question);
|
|
182
|
+
if (terms.length === 0)
|
|
183
|
+
return [];
|
|
184
|
+
const rows = this.db
|
|
185
|
+
.prepare(`SELECT p.id AS page_id, p.url, p.title, p.host, p.learned_at, s.ord, s.text, bm25(passages, 1.0, 0.5) AS score
|
|
186
|
+
FROM passages s JOIN pages p ON p.id = s.page_id
|
|
187
|
+
WHERE passages MATCH ? ORDER BY score LIMIT 200`)
|
|
188
|
+
.all(ftsQuery(terms));
|
|
189
|
+
const hits = [];
|
|
190
|
+
const perPage = new Map();
|
|
191
|
+
for (const r of rows) {
|
|
192
|
+
if (options.only && !options.only.includes(r.page_id))
|
|
193
|
+
continue;
|
|
194
|
+
if (options.except?.includes(r.page_id))
|
|
195
|
+
continue;
|
|
196
|
+
const { found, ratio } = coverage(terms, `${r.title} ${r.text}`);
|
|
197
|
+
if (ratio < (options.minRatio ?? 0) || found < (options.minFound ?? 0))
|
|
198
|
+
continue;
|
|
199
|
+
const n = perPage.get(r.page_id) ?? 0;
|
|
200
|
+
if (n >= (options.perPage ?? 2))
|
|
201
|
+
continue;
|
|
202
|
+
perPage.set(r.page_id, n + 1);
|
|
203
|
+
hits.push({ pageId: r.page_id, url: r.url, title: r.title, host: r.host, learnedAt: r.learned_at, ord: Number(r.ord), text: r.text, score: r.score, found, ratio });
|
|
204
|
+
if (hits.length >= (options.limit ?? 6))
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
return hits;
|
|
208
|
+
}
|
|
209
|
+
/** Les premiers passages d'une page : son début, quand aucun mot de la question n'y figure. */
|
|
210
|
+
opening(pageId, count = 1) {
|
|
211
|
+
const page = this.page(pageId);
|
|
212
|
+
if (!page)
|
|
213
|
+
return [];
|
|
214
|
+
const rows = this.db.prepare('SELECT ord, text FROM passages WHERE page_id = ? ORDER BY CAST(ord AS INTEGER) LIMIT ?').all(pageId, count);
|
|
215
|
+
return rows.map((r) => ({ pageId, url: page.url, title: page.title, host: page.host, learnedAt: page.learnedAt, ord: Number(r.ord), text: r.text, score: 0, found: 0, ratio: 0 }));
|
|
216
|
+
}
|
|
217
|
+
/** Note que des passages de ces pages ont été donnés au modèle. */
|
|
218
|
+
used(pageIds) {
|
|
219
|
+
const at = this.iso();
|
|
220
|
+
const stmt = this.db.prepare('UPDATE pages SET uses = uses + 1, used_at = ? WHERE id = ?');
|
|
221
|
+
for (const id of new Set(pageIds))
|
|
222
|
+
stmt.run(at, id);
|
|
223
|
+
}
|
|
224
|
+
list(options = {}) {
|
|
225
|
+
const limit = Math.min(Math.max(options.limit ?? 100, 1), 500);
|
|
226
|
+
const q = options.q?.trim();
|
|
227
|
+
let rows;
|
|
228
|
+
if (q) {
|
|
229
|
+
const like = `%${q.replace(/[\\%_]/g, (c) => `\\${c}`)}%`;
|
|
230
|
+
const terms = queryTerms(q);
|
|
231
|
+
const byText = terms.length
|
|
232
|
+
? this.db.prepare('SELECT DISTINCT page_id FROM passages WHERE passages MATCH ?').all(ftsQuery(terms)).map((r) => r.page_id)
|
|
233
|
+
: [];
|
|
234
|
+
rows = this.db
|
|
235
|
+
.prepare(`SELECT id, url, host, title, learned_at, used_at, uses, chars, query FROM pages
|
|
236
|
+
WHERE title LIKE ? ESCAPE '\\' OR url LIKE ? ESCAPE '\\' OR id IN (SELECT value FROM json_each(?))
|
|
237
|
+
ORDER BY COALESCE(used_at, learned_at) DESC LIMIT ?`)
|
|
238
|
+
.all(like, like, JSON.stringify(byText), limit);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
rows = this.db.prepare('SELECT id, url, host, title, learned_at, used_at, uses, chars, query FROM pages ORDER BY COALESCE(used_at, learned_at) DESC LIMIT ?').all(limit);
|
|
242
|
+
}
|
|
243
|
+
const totals = this.db.prepare('SELECT COUNT(*) AS n, COALESCE(SUM(chars), 0) AS c FROM pages').get();
|
|
244
|
+
return { pages: rows.map(toPage), total: Number(totals.n), chars: Number(totals.c), bytes: this.bytes() };
|
|
245
|
+
}
|
|
246
|
+
get size() {
|
|
247
|
+
return Number(this.db.prepare('SELECT COUNT(*) AS n FROM pages').get().n);
|
|
248
|
+
}
|
|
249
|
+
bytes() {
|
|
250
|
+
if (!this.file)
|
|
251
|
+
return 0;
|
|
252
|
+
let total = 0;
|
|
253
|
+
for (const suffix of ['', '-wal'])
|
|
254
|
+
total += fs.statSync(this.file + suffix, { throwIfNoEntry: false })?.size ?? 0;
|
|
255
|
+
return total;
|
|
256
|
+
}
|
|
257
|
+
forget(id) {
|
|
258
|
+
const page = this.page(id);
|
|
259
|
+
if (!page)
|
|
260
|
+
return null;
|
|
261
|
+
this.db.prepare('DELETE FROM passages WHERE page_id = ?').run(id);
|
|
262
|
+
this.db.prepare('DELETE FROM pages WHERE id = ?').run(id);
|
|
263
|
+
return page;
|
|
264
|
+
}
|
|
265
|
+
forgetAll() {
|
|
266
|
+
const n = this.size;
|
|
267
|
+
this.db.exec('DELETE FROM passages; DELETE FROM pages;');
|
|
268
|
+
this.db.exec('VACUUM');
|
|
269
|
+
return n;
|
|
270
|
+
}
|
|
271
|
+
/** Au-delà de MAX_CHARS, les pages les moins récemment utilisées partent, jusqu'à 90 % du plafond. */
|
|
272
|
+
evict(max = this.maxChars) {
|
|
273
|
+
let total = Number(this.db.prepare('SELECT COALESCE(SUM(chars), 0) AS c FROM pages').get().c);
|
|
274
|
+
if (total <= max)
|
|
275
|
+
return;
|
|
276
|
+
const old = this.db.prepare('SELECT id, chars FROM pages ORDER BY COALESCE(used_at, learned_at) ASC').all();
|
|
277
|
+
for (const p of old) {
|
|
278
|
+
if (total <= max * 0.9)
|
|
279
|
+
break;
|
|
280
|
+
this.forget(p.id);
|
|
281
|
+
total -= Number(p.chars);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|