create-byan-agent 2.4.6 → 2.5.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/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [2.5.0] - 2026-02-19
11
+
12
+ ### 🔄 Version Update
13
+
14
+ **Minor version bump to 2.5.0**
15
+
16
+ - Updated package version from 2.4.6 to 2.5.0
17
+ - Synchronized version references in README.md and package.json description
18
+ - Preparation for new features and improvements
19
+
20
+ ---
21
+
10
22
  ## [2.4.5] - 2026-02-12
11
23
 
12
24
  ### 🐛 Fixed - Copilot CLI Auth Command
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # BYAN v2.3.2 - Build Your AI Network
1
+ # BYAN v2.5.0 - Build Your AI Network
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/create-byan-agent.svg)](https://www.npmjs.com/package/create-byan-agent)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -9,7 +9,7 @@
9
9
 
10
10
  Create custom AI agents through intelligent interviews + **Hermes Universal Dispatcher** for intelligent routing across 35+ specialized agents.
11
11
 
12
- ### 🎯 New in v2.3.2: Hermes - Universal Dispatcher
12
+ ### 🎯 New in v2.5.0: Hermes - Universal Dispatcher
13
13
 
14
14
  **One entry point to rule them all** 🏛️
15
15
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.4.6",
4
- "description": "BYAN v2.3.2 - Intelligent AI agent ecosystem with Hermes universal dispatcher + Multi-platform support (Copilot CLI, Claude, Codex) + Automatic LLM cost optimization (87.5% savings) + Node 12+ compatible",
3
+ "version": "2.5.0",
4
+ "description": "BYAN v2.5.0 - Intelligent AI agent ecosystem with Hermes universal dispatcher + Multi-platform support (Copilot CLI, Claude, Codex) + Automatic LLM cost optimization (87.5% savings) + Node 12+ compatible",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "create-byan-agent": "./install/bin/create-byan-agent-v2.js",
@@ -9,6 +9,7 @@ class SessionState {
9
9
  this.analysisResults = {};
10
10
  this.agentProfileDraft = {};
11
11
  this.context = {};
12
+ this.facts = { verified: [], claims: [], disputed: [], opinions: [] };
12
13
  }
13
14
 
14
15
  addQuestion(question) {
@@ -26,6 +27,19 @@ class SessionState {
26
27
  });
27
28
  }
28
29
 
30
+ addFact(fact) {
31
+ if (!this.facts.claims) this.facts = { verified: [], claims: [], disputed: [], opinions: [] };
32
+ const target = fact.status === 'VERIFIED' ? 'verified'
33
+ : fact.status === 'DISPUTED' ? 'disputed'
34
+ : fact.status === 'OPINION' || fact.status === 'HYPOTHESIS' ? 'opinions'
35
+ : 'claims';
36
+ this.facts[target].push({ ...fact, session: this.sessionId, created_at: Date.now() });
37
+ }
38
+
39
+ getFacts() {
40
+ return JSON.parse(JSON.stringify(this.facts));
41
+ }
42
+
29
43
  setAnalysisResults(data) {
30
44
  this.analysisResults = data;
31
45
  }
@@ -78,7 +92,8 @@ class SessionState {
78
92
  userResponses: this.userResponses,
79
93
  analysisResults: this.analysisResults,
80
94
  agentProfileDraft: this.agentProfileDraft,
81
- context: this.context
95
+ context: this.context,
96
+ facts: this.facts
82
97
  };
83
98
  }
84
99
 
@@ -91,6 +106,7 @@ class SessionState {
91
106
  state.analysisResults = data.analysisResults || {};
92
107
  state.agentProfileDraft = data.agentProfileDraft || {};
93
108
  state.context = data.context || {};
109
+ state.facts = data.facts || { verified: [], claims: [], disputed: [], opinions: [] };
94
110
  return state;
95
111
  }
96
112
  }
@@ -13,9 +13,10 @@
13
13
  const Logger = require('../observability/logger');
14
14
 
