create-byan-agent 2.4.6 → 2.6.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.
@@ -0,0 +1,96 @@
1
+ /**
2
+ * FactSheet - Generates a Markdown fact sheet from session facts
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ class FactSheet {
9
+ constructor(outputPath = '_byan-output/fact-sheets') {
10
+ this.outputPath = outputPath;
11
+ }
12
+
13
+ generate(sessionId, facts) {
14
+ if (!sessionId) throw new Error('sessionId is required');
15
+ if (!facts || typeof facts !== 'object') throw new Error('facts must be an object');
16
+
17
+ const { verified = [], claims = [], disputed = [], opinions = [] } = facts;
18
+ const total = verified.length + claims.length + disputed.length + opinions.length;
19
+ const sourced = verified.length + claims.length;
20
+ const trustScore = total > 0 ? Math.round((sourced / total) * 100) : 100;
21
+ const badge = FactSheet.trustBadge(trustScore);
22
+
23
+ const date = new Date().toISOString().slice(0, 10);
24
+ const lines = [
25
+ `# Fact Sheet — Session ${sessionId}`,
26
+ `**Date :** ${date}`,
27
+ `**Truth Score :** ${trustScore}% (${sourced}/${total} claims sourced) ${badge}`,
28
+ ''
29
+ ];
30
+
31
+ if (verified.length > 0) {
32
+ lines.push('## Facts verifies par l\'utilisateur');
33
+ verified.forEach(f => lines.push(`- [FACT USER-VERIFIED] ${f.claim}`));
34
+ lines.push('');
35
+ }
36
+
37
+ if (claims.length > 0) {
38
+ lines.push('## Claims sources (LEVEL-1 a LEVEL-3)');
39
+ claims.forEach(f => {
40
+ const src = f.source ? ` — ${f.source.url || f.source}` : '';
41
+ const proof = f.proof ? ` — proof: ${f.proof.content || f.proof}` : '';
42
+ lines.push(`- [CLAIM L${f.level || '?'}] ${f.claim}${src}${proof}`);
43
+ });
44
+ lines.push('');
45
+ }
46
+
47
+ if (disputed.length > 0) {
48
+ lines.push('## Claims disputes (sources contradictoires)');
49
+ disputed.forEach(f => lines.push(`- [DISPUTED] ${f.claim}`));
50
+ lines.push('');
51
+ }
52
+
53
+ if (opinions.length > 0) {
54
+ lines.push('## Opinions et hypotheses declarees');
55
+ opinions.forEach(f => {
56
+ const prefix = f.status === 'HYPOTHESIS' ? '[HYPOTHESIS]' : '[OPINION]';
57
+ lines.push(`- ${prefix} ${f.claim}`);
58
+ });
59
+ lines.push('');
60
+ }
61
+
62
+ if (trustScore < 80) {
63
+ lines.push('## Avertissement');
64
+ lines.push(`Truth Score de ${trustScore}% — plus de ${100 - trustScore}% des assertions ne sont pas sourcees.`);
65
+ lines.push('');
66
+ }
67
+
68
+ return lines.join('\n');
69
+ }
70
+
71
+ /**
72
+ * Returns a text badge representing the trust level
73
+ * @param {number} score - 0 to 100
74
+ * @returns {string}
75
+ */
76
+ static trustBadge(score) {
77
+ if (score >= 90) return '[Trust: A — Excellent]';
78
+ if (score >= 75) return '[Trust: B — Bon]';
79
+ if (score >= 60) return '[Trust: C — Acceptable]';
80
+ if (score >= 40) return '[Trust: D — Faible]';
81
+ return '[Trust: F — Non-source]';
82
+ }
83
+
84
+ save(sessionId, facts) {
85
+ const content = this.generate(sessionId, facts);
86
+ const date = new Date().toISOString().slice(0, 10);
87
+ const filename = `session-${date}-${sessionId.slice(0, 8)}.md`;
88
+ const fullPath = path.join(this.outputPath, filename);
89
+
90
+ fs.mkdirSync(this.outputPath, { recursive: true });
91
+ fs.writeFileSync(fullPath, content, 'utf8');
92
+ return fullPath;
93
+ }
94
+ }
95
+
96
+ module.exports = FactSheet;
@@ -0,0 +1,263 @@
1
+ /**
2
+ * FactChecker - Scientific fact-checking for BYAN
3
+ *
4
+ * Principles : demonstrable, quantifiable, reproducible
5
+ * Method : 4 assertion types x 5 proof levels
6
+ * Rule : Never generate a URL — only cite verified knowledge base
7
+ *
8
+ * @class FactChecker
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const LevelScorer = require('./level-scorer');
14
+ const ClaimParser = require('./claim-parser');
15
+ const FactSheet = require('./fact-sheet');
16
+ const KnowledgeGraph = require('./knowledge-graph');
17
+
18
+ const ASSERTION_TYPES = ['REASONING', 'HYPOTHESIS', 'CLAIM', 'FACT'];
19
+
20
+ // Fact half-lives in days by domain (null = never expires)
21
+ const DEFAULT_HALF_LIVES = {
22
+ security: 180, // 6 months — CVEs and vulns change fast
23
+ performance: 365, // 1 year — benchmarks depend on versions
24
+ compliance: 180, // 6 months — regulations evolve
25
+ javascript: 365, // 1 year — ecosystem moves fast
26
+ general: 730, // 2 years — general tech claims
27
+ algorithms: null // never — Big O does not change
28
+ };
29
+
30
+ class FactChecker {
31
+ constructor(config = {}, sessionState = null) {
32
+ this.config = {
33
+ enabled: true,
34
+ mode: 'offline',
35
+ min_level: 3,
36
+ strict_domains: ['security', 'performance', 'compliance'],
37
+ output_fact_sheet: true,
38
+ fact_sheet_path: '_byan-output/fact-sheets',
39
+ knowledge_base: '_byan/knowledge/sources.md',
40
+ axioms: '_byan/knowledge/axioms.md',
41
+ half_lives: DEFAULT_HALF_LIVES,
42
+ ...config
43
+ };
44
+ this.sessionState = sessionState;
45
+ this.graph = new KnowledgeGraph(this.config.graph_path || '_byan/_memory/fact-graph.json');
46
+ this.scorer = new LevelScorer();
47
+ this.parser = new ClaimParser(config.auto_trigger_patterns || []);
48
+ this.sheet = new FactSheet(this.config.fact_sheet_path);
49
+ this._knowledgeBase = null;
50
+ this._axioms = null;
51
+ }
52
+
53
+ /**
54
+ * Check a claim against the knowledge base
55
+ * @param {string} claim
56
+ * @param {object} options - { level, source, proof, domain }
57
+ * @returns {{ status, level, score, assertionType, message }}
58
+ */
59
+ check(claim, options = {}) {
60
+ if (typeof claim !== 'string' || !claim) {
61
+ throw new Error('claim must be a non-empty string');
62
+ }
63
+
64
+ const { level = 5, source = null, proof = null, domain = null } = options;
65
+
66
+ if (level < 1 || level > 5) throw new Error('level must be between 1 and 5');
67
+
68
+ if (domain && this.scorer.isBlockedInDomain(level, domain)) {
69
+ return {
70
+ status: 'BLOCKED',
71
+ level,
72
+ score: this.scorer.score(level),
73
+ assertionType: 'OPINION',
74
+ message: `Domain "${domain}" requires LEVEL-${this.config.strict_domains.includes(domain) ? 2 : 1} minimum. Got LEVEL-${level}.`
75
+ };
76
+ }
77
+
78
+ if (level > this.config.min_level) {
79
+ return {
80
+ status: 'OPINION',
81
+ level,
82
+ score: this.scorer.score(level),
83
+ assertionType: 'HYPOTHESIS',
84
+ message: `LEVEL-${level} is below min_level (${this.config.min_level}). Marked as HYPOTHESIS.`
85
+ };
86
+ }
87
+
88
+ const fact = {
89
+ claim,
90
+ level,
91
+ source,
92
+ proof,
93
+ status: 'CLAIM',
94
+ confidence: this.scorer.score(level),
95
+ assertionType: 'CLAIM'
96
+ };
97
+
98
+ if (this.sessionState) this.sessionState.addFact(fact);
99
+ this.graph.add({ ...fact, domain: domain || 'general', expires_at: this.expiresAt(domain || 'general') });
100
+
101
+ return {
102
+ status: 'CLAIM',
103
+ level,
104
+ score: fact.confidence,
105
+ assertionType: 'CLAIM',
106
+ message: `[CLAIM L${level}] ${claim}${source ? ' — ' + (source.url || source) : ''}${proof ? ' — proof: ' + (proof.content || proof) : ''}`
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Register a user-verified fact with proof artifact
112
+ * @param {string} claim
113
+ * @param {string|object} proof - command output, screenshot path, log excerpt
114
+ * @returns {{ status, message }}
115
+ */
116
+ verify(claim, proof) {
117
+ if (typeof claim !== 'string' || !claim) throw new Error('claim must be a non-empty string');
118
+ if (!proof) throw new Error('proof artifact is required for USER-VERIFIED facts');
119
+
120
+ const date = new Date().toISOString().slice(0, 10);
121
+ const fact = {
122
+ claim,
123
+ level: 1,
124
+ proof: typeof proof === 'string' ? { type: 'user-provided', content: proof } : proof,
125
+ status: 'VERIFIED',
126
+ confidence: 100,
127
+ assertionType: 'FACT',
128
+ verified_at: date
129
+ };
130
+
131
+ if (this.sessionState) this.sessionState.addFact(fact);
132
+ this.graph.add({ ...fact, domain: 'verified', expires_at: null });
133
+
134
+ return {
135
+ status: 'VERIFIED',
136
+ message: `[FACT USER-VERIFIED ${date}] ${claim}`
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Detect implicit claims in a text using trigger patterns
142
+ * @param {string} text
143
+ * @returns {Array<{ matched, excerpt, position }>}
144
+ */
145
+ parse(text) {
146
+ return this.parser.parse(text);
147
+ }
148
+
149
+ /**
150
+ * Generate and optionally save the session Fact Sheet
151
+ * @param {string} sessionId
152
+ * @param {object} facts - { verified, claims, disputed, opinions }
153
+ * @param {boolean} save - write to disk
154
+ * @returns {{ content, filePath }}
155
+ */
156
+ generateFactSheet(sessionId, facts, save = true) {
157
+ const content = this.sheet.generate(sessionId, facts);
158
+ let filePath = null;
159
+ if (save && this.config.output_fact_sheet) {
160
+ filePath = this.sheet.save(sessionId, facts);
161
+ }
162
+ return { content, filePath };
163
+ }
164
+
165
+ /**
166
+ * Calculate expiry date for a fact based on its domain
167
+ * @param {string} domain
168
+ * @param {string} createdAt - ISO date string
169
+ * @returns {string|null} ISO expiry date or null if never expires
170
+ */
171
+ expiresAt(domain, createdAt = new Date().toISOString()) {
172
+ const halfLife = (this.config.half_lives || DEFAULT_HALF_LIVES)[domain];
173
+ if (halfLife === null || halfLife === undefined) return null;
174
+ const created = new Date(createdAt);
175
+ const expiry = new Date(Date.UTC(
176
+ created.getUTCFullYear(),
177
+ created.getUTCMonth(),
178
+ created.getUTCDate() + halfLife
179
+ ));
180
+ return expiry.toISOString().slice(0, 10);
181
+ }
182
+
183
+ /**
184
+ * Check if a stored fact has expired
185
+ * @param {object} fact - must have created_at and domain
186
+ * @returns {{ expired: boolean, daysLeft: number|null, warning: string|null }}
187
+ */
188
+ checkExpiration(fact) {
189
+ if (!fact || !fact.created_at) throw new Error('fact.created_at is required');
190
+ const domain = fact.domain || 'general';
191
+ const expiry = this.expiresAt(domain, fact.created_at);
192
+
193
+ if (!expiry) return { expired: false, daysLeft: null, warning: null };
194
+
195
+ const now = new Date();
196
+ const expiryDate = new Date(expiry);
197
+ const daysLeft = Math.ceil((expiryDate - now) / (1000 * 60 * 60 * 24));
198
+
199
+ if (daysLeft <= 0) {
200
+ return { expired: true, daysLeft: 0, warning: `[EXPIRED] This fact (domain: ${domain}) is ${Math.abs(daysLeft)} days past its expiry date. Re-verify before using.` };
201
+ }
202
+ if (daysLeft <= 30) {
203
+ return { expired: false, daysLeft, warning: `[EXPIRING SOON] This fact expires in ${daysLeft} days. Consider re-verifying.` };
204
+ }
205
+ return { expired: false, daysLeft, warning: null };
206
+ }
207
+
208
+ /**
209
+ * Calculate confidence propagation through a reasoning chain
210
+ * Confidence degrades multiplicatively: 80% x 80% x 80% = 51%
211
+ * @param {Array<number>} scores - confidence scores for each chain step (0-100)
212
+ * @returns {{ finalScore: number, warning: string|null, steps: number }}
213
+ */
214
+ chain(scores) {
215
+ if (!Array.isArray(scores) || scores.length === 0) {
216
+ throw new Error('scores must be a non-empty array of numbers');
217
+ }
218
+ for (const s of scores) {
219
+ if (typeof s !== 'number' || s < 0 || s > 100) {
220
+ throw new Error('Each score must be a number between 0 and 100');
221
+ }
222
+ }
223
+
224
+ const finalScore = Math.round(
225
+ scores.reduce((acc, s) => acc * (s / 100), 1) * 100
226
+ );
227
+
228
+ let warning = null;
229
+ if (scores.length > 3) {
230
+ warning = `Chain of ${scores.length} steps detected. Confidence degraded to ${finalScore}% (${scores.join('% x ')}%). Consider finding a direct source instead of a long deduction chain.`;
231
+ } else if (finalScore < 60) {
232
+ warning = `Chain confidence is ${finalScore}% — below 60% threshold. This conclusion should not be presented as a firm recommendation.`;
233
+ }
234
+
235
+ return { finalScore, steps: scores.length, warning };
236
+ }
237
+
238
+ _loadKnowledgeBase() {
239
+ if (this._knowledgeBase) return this._knowledgeBase;
240
+ try {
241
+ this._knowledgeBase = fs.readFileSync(
242
+ path.resolve(this.config.knowledge_base), 'utf8'
243
+ );
244
+ } catch {
245
+ this._knowledgeBase = '';
246
+ }
247
+ return this._knowledgeBase;
248
+ }
249
+
250
+ _loadAxioms() {
251
+ if (this._axioms) return this._axioms;
252
+ try {
253
+ this._axioms = fs.readFileSync(
254
+ path.resolve(this.config.axioms), 'utf8'
255
+ );
256
+ } catch {
257
+ this._axioms = '';
258
+ }
259
+ return this._axioms;
260
+ }
261
+ }
262
+
263
+ module.exports = FactChecker;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * KnowledgeGraph - Persistent fact store across BYAN sessions
3
+ *
4
+ * Storage: _byan/_memory/fact-graph.json
5
+ * Each entry: { id, claim, domain, status, confidence, created_at, expires_at, source, session_id }
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const crypto = require('crypto');
11
+
12
+ class KnowledgeGraph {
13
+ constructor(storagePath = '_byan/_memory/fact-graph.json') {
14
+ this.storagePath = storagePath;
15
+ this._data = null;
16
+ }
17
+
18
+ load() {
19
+ if (this._data) return this._data;
20
+ try {
21
+ const raw = fs.readFileSync(path.resolve(this.storagePath), 'utf8');
22
+ this._data = JSON.parse(raw);
23
+ } catch {
24
+ this._data = { version: 1, facts: [], updated_at: null };
25
+ }
26
+ return this._data;
27
+ }
28
+
29
+ save() {
30
+ const data = this.load();
31
+ data.updated_at = new Date().toISOString();
32
+ const dir = path.dirname(path.resolve(this.storagePath));
33
+ fs.mkdirSync(dir, { recursive: true });
34
+ fs.writeFileSync(path.resolve(this.storagePath), JSON.stringify(data, null, 2), 'utf8');
35
+ }
36
+
37
+ /**
38
+ * Add a fact to the graph. Deduplicates by claim text (updates existing).
39
+ * @param {object} fact - { claim, domain, status, confidence, source, session_id, expires_at }
40
+ * @returns {string} fact id
41
+ */
42
+ add(fact) {
43
+ if (!fact || !fact.claim) throw new Error('fact.claim is required');
44
+ const data = this.load();
45
+ const existing = data.facts.find(f => f.claim === fact.claim && f.domain === fact.domain);
46
+
47
+ if (existing) {
48
+ Object.assign(existing, fact, { updated_at: new Date().toISOString() });
49
+ this.save();
50
+ return existing.id;
51
+ }
52
+
53
+ const id = crypto.createHash('md5').update(fact.claim + (fact.domain || '')).digest('hex').slice(0, 8);
54
+ const entry = {
55
+ id,
56
+ claim: fact.claim,
57
+ domain: fact.domain || 'general',
58
+ status: fact.status || 'CLAIM',
59
+ confidence: fact.confidence || 50,
60
+ source: fact.source || null,
61
+ session_id: fact.session_id || null,
62
+ expires_at: fact.expires_at || null,
63
+ created_at: fact.created_at || new Date().toISOString().slice(0, 10),
64
+ updated_at: new Date().toISOString()
65
+ };
66
+
67
+ data.facts.push(entry);
68
+ this.save();
69
+ return id;
70
+ }
71
+
72
+ /**
73
+ * Query facts with optional filters
74
+ * @param {object} filters - { domain, status, expiredOnly, sessionId }
75
+ * @returns {Array}
76
+ */
77
+ query({ domain, status, expiredOnly, sessionId } = {}) {
78
+ const { facts } = this.load();
79
+ return facts.filter(f => {
80
+ if (domain && f.domain !== domain) return false;
81
+ if (status && f.status !== status) return false;
82
+ if (sessionId && f.session_id !== sessionId) return false;
83
+ if (expiredOnly) {
84
+ if (!f.expires_at) return false;
85
+ return new Date(f.expires_at) < new Date();
86
+ }
87
+ return true;
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Run expiration check on all facts using FactChecker.checkExpiration
93
+ * @param {object} checker - FactChecker instance
94
+ * @returns {{ expired: Array, expiringSoon: Array, healthy: Array }}
95
+ */
96
+ audit(checker) {
97
+ const { facts } = this.load();
98
+ const result = { expired: [], expiringSoon: [], healthy: [] };
99
+
100
+ for (const fact of facts) {
101
+ const check = checker.checkExpiration({
102
+ claim: fact.claim,
103
+ domain: fact.domain,
104
+ created_at: fact.created_at
105
+ });
106
+
107
+ if (check.expired) {
108
+ result.expired.push({ ...fact, _check: check });
109
+ } else if (check.warning) {
110
+ result.expiringSoon.push({ ...fact, _check: check });
111
+ } else {
112
+ result.healthy.push(fact);
113
+ }
114
+ }
115
+
116
+ return result;
117
+ }
118
+
119
+ /**
120
+ * Remove all expired facts from the graph
121
+ * @param {object} checker - FactChecker instance
122
+ * @returns {number} count of pruned facts
123
+ */
124
+ prune(checker) {
125
+ const { expired } = this.audit(checker);
126
+ const data = this.load();
127
+ const expiredIds = new Set(expired.map(f => f.id));
128
+ const before = data.facts.length;
129
+ data.facts = data.facts.filter(f => !expiredIds.has(f.id));
130
+ this.save();
131
+ return before - data.facts.length;
132
+ }
133
+
134
+ /**
135
+ * Statistics by domain and status
136
+ * @returns {object}
137
+ */
138
+ stats() {
139
+ const { facts } = this.load();
140
+ const byDomain = {};
141
+ const byStatus = {};
142
+
143
+ for (const f of facts) {
144
+ byDomain[f.domain] = (byDomain[f.domain] || 0) + 1;
145
+ byStatus[f.status] = (byStatus[f.status] || 0) + 1;
146
+ }
147
+
148
+ return { total: facts.length, byDomain, byStatus };
149
+ }
150
+ }
151
+
152
+ module.exports = KnowledgeGraph;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * LevelScorer - Scores a fact's confidence based on its proof level
3
+ *
4
+ * LEVEL-1 : Official spec / RFC / Primary documentation → 95
5
+ * LEVEL-2 : Reproducible benchmark / Executable code → 80
6
+ * LEVEL-3 : Peer-reviewed article / Independent source → 65
7
+ * LEVEL-4 : Community consensus (> 1000 votes) → 50
8
+ * LEVEL-5 : Opinion / Personal experience → 20
9
+ */
10
+
11
+ const LEVEL_SCORES = { 1: 95, 2: 80, 3: 65, 4: 50, 5: 20 };
12
+
13
+ const STRICT_DOMAIN_MIN_LEVEL = {
14
+ security: 2,
15
+ performance: 2,
16
+ compliance: 1
17
+ };
18
+
19
+ class LevelScorer {
20
+ score(level) {
21
+ if (!Number.isInteger(level) || level < 1 || level > 5) {
22
+ throw new Error('Level must be an integer between 1 and 5');
23
+ }
24
+ return LEVEL_SCORES[level];
25
+ }
26
+
27
+ isBlockedInDomain(level, domain) {
28
+ const minLevel = STRICT_DOMAIN_MIN_LEVEL[domain];
29
+ if (!minLevel) return false;
30
+ return level > minLevel;
31
+ }
32
+
33
+ describeLevel(level) {
34
+ const descriptions = {
35
+ 1: 'Official spec / RFC / Primary documentation',
36
+ 2: 'Reproducible benchmark / Executable proof',
37
+ 3: 'Peer-reviewed / Independent source',
38
+ 4: 'Community consensus',
39
+ 5: 'Opinion / Personal experience'
40
+ };
41
+ return descriptions[level] || 'Unknown level';
42
+ }
43
+ }
44
+
45
+ module.exports = LevelScorer;