mycai 1.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.
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Resonance SDK - Express.js Microservice Integration Example
3
+ * Embeds Resonance AI Engine into a secure B2B REST microservice.
4
+ *
5
+ * Run with: node sdk/examples/express_integration.js
6
+ */
7
+
8
+ import express from 'express';
9
+ import { ResonanceEngine, LicenseManager } from '../index.js';
10
+
11
+ const app = express();
12
+ app.use(express.json());
13
+
14
+ // Initialize Sovereign / Business Engine
15
+ const licenseKey = process.env.RESONANCE_LICENSE_KEY || LicenseManager.generateLicenseKey({
16
+ tier: 'SOVEREIGN',
17
+ expiresAt: Date.now() + 365 * 24 * 3600 * 1000,
18
+ clientId: 'internal-enterprise'
19
+ });
20
+
21
+ const engine = new ResonanceEngine({ licenseKey });
22
+ await engine.init();
23
+
24
+ // 1. Q&A and Reasoning Endpoint (< 1ms)
25
+ app.post('/api/ask', async (req, res) => {
26
+ try {
27
+ const { prompt } = req.body;
28
+ const result = await engine.ask(prompt);
29
+ res.json(result);
30
+ } catch (err) {
31
+ res.status(500).json({ error: err.message });
32
+ }
33
+ });
34
+
35
+ // 2. Document Ingestion Endpoint
36
+ app.post('/api/ingest', async (req, res) => {
37
+ try {
38
+ const { data, type, title, metadata } = req.body;
39
+ const result = await engine.ingest({ data, type, title, metadata });
40
+ res.json(result);
41
+ } catch (err) {
42
+ res.status(400).json({ error: err.message });
43
+ }
44
+ });
45
+
46
+ // 3. Summarization Endpoint
47
+ app.post('/api/summarize', (req, res) => {
48
+ const { page, document, mode } = req.body;
49
+ const summary = engine.summarize({ page, document, mode });
50
+ res.json({ summary });
51
+ });
52
+
53
+ // 4. Telemetry Endpoint
54
+ app.get('/api/telemetry', (req, res) => {
55
+ res.json(engine.getTelemetry());
56
+ });
57
+
58
+ const PORT = process.env.PORT || 4100;
59
+ app.listen(PORT, () => {
60
+ console.log(`📡 Resonance B2B Microservice active on http://localhost:${PORT}`);
61
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Resonance SDK v1.0 - Node.js Runtime Example
3
+ * Demonstrates basic text generation and morphology analysis in Node.js.
4
+ */
5
+
6
+ import { ResonanceSDK } from '../resonance_sdk.js';
7
+
8
+ async function run() {
9
+ console.log('--- Resonance SDK Node.js Example ---');
10
+
11
+ const sdk = new ResonanceSDK({ temperature: 0.5, maxLength: 8 });
12
+
13
+ console.log('Initializing SDK...');
14
+ await sdk.init();
15
+ console.log('SDK Metrics:', sdk.getMetrics());
16
+
17
+ // Generate text
18
+ console.log('\nGenerating text...');
19
+ const prompt = "yeni bir kitap";
20
+ const result = sdk.generate(prompt);
21
+ console.log(`Prompt: "${prompt}"`);
22
+ console.log(`Generated: "${result.generatedText}"`);
23
+ console.log(`Latency: ${result.totalLatencyMs.toFixed(2)} ms`);
24
+
25
+ // Analyze word
26
+ console.log('\nAnalyzing word morphology...');
27
+ const word = "evlerimizden";
28
+ const analysis = sdk.analyze(word);
29
+ console.log(`Word: "${word}"`);
30
+ console.log('Analysis:', analysis);
31
+ }
32
+
33
+ run().catch(console.error);
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Resonance SDK - 60 Second B2B Quickstart
3
+ * Run with: node sdk/examples/quickstart.js
4
+ */
5
+
6
+ import { ResonanceEngine, LicenseManager, FileStorageAdapter } from '../index.js';
7
+
8
+ async function main() {
9
+ console.log('🚀 Initializing Resonance Engine (B2B Infrastructure SDK)...');
10
+
11
+ // 1. Generate a test Business Tier License
12
+ const licenseKey = LicenseManager.generateLicenseKey({
13
+ tier: 'BUSINESS',
14
+ expiresAt: Date.now() + 30 * 24 * 3600 * 1000, // 30 days
15
+ clientId: 'acme-defense-corp'
16
+ });
17
+
18
+ // 2. Instantiate Engine with File Persistence
19
+ const engine = new ResonanceEngine({
20
+ licenseKey,
21
+ storage: new FileStorageAdapter('./scratch/quickstart_memory.json')
22
+ });
23
+
24
+ await engine.init();
25
+ console.log('✅ License Verified:', engine.getTelemetry().tier, '| Quota:', engine.getTelemetry().maxNodesQuota);
26
+
27
+ // 3. Ingest Law / Enterprise Knowledge
28
+ console.log('\n📥 Ingesting Document...');
29
+ await engine.ingest({
30
+ type: 'text',
31
+ title: '6100 Sayılı Hukuk Muhakemeleri Kanunu',
32
+ data: `
33
+ Madde 1: Göreve ilişkin kurallar, kamu düzenindendir.
34
+ Madde 2: Asliye hukuk mahkemelerinin görevi, dava konusunun değer ve miktarına bakılmaksızın, malvarlığı haklarına ilişkin davalarla şahıs varlığına ilişkin davaları görmektir.
35
+ Madde 3: İflas ve konkordato davaları asliye ticaret mahkemesinde görülür.
36
+ `
37
+ });
38
+
39
+ // 4. Ask Direct Fact Question (< 1ms reaction)
40
+ console.log('\n❓ Query: "Asliye hukuk mahkemelerinin görevi nedir?"');
41
+ const response = await engine.ask("Asliye hukuk mahkemelerinin görevi nedir?");
42
+ console.log('⚡ Route:', response.route, '| Latency:', response.latencyMs, 'ms');
43
+ console.log('💬 Answer:\n', response.answer);
44
+
45
+ // 5. Ask Math AST Gate
46
+ console.log('\n❓ Math Gate Query: "250 * 4 + (1500 / 3)"');
47
+ const mathRes = await engine.ask("250 * 4 + (1500 / 3)");
48
+ console.log('⚡ Math Answer:', mathRes.answer, '| Latency:', mathRes.latencyMs, 'ms (Bypassed LLM:', mathRes.llmBypassed, ')');
49
+
50
+ // 6. Inspect Telemetry
51
+ console.log('\n📊 Real-Time Telemetry:');
52
+ console.log(engine.getTelemetry());
53
+ }
54
+
55
+ main().catch(console.error);
package/sdk/index.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @resonance/core — Commercial B2B Infrastructure SDK
3
+ * Zero-Cloud, Sub-Millisecond Turkish Language Intelligence & Sovereign Memory Engine.
4
+ *
5
+ * @license Commercial
6
+ * @author Turkish Resonance AI Core Team
7
+ */
8
+
9
+ export { ResonanceEngine, ResonanceSDK } from './resonance_engine.js';
10
+ export { GHRLatticeMemory } from '../core/ghr_lattice_memory.js';
11
+ export { LicenseManager, LICENSE_TIERS } from './licensing.js';
12
+ export { IngestionEngine } from './ingestion.js';
13
+ export {
14
+ BaseStorageAdapter,
15
+ MemoryStorageAdapter,
16
+ FileStorageAdapter,
17
+ IndexedDBStorageAdapter
18
+ } from './storage.js';
19
+
20
+ // Default export
21
+ import { ResonanceEngine } from './resonance_engine.js';
22
+ export default ResonanceEngine;
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Resonance SDK - Modular Document Ingestion Engine
3
+ * Zero-Bloat Ingestion Pipeline: Text and CSV are native (0 KB external dependency).
4
+ * Heavy parsers (PDF, XLSX) are dynamically imported only on-demand when called.
5
+ *
6
+ * @license Commercial
7
+ * @author Turkish Resonance AI Core Team
8
+ */
9
+
10
+ export class IngestionEngine {
11
+ constructor(options = {}) {
12
+ this.maxChunkSize = options.maxChunkSize || 1000;
13
+ this.overlap = options.overlap || 100;
14
+ this._pdfModule = null;
15
+ this._xlsxModule = null;
16
+ }
17
+
18
+ /**
19
+ * Universal ingest method
20
+ * @param {Object} input
21
+ * @param {string|Buffer|ArrayBuffer} input.data - File content, text, or buffer
22
+ * @param {'text'|'pdf'|'csv'|'url'} [input.type='text']
23
+ * @param {string} [input.title='Untitled']
24
+ * @param {Object} [input.metadata={}]
25
+ * @returns {Promise<{ success: boolean, records: Array<{ query: string, content: string, confidence: number }>, stats: Object }>}
26
+ */
27
+ async ingest(input) {
28
+ if (!input || !input.data) {
29
+ throw new Error('Ingest input requires a valid "data" field');
30
+ }
31
+
32
+ const type = (input.type || this._detectType(input)).toLowerCase();
33
+ const title = input.title || 'Belge';
34
+ const metadata = input.metadata || {};
35
+
36
+ switch (type) {
37
+ case 'pdf':
38
+ return await this.ingestPdf(input.data, title, metadata);
39
+ case 'csv':
40
+ case 'tsv':
41
+ return this.ingestCsv(input.data, title, metadata);
42
+ case 'url':
43
+ return await this.ingestUrl(input.data, title, metadata);
44
+ case 'text':
45
+ default:
46
+ return this.ingestText(input.data, title, metadata);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Native lightweight plain text / markdown ingestion
52
+ */
53
+ ingestText(data, title, metadata = {}) {
54
+ const rawText = typeof data === 'string' ? data : data.toString('utf8');
55
+
56
+ // Split by double newlines OR numbered articles like "Madde 1:", "1. ", etc.
57
+ let paragraphs = [];
58
+ if (/Madde\s+\d+[:.]/i.test(rawText)) {
59
+ paragraphs = rawText.split(/(?=\bMadde\s+\d+[:.])/i).map(p => p.trim()).filter(p => p.length > 10);
60
+ } else {
61
+ paragraphs = rawText
62
+ .split(/\n{2,}|\r\n\r\n/)
63
+ .map(p => p.trim())
64
+ .filter(p => p.length > 15);
65
+ }
66
+
67
+ const records = [];
68
+ if (paragraphs.length <= 1) {
69
+ const content = `${title} — ${rawText.trim()}`;
70
+ records.push({
71
+ query: `${title} ${rawText.slice(0, 300)}`,
72
+ content,
73
+ confidence: 0.90,
74
+ metadata: { ...metadata, section: 1 }
75
+ });
76
+ } else {
77
+ paragraphs.forEach((para, idx) => {
78
+ const content = `${title} — Bölüm ${idx + 1} — ${para}`;
79
+ records.push({
80
+ query: `${title} ${para.slice(0, 300)}`,
81
+ content,
82
+ confidence: 0.90,
83
+ metadata: { ...metadata, section: idx + 1 }
84
+ });
85
+ });
86
+ }
87
+
88
+ return {
89
+ success: true,
90
+ records,
91
+ stats: { type: 'text', totalRecords: records.length, characters: rawText.length }
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Native lightweight CSV / TSV tabular ingestion (Zero-dependency)
97
+ */
98
+ ingestCsv(data, title, metadata = {}) {
99
+ const rawStr = typeof data === 'string' ? data : data.toString('utf8');
100
+ const lines = rawStr.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
101
+ if (lines.length === 0) return { success: false, records: [], stats: { totalRecords: 0 } };
102
+
103
+ const delimiter = lines[0].includes('\t') ? '\t' : (lines[0].includes(';') ? ';' : ',');
104
+ const headers = lines[0].split(delimiter).map(h => h.trim().replace(/^["']|["']$/g, '').toLowerCase());
105
+
106
+ // Map common Turkish / English columns
107
+ const titleIdx = headers.findIndex(h => /başlık|baslik|title|name|ad|proje|konu/i.test(h));
108
+ const descIdx = headers.findIndex(h => /açıklama|aciklama|desc|özet|ozet|detay|summary|detail/i.test(h));
109
+
110
+ const records = [];
111
+ for (let i = 1; i < lines.length; i++) {
112
+ const parts = lines[i].split(delimiter).map(p => p.trim().replace(/^["']|["']$/g, ''));
113
+ if (parts.length < 2) continue;
114
+
115
+ const itemTitle = titleIdx >= 0 && parts[titleIdx] ? parts[titleIdx] : `${title} #${i}`;
116
+ let itemDesc = '';
117
+
118
+ if (descIdx >= 0 && parts[descIdx]) {
119
+ itemDesc = parts[descIdx];
120
+ } else {
121
+ itemDesc = headers.map((h, hIdx) => `${h}: ${parts[hIdx] || ''}`).join(', ');
122
+ }
123
+
124
+ const content = `${itemTitle} — ${itemDesc}`;
125
+ records.push({
126
+ query: `${title} ${itemTitle} ${itemDesc}`,
127
+ content,
128
+ confidence: 0.88,
129
+ metadata: { ...metadata, row: i }
130
+ });
131
+ }
132
+
133
+ return {
134
+ success: true,
135
+ records,
136
+ stats: { type: 'csv', totalRecords: records.length, rows: lines.length - 1 }
137
+ };
138
+ }
139
+
140
+ /**
141
+ * On-Demand Dynamic PDF Ingestion (Keeps core SDK ultra-lightweight)
142
+ */
143
+ async ingestPdf(data, title, metadata = {}) {
144
+ let pdfjs = null;
145
+
146
+ // 1. Check browser global
147
+ if (typeof window !== 'undefined' && window.pdfjsLib) {
148
+ pdfjs = window.pdfjsLib;
149
+ } else {
150
+ // 2. Dynamic import in Node.js
151
+ try {
152
+ pdfjs = await import('pdfjs-dist/legacy/build/pdf.js');
153
+ } catch (e) {
154
+ throw new Error('PDF parsing requires "pdfjs-dist" package in Node.js or "pdf.min.js" in Browser. Install via: npm install pdfjs-dist');
155
+ }
156
+ }
157
+
158
+ let rawBuffer = data;
159
+ if (typeof data === 'string') {
160
+ // Handle base64 string
161
+ const binaryStr = atob(data);
162
+ const bytes = new Uint8Array(binaryStr.length);
163
+ for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
164
+ rawBuffer = bytes;
165
+ } else if (Buffer.isBuffer(data)) {
166
+ rawBuffer = new Uint8Array(data);
167
+ }
168
+
169
+ const doc = await pdfjs.getDocument({ data: rawBuffer }).promise;
170
+ const records = [];
171
+ const maxPages = Math.min(doc.numPages, 100);
172
+
173
+ for (let p = 1; p <= maxPages; p++) {
174
+ const page = await doc.getPage(p);
175
+ const textContent = await page.getTextContent();
176
+ const pageText = textContent.items.map(it => it.str).join(' ').trim();
177
+ if (pageText.length > 20) {
178
+ const content = `${title} — Sayfa ${p} — ${pageText}`;
179
+ records.push({
180
+ query: `${title} sayfa ${p} ${pageText.slice(0, 300)}`,
181
+ content,
182
+ confidence: 0.92,
183
+ metadata: { ...metadata, page: p, totalPages: doc.numPages }
184
+ });
185
+ }
186
+ }
187
+
188
+ return {
189
+ success: true,
190
+ records,
191
+ stats: { type: 'pdf', totalPages: doc.numPages, processedPages: records.length }
192
+ };
193
+ }
194
+
195
+ /**
196
+ * URL Ingestion (HTML clean text extraction)
197
+ */
198
+ async ingestUrl(url, title, metadata = {}) {
199
+ if (typeof fetch === 'undefined') {
200
+ throw new Error('URL ingestion requires global fetch API');
201
+ }
202
+
203
+ const resp = await fetch(url);
204
+ if (!resp.ok) throw new Error(`Failed to fetch URL: HTTP ${resp.status}`);
205
+
206
+ const contentType = resp.headers.get('content-type') || '';
207
+ if (contentType.includes('pdf') || url.endsWith('.pdf')) {
208
+ const arrayBuf = await resp.arrayBuffer();
209
+ return await this.ingestPdf(new Uint8Array(arrayBuf), title, metadata);
210
+ }
211
+
212
+ const rawHtml = await resp.text();
213
+ const cleanText = rawHtml
214
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ' ')
215
+ .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, ' ')
216
+ .replace(/<[^>]+>/g, ' ')
217
+ .replace(/&nbsp;/g, ' ')
218
+ .replace(/\s{2,}/g, ' ')
219
+ .trim();
220
+
221
+ return this.ingestText(cleanText, title, { ...metadata, sourceUrl: url });
222
+ }
223
+
224
+ _detectType(input) {
225
+ if (typeof input.data === 'string') {
226
+ if (input.data.startsWith('http://') || input.data.startsWith('https://')) return 'url';
227
+ if (input.data.includes(',') && input.data.includes('\n')) return 'csv';
228
+ return 'text';
229
+ }
230
+ if (input.filename) {
231
+ const ext = input.filename.split('.').pop().toLowerCase();
232
+ if (ext === 'pdf') return 'pdf';
233
+ if (ext === 'csv') return 'csv';
234
+ }
235
+ return 'text';
236
+ }
237
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Resonance SDK - Commercial Offline Licensing Engine (B2B Infrastructure)
3
+ * 100% Air-Gapped cryptographic license validation and quota enforcement.
4
+ *
5
+ * License format: RES-{TIER}-{EXPIRY_TIMESTAMP}-{MAX_NODES}-{HMAC_SIGNATURE}
6
+ * Example: RES-BUSINESS-1798761600000-50000-a1b2c3d4e5f6...
7
+ *
8
+ * @license Commercial
9
+ * @author Turkish Resonance AI Core Team
10
+ */
11
+
12
+ import crypto from 'crypto';
13
+
14
+ export const LICENSE_TIERS = {
15
+ STARTER: {
16
+ name: 'Starter',
17
+ maxNodes: 1000,
18
+ allowedRuntimes: ['node', 'browser'],
19
+ features: ['math', 'text', 'basic_memory', 'telemetry'],
20
+ wasmRequired: false
21
+ },
22
+ BUSINESS: {
23
+ name: 'Business',
24
+ maxNodes: 50000,
25
+ allowedRuntimes: ['node', 'browser', 'electron', 'edge'],
26
+ features: ['math', 'morphology', 'text', 'basic_memory', 'pdf', 'csv', 'living_memory', 'smart_summarizer', 'conflict_resolution', 'telemetry'],
27
+ wasmRequired: false
28
+ },
29
+ SOVEREIGN: {
30
+ name: 'Sovereign',
31
+ maxNodes: Infinity,
32
+ allowedRuntimes: ['node', 'browser', 'electron', 'edge', 'tactical_hardware'],
33
+ features: ['math', 'morphology', 'text', 'basic_memory', 'pdf', 'csv', 'living_memory', 'smart_summarizer', 'conflict_resolution', 'wasm_simd', 'custom_rules', 'multi_graph', 'telemetry'],
34
+ wasmRequired: true
35
+ }
36
+ };
37
+
38
+ export class LicenseManager {
39
+ /**
40
+ * @param {string} [vendorSecret] - Master secret for signing/verifying licenses.
41
+ */
42
+ constructor(vendorSecret = 'RESONANCE_SOVEREIGN_KEY_2026_AIRGAPPED_HMAC') {
43
+ this._secret = vendorSecret;
44
+ this.currentLicense = null;
45
+ this.tier = LICENSE_TIERS.STARTER;
46
+ this.expiryDate = null;
47
+ this.maxNodes = 1000;
48
+ this.isValid = false;
49
+ this.nodeCount = 0;
50
+ }
51
+
52
+ /**
53
+ * Generate an offline commercial license key (Vendor use)
54
+ * @param {Object} opts
55
+ * @param {'STARTER'|'BUSINESS'|'SOVEREIGN'} opts.tier
56
+ * @param {number|Date} opts.expiresAt
57
+ * @param {number} [opts.maxNodes]
58
+ * @param {string} [opts.clientId]
59
+ * @returns {string}
60
+ */
61
+ static generateLicenseKey(opts, secret = 'RESONANCE_SOVEREIGN_KEY_2026_AIRGAPPED_HMAC') {
62
+ const tierKey = (opts.tier || 'STARTER').toUpperCase();
63
+ if (!LICENSE_TIERS[tierKey]) throw new Error(`Invalid tier: ${tierKey}`);
64
+
65
+ const tierDef = LICENSE_TIERS[tierKey];
66
+ const expiry = opts.expiresAt instanceof Date ? opts.expiresAt.getTime() : (opts.expiresAt || (Date.now() + 365 * 24 * 3600 * 1000));
67
+ const maxNodes = opts.maxNodes || (tierDef.maxNodes === Infinity ? 0 : tierDef.maxNodes);
68
+ const clientId = (opts.clientId || 'b2b-customer').replace(/[^a-zA-Z0-9]/g, '');
69
+
70
+ const payload = `${tierKey}:${expiry}:${maxNodes}:${clientId}`;
71
+ const hmac = crypto.createHmac('sha256', secret);
72
+ hmac.update(payload);
73
+ const signature = hmac.digest('hex').slice(0, 32);
74
+
75
+ return `RES-${tierKey}-${expiry}-${maxNodes}-${clientId}-${signature}`;
76
+ }
77
+
78
+ /**
79
+ * Cryptographically verify an offline license key without cloud ping
80
+ * @param {string} key
81
+ * @returns {{ valid: boolean, tier: Object, expiresAt: Date, maxNodes: number, reason?: string }}
82
+ */
83
+ verifyLicense(key) {
84
+ if (!key || typeof key !== 'string') {
85
+ return this._fallbackCommunityLicense('No license key provided');
86
+ }
87
+
88
+ const parts = key.trim().split('-');
89
+ if (parts.length !== 6 || parts[0] !== 'RES') {
90
+ return this._fallbackCommunityLicense('Invalid license key format');
91
+ }
92
+
93
+ const [_, tierKey, expiryStr, maxNodesStr, clientId, signature] = parts;
94
+ const tierDef = LICENSE_TIERS[tierKey];
95
+ if (!tierDef) {
96
+ return this._fallbackCommunityLicense(`Unrecognized tier: ${tierKey}`);
97
+ }
98
+
99
+ const expiryTime = parseInt(expiryStr, 10);
100
+ const maxNodes = parseInt(maxNodesStr, 10);
101
+
102
+ // Verify cryptographic signature
103
+ const payload = `${tierKey}:${expiryTime}:${maxNodes}:${clientId}`;
104
+ const hmac = crypto.createHmac('sha256', this._secret);
105
+ hmac.update(payload);
106
+ const expectedSig = hmac.digest('hex').slice(0, 32);
107
+
108
+ // Constant-time comparison
109
+ if (!crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expectedSig, 'hex'))) {
110
+ return this._fallbackCommunityLicense('Cryptographic signature verification failed');
111
+ }
112
+
113
+ // Check expiration
114
+ if (Date.now() > expiryTime) {
115
+ return this._fallbackCommunityLicense(`License expired on ${new Date(expiryTime).toISOString()}`);
116
+ }
117
+
118
+ // Valid license
119
+ this.currentLicense = key;
120
+ this.tier = tierDef;
121
+ this.expiryDate = new Date(expiryTime);
122
+ this.maxNodes = maxNodes === 0 ? Infinity : maxNodes;
123
+ this.isValid = true;
124
+
125
+ return {
126
+ valid: true,
127
+ tier: tierDef,
128
+ expiresAt: this.expiryDate,
129
+ maxNodes: this.maxNodes,
130
+ clientId
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Check if a feature is authorized by the current license
136
+ * @param {string} featureName
137
+ * @returns {boolean}
138
+ */
139
+ canUseFeature(featureName) {
140
+ return this.tier.features.includes(featureName);
141
+ }
142
+
143
+ /**
144
+ * Check if adding N nodes exceeds quota
145
+ * @param {number} currentTotal
146
+ * @param {number} addingCount
147
+ * @returns {boolean}
148
+ */
149
+ checkNodeQuota(currentTotal, addingCount = 1) {
150
+ if (this.maxNodes === Infinity) return true;
151
+ return (currentTotal + addingCount) <= this.maxNodes;
152
+ }
153
+
154
+ _fallbackCommunityLicense(reason) {
155
+ this.currentLicense = null;
156
+ this.tier = LICENSE_TIERS.STARTER;
157
+ this.expiryDate = null;
158
+ this.maxNodes = LICENSE_TIERS.STARTER.maxNodes;
159
+ this.isValid = false;
160
+ return {
161
+ valid: false,
162
+ tier: this.tier,
163
+ expiresAt: null,
164
+ maxNodes: this.maxNodes,
165
+ reason
166
+ };
167
+ }
168
+ }