15
15
  class FiveWhysAnalyzer {
16
- constructor(sessionState, logger) {
16
+ constructor(sessionState, logger, factChecker = null) {
17
17
  this.sessionState = sessionState;
18
18
  this.logger = logger || new Logger({ logDir: 'logs', logFile: 'five-whys.log' });
19
+ this.factChecker = factChecker;
19
20
 
20
21
  this.depth = 0;
21
22
  this.maxDepth = 5;
@@ -123,9 +124,17 @@ class FiveWhysAnalyzer {
123
124
  depth: this.depth,
124
125
  question: `Why #${this.depth}`,
125
126
  answer: answer.trim(),
126
- timestamp: new Date().toISOString()
127
+ timestamp: new Date().toISOString(),
128
+ assertionType: 'HYPOTHESIS'
127
129
  });
128
130
 
131
+ if (this.factChecker) {
132
+ const claims = this.factChecker.parse(answer);
133
+ if (claims.length > 0) {
134
+ this.logger.info('Fact-check triggered on WHY answer', { depth: this.depth, patterns: claims.map(c => c.matched) });
135
+ }
136
+ }
137
+
129
138
  const rootCauseAnalysis = this._analyzeForRootCause(answer);
130
139
 
131
140
  if (rootCauseAnalysis.isRootCause && this.depth >= 3) {
@@ -0,0 +1,51 @@
1
+ /**
2
+ * ClaimParser - Detects implicit claims in text using trigger patterns
3
+ *
4
+ * Auto-triggers fact-check when danger patterns are found :
5
+ * absolute words, superlatives, unverified best practices, etc.
6
+ */
7
+
8
+ const DEFAULT_PATTERNS = [
9
+ /\b(toujours|jamais|forcement|evidemment|clairement)\b/i,
10
+ /\b(always|never|obviously|clearly|certainly|definitely)\b/i,
11
+ /\b(plus rapide|plus sur|mieux|optimal|meilleur|superieur)\b/i,
12
+ /\b(faster|safer|better|optimal|superior|best)\b/i,
13
+ /\b(il est bien connu que|tout le monde sait|generalement accepte)\b/i,
14
+ /\b(it is well known that|everyone knows|generally accepted)\b/i,
15
+ /\b(bonne pratique|best practice|standard de facto|industry standard)\b/i,
16
+ /\b(prouve que|demontre que|il est clair que)\b/i,
17
+ /\b(proven|demonstrates that|it is clear that)\b/i
18
+ ];
19
+
20
+ class ClaimParser {
21
+ constructor(customPatterns = []) {
22
+ this.patterns = [
23
+ ...DEFAULT_PATTERNS,
24
+ ...customPatterns.map(p => new RegExp(p, 'i'))
25
+ ];
26
+ }
27
+
28
+ parse(text) {
29
+ if (typeof text !== 'string' || !text) return [];
30
+
31
+ const detected = [];
32
+ for (const pattern of this.patterns) {
33
+ const match = pattern.exec(text);
34
+ if (match) {
35
+ detected.push({
36
+ pattern: pattern.source,
37
+ matched: match[0],
38
+ position: match.index,
39
+ excerpt: text.slice(Math.max(0, match.index - 30), match.index + 60).trim()
40
+ });
41
+ }
42
+ }
43
+ return detected;
44
+ }
45
+
46
+ containsClaim(text) {
47
+ return this.parse(text).length > 0;
48
+ }
49
+ }
50
+
51
+ module.exports = ClaimParser;
@@ -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,259 @@
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
+ created.setDate(created.getDate() + halfLife);
176
+ return created.toISOString().slice(0, 10);
177
+ }
178
+
179
+ /**
180
+ * Check if a stored fact has expired
181
+ * @param {object} fact - must have created_at and domain
182
+ * @returns {{ expired: boolean, daysLeft: number|null, warning: string|null }}
183
+ */
184
+ checkExpiration(fact) {
185
+ if (!fact || !fact.created_at) throw new Error('fact.created_at is required');
186
+ const domain = fact.domain || 'general';
187
+ const expiry = this.expiresAt(domain, fact.created_at);
188
+
189
+ if (!expiry) return { expired: false, daysLeft: null, warning: null };
190
+
191
+ const now = new Date();
192
+ const expiryDate = new Date(expiry);
193
+ const daysLeft = Math.ceil((expiryDate - now) / (1000 * 60 * 60 * 24));
194
+
195
+ if (daysLeft <= 0) {
196
+ return { expired: true, daysLeft: 0, warning: `[EXPIRED] This fact (domain: ${domain}) is ${Math.abs(daysLeft)} days past its expiry date. Re-verify before using.` };
197
+ }
198
+ if (daysLeft <= 30) {
199
+ return { expired: false, daysLeft, warning: `[EXPIRING SOON] This fact expires in ${daysLeft} days. Consider re-verifying.` };
200
+ }
201
+ return { expired: false, daysLeft, warning: null };
202
+ }
203
+
204
+ /**
205
+ * Calculate confidence propagation through a reasoning chain
206
+ * Confidence degrades multiplicatively: 80% x 80% x 80% = 51%
207
+ * @param {Array<number>} scores - confidence scores for each chain step (0-100)
208
+ * @returns {{ finalScore: number, warning: string|null, steps: number }}
209
+ */
210
+ chain(scores) {
211
+ if (!Array.isArray(scores) || scores.length === 0) {
212
+ throw new Error('scores must be a non-empty array of numbers');
213
+ }
214
+ for (const s of scores) {
215
+ if (typeof s !== 'number' || s < 0 || s > 100) {
216
+ throw new Error('Each score must be a number between 0 and 100');
217
+ }
218
+ }
219
+
220
+ const finalScore = Math.round(
221
+ scores.reduce((acc, s) => acc * (s / 100), 1) * 100
222
+ );
223
+
224
+ let warning = null;
225
+ if (scores.length > 3) {
226
+ 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.`;
227
+ } else if (finalScore < 60) {
228
+ warning = `Chain confidence is ${finalScore}% — below 60% threshold. This conclusion should not be presented as a firm recommendation.`;
229
+ }
230
+
231
+ return { finalScore, steps: scores.length, warning };
232
+ }
233
+
234
+ _loadKnowledgeBase() {
235
+ if (this._knowledgeBase) return this._knowledgeBase;
236
+ try {
237
+ this._knowledgeBase = fs.readFileSync(
238
+ path.resolve(this.config.knowledge_base), 'utf8'
239
+ );
240
+ } catch {
241
+ this._knowledgeBase = '';
242
+ }
243
+ return this._knowledgeBase;
244
+ }
245
+
246
+ _loadAxioms() {
247
+ if (this._axioms) return this._axioms;
248
+ try {
249
+ this._axioms = fs.readFileSync(
250
+ path.resolve(this.config.axioms), 'utf8'
251
+ );
252
+ } catch {
253
+ this._axioms = '';
254
+ }
255
+ return this._axioms;
256
+ }
257
+ }
258
+
259
+ 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